Project structure
The monorepo layout of the TanStack + Cloudflare D1 starter and where to make common changes.
The repository is a pnpm workspace orchestrated by Turborepo. Five apps and two shared packages live under apps/* and packages/*. Run every command from the repository root.
The five apps
| Package | Path | What it is |
|---|---|---|
@nextbase/web | apps/web | The customer-facing React Router app and the Worker that serves it, including the User MCP. Vite builds it with @cloudflare/vite-plugin; wrangler.jsonc is the Worker config. |
@nextbase/admin | apps/admin | The administrator app, a second React Router app and Worker on its own origin (port 5174 locally). It has its own routes, login and Better Auth secret, reuses the web app's server modules through the ~ alias, and serves the Admin MCP. |
@nextbase/jobs | apps/jobs | The background Worker: Queue consumer, dead-letter consumer, /health endpoint and two crons (* * * * * for outbox recovery and cleanup, 0 3 * * * for daily reconciliation and retention). |
@nextbase/database | apps/database | Drizzle schema, generated D1 types, the hand-written SQL migrations and the seed. |
@nextbase/email | apps/email | The 41 React Email templates and the renderer the jobs Worker calls. pnpm dev also runs its preview on port 3001. |
packages/contracts holds the shared zod schemas and types: auth (Principal, WorkspaceContext, roles), job envelopes, problem details, observability helpers and workspace contracts. packages/typescript-config holds the shared tsconfig bases.
infra/admin-domain and infrastructure/demo-domain are two small Cloudflare Pages projects that proxy the demo's custom domains to the admin and web Workers through a service binding. You only need them if you attach a domain the same way.
Where to make changes
Routes and pages
Web routes are declared in apps/web/app/routes.ts and implemented in apps/web/app/routes/. Public marketing, blog, changelog, docs, feedback and roadmap pages share routes/public.tsx. Signed-in pages sit under routes/app-layout.tsx: dashboard.tsx, workspaces.tsx, workspace.tsx (/workspace/:workspaceSlug/*), project.tsx and user.tsx (/user/*). API routes are the api-*.ts files. impersonate.ts and stop-impersonating.ts are the two halves of the cross-origin impersonation handoff. Add a route in routes.ts and a matching file next to the others.
Admin routes are declared in apps/admin/app/routes.ts: login.tsx, the password and verification pages, admin.tsx (/app-admin/*, one module that switches on the section), api-auth.ts, api-media.ts, api-health.ts and api-admin-insights.ts. The web app has no /app-admin routes.
Reusable UI lives in apps/web/app/components/, with components/ui/ for primitives. Hooks are in app/hooks/, small utilities in app/lib/. The admin app imports these through the same ~ alias.
Server logic and data access
Everything that runs only on the Worker is in apps/web/app/server/*.server.ts. Start with session.server.ts for the principal, authorization.server.ts for workspace capabilities, repositories.server.ts for tenant-scoped reads and writes, workspaces.server.ts and project-mutations.server.ts for the core domain, payments.server.ts for Stripe and queue.server.ts for nudging the Queue. The admin app adds apps/admin/app/server/admin-magic-link.server.ts for issuing magic links to users from the console.
Keep the rule from AGENTS.md: resolve a WorkspaceContext from the session and route slug first, then pass it into repositories. Mutations that need atomicity use a single D1 batch() and write audit and outbox rows in that batch.
Schema and migrations
Edit the Drizzle tables in apps/database/src/schema/ (auth.ts, core.ts, billing.ts, content.ts, system.ts). Run pnpm db:generate to produce SQL under apps/database/migrations/generated/, then add the reviewed statements as the next numbered file in apps/database/migrations/ (the folder Wrangler applies, currently 0001 to 0025; 0025_user_admin_mcp.sql creates the mcp_records table for agent connections). Apply with pnpm db:migrate and run pnpm test:integration.
Migrations are forward-only. FTS5 search tables, the development email inbox and triggers live in the hand-written migrations rather than in Drizzle tables.
Authentication
apps/web/app/server/auth.server.ts for Better Auth options, plugins and hooks; auth-handler.server.ts and auth-route.server.ts for request handling; impersonation.server.ts for the read-only impersonation rule; apps/web/workers/auth-rate-limiter.ts for the limiter. The admin app reuses all of these with ADMIN_APP=true. Details are on the authentication page.
Emails
Templates are in apps/email/emails/, rendered by apps/email/src/render.tsx. Sending starts in apps/web/app/server/email.server.ts (enqueueEmail) and finishes in apps/jobs/src/handlers.ts (sendEmail), which chooses Resend or the local inbox. To add an email, add the template, then call enqueueEmail with its name and props.
Background jobs and workers
Job types and the envelope schema are in packages/contracts/src/jobs.ts. The dispatcher is dispatchJob in apps/jobs/src/handlers.ts; leases, receipts and retries are in outbox.ts. A new job needs a type, a handler case, a stable idempotency key, and a producer that inserts the outbox row from the web or admin app. The Durable Objects (RealtimeRoom, AuthRateLimiter) are in apps/web/workers/; the admin and jobs Workers reach them through script_name bindings, as apps/jobs/AGENTS.md describes.
MCP
apps/web/src/mcp/ with core.ts, adapter.ts and d1-store.ts, mounted by both apps/web/workers/app.ts and apps/admin/workers/app.ts. See agent connections.
Tests
| Kind | Where | Command |
|---|---|---|
| Unit tests | *.test.ts next to the code in every package | pnpm test |
| D1 integration tests (workerd and Miniflare) | apps/web/test/*.integration.test.ts | pnpm test:integration |
| Browser smoke tests (web and admin) | apps/web/e2e/*.spec.ts | pnpm test:e2e |
| Everything plus lint, format, types and build | pnpm verify |
CI (.github/workflows/ci.yml) runs pnpm verify, pnpm test:integration and the Playwright suite on every pull request. agent-config.yml checks that the generated AI tool configuration is in sync (pnpm agents:check). Use pnpm changeset to describe a user-visible change; release.yml maintains the version PR.
Configuration files
apps/web/wrangler.jsonc, apps/admin/wrangler.jsonc and apps/jobs/wrangler.jsonc define bindings and vars per environment. The root package.json holds the scripts you will use most (db:*, cf:*, admin:dev, verify). turbo.json wires build, dev, test and test:integration across packages. Lint and format use .oxlintrc.json and .oxfmtrc.json.