Skip to content
rafaelphhPublic

About

yt- b

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

camelAI Starter Template

Full-stack React app with SSR, Durable Objects, and SQLite. Built on React Router 7 and Cloudflare Workers.

Architecture Default

Use React Router 7 in framework mode (successor to Remix): data reads in route loader(), mutations in action(), and forms/fetchers for server-driven updates. Prefer this over SPA-style client fetching patterns by default.

Quick Start

bun dev          # Start dev server
bun run test     # Run tests
bun run build    # Build for production
bun run deploy   # Deploy to Cloudflare

Project Structure

app/
  routes/           # React Router routes (loaders, actions, components)
  schemas/          # Zod schemas (shared between routes and DOs)
workers/
  app.ts            # Worker entry point (exports DOs)
  example-do.ts     # Example Durable Object with SQLite
  tests/            # Vitest tests for Durable Objects
wrangler.jsonc      # Cloudflare config (bindings, migrations)
vitest.config.ts    # Vitest config for Workers pool

Key Patterns

Accessing Cloudflare Bindings in Loaders/Actions

export async function loader({ context }: Route.LoaderArgs) {
  // Access any binding via context.cloudflare.env
  const id = context.cloudflare.env.EXAMPLE_DO.idFromName("global");
  const stub = context.cloudflare.env.EXAMPLE_DO.get(id);

  // Call RPC methods directly (not fetch!)
  const data = await stub.listContacts();
  return { data };
}

SQL Data Proxy Service Binding

The starter includes a DATA_PROXY service binding.

  • Local dev: binding points to LocalDataProxyService (workers/data-proxy.ts), which forwards to DATA_PROXY_URL when set
  • camelAI deploy: platform rewrites DATA_PROXY to its internal service binding
export async function loader({ context }: Route.LoaderArgs) {
  const result = await context.cloudflare.env.DATA_PROXY.mysqlQuery({
    mode: "read",
    host: "db.example.com",
    user: "user",
    password: "pass",
    database: "analytics",
    query: "SELECT * FROM orders WHERE customer_id = ?",
    params: [123],
  });

  if (!result.ok) {
    throw new Error(result.error.message);
  }

  return { rows: result.data.recordset ?? [] };
}

Durable Object with SQLite

export class MyDO extends DurableObject<Env> {
  private sql: SqlStorage;

  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env);
    this.sql = ctx.storage.sql;

    // Create tables (idempotent)
    this.sql.exec(`CREATE TABLE IF NOT EXISTS items (...)`);
  }

  // RPC method - callable from routes
  async listItems() {
    return this.sql.exec("SELECT * FROM items").toArray();
  }
}

Shared Zod Schemas

Put schemas in app/schemas/ and import in both routes and DOs:

// app/schemas/contact.ts
export const CreateContactSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
});

// In route action:
const result = CreateContactSchema.safeParse(formData);

// In DO: use the same types
async createContact(input: CreateContactInput) { ... }

HydrateFallback for Loading States

// Shows during initial hydration (SSR → client)
export function HydrateFallback() {
  return <div>Loading...</div>;
}

Testing

bun run test  # Run tests with vitest

Testing Durable Objects

Tests use @cloudflare/vitest-pool-workers for isolated DO testing:

import { describe, it, expect } from "vitest";
import { env } from "cloudflare:test";

describe("MyDO", () => {
  it("creates and retrieves items", async () => {
    // Get a DO stub from env bindings
    const id = env.MY_DO.idFromName("test");
    const stub = env.MY_DO.get(id);

    // Call RPC methods directly
    await stub.createItem({ name: "Test" });
    const items = await stub.listItems();

    expect(items).toHaveLength(1);
  });
});

See workers/tests/example-do.test.ts for a complete example.

Known limitation: Tests expecting DO methods to throw exceptions may cause "Failed to pop isolated storage stack frame" errors. Test error handling in route actions instead.

Adding a New Durable Object

  1. Create the class in workers/my-do.ts
  2. Export from workers/app.ts: export { MyDO } from "./my-do"
  3. Add binding to wrangler.jsonc:
    "durable_objects": {
      "bindings": [{ "name": "MY_DO", "class_name": "MyDO" }]
    }
  4. Add migration (increment tag):
    "migrations": [
      { "tag": "v1", "new_sqlite_classes": ["ExampleDO"] },
      { "tag": "v2", "new_sqlite_classes": ["MyDO"] }
    ]
  5. postinstall runs wrangler types automatically; re-run bun run cf-typegen after binding changes to update Env types

Adding shadcn/ui Components

Use bunx to run the shadcn CLI:

bunx --bun shadcn@latest add button card input

Components install to app/components/ui/.

About

yt- b

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages