Project structure
The TanStack + Neon monorepo layout and where to change routes, services, schema, auth, the admin app, emails, jobs, MCP and tests.
The repository is a pnpm and Turborepo monorepo with four workspace apps and one shared package. Route loaders, actions and HTTP controllers all call the same authorization-aware services, and server rendering never fetches from its own HTTP API. The admin app compiles its own route graph and server but imports its server modules from apps/web/src/server, so there is one copy of the auth, database and authorization code.
Tree
| Path | Responsibility |
|---|---|
apps/web (@nextbase/web) | React Router UI, Express server, Better Auth, /api/v1, domain services, the User MCP server and the outbox worker. Deploys as the product on port 3000 |
apps/admin (@nextbase/admin) | The administration app: its own route graph, Express server, users list, audited impersonation and the Admin MCP server. Deploys separately on port 3001 |
apps/database (@nextbase/database) | Drizzle schema, reviewed SQL migrations, RLS policies, role bootstrap, seed, db:check and the admin grant script |
apps/email (@nextbase/email) | React Email templates (VerificationEmail, TeamInvitationEmail) with a preview server. apps/web doesn't depend on it yet; the auth emails are inline HTML |
packages/typescript-config | Shared base.json and react.json TypeScript configs |
docs | mcp-connections.md (OAuth and MCP protocol details) and admin-app.md (origins, impersonation and admin deployment) |
scripts/agents | sync.mjs generates every AI tool config from AGENTS.md, .agents/config.json and .agents/skills/ |
.agents | Hand-edited source for the generated agent files: MCP servers for coding tools and the new-page, db-migration and mcp-tool skills |
Where to make changes
Routes and pages
Routes are declared in apps/web/app/routes.ts with React Router's route(), layout() and index() helpers, and each file lives in apps/web/app/routes/. Public pages are /, /login, /sign-up, /forgot-password, /reset-password and /auth/impersonation. routes/app-layout.tsx wraps /app, /app/workspaces, /app/projects and /app/settings and calls requireSession. Add a route entry, create the file, then run pnpm typecheck to regenerate the +types route types. The new-page skill in .agents/skills/ describes the same steps for an AI coding tool.
Admin pages live in apps/admin/app/routes.ts and apps/admin/app/routes/: / redirects to /app-admin, /login is the admin sign-in, admin-layout.tsx wraps /app-admin and /app-admin/users, and the remaining sections share admin-section.tsx as a placeholder. Every protected loader and action calls requireAppAdmin itself; the layout check alone is not enough.
Shared UI lives in apps/web/app/components/ (app-shell.tsx, ui.tsx, logo.tsx, theme-toggle.tsx). The admin app has its own copies under apps/admin/app/components/ and apps/admin/app/shared/, and re-exports the Better Auth client from web through shared-auth-client.ts. Client-only helpers (auth-client.ts, auth-return.ts) are in apps/web/app/lib/.
Path aliases in web: ~/* is app/*, @server/* is src/server/*, @contracts/* is src/contracts/*. In the admin app @server/* resolves to ../web/src/server/*.
Server and data access
Loaders and actions get requestId and getPrincipal() from apps/web/src/server/request-context.ts and call services in apps/web/src/server/services/ (workspaces.ts ships; add yours next to it). The same services back the HTTP API in apps/web/src/server/api.ts, mounted at /api/v1 with GET /bootstrap, GET /workspaces and POST /workspaces. Mutating API requests must be JSON and same-origin; failures are application/problem+json with a requestId. Input shapes are zod schemas in apps/web/src/contracts/.
Put new business rules in a service, wrap protected work in withActorTransaction from apps/web/src/server/actor-transaction.ts, and write an audit_events row for security-relevant mutations. Don't query app.* tables from a route file or outside an actor transaction.
Schema and migrations
The Drizzle schema is apps/database/src/schema.ts: Better Auth tables in public (user, session, account, verification, rate_limit, mcp_records) and domain tables in the app schema. Migrations are plain SQL files in apps/database/migrations/, applied in filename order by apps/database/src/migrate.ts, which stores a SHA-256 checksum per file in public.nextbase_migrations. The flow is:
- Edit
schema.ts. pnpm --filter @nextbase/database exec drizzle-kit generate --name <change>writesmigrations/NNNN_<change>.sqlplus themeta/snapshot and journal.- Append grants,
enableandforce row level security, and policies to that SQL file, following0001_security.sqland0003_user_admin_mcp.sql. Newapp.*tables need RLS and column-scopedgrant updatetoapp_runtime. pnpm db:migrate && pnpm db:check, updatesrc/seed.tsif needed, add RLS integration tests. Add the table to the list insrc/check.tssodb:checkkeeps verifying it.
Never edit an applied migration; add a new one. Local role bootstrap and PgBouncer config live in apps/database/docker/ (postgres-init.sql, pgbouncer.ini, userlist.txt).
Authentication
Everything Better Auth is in apps/web/src/server/auth.ts, with helpers next to it: principal.ts, session.ts, request-context.ts, authorization.ts (requireSession, requireAppAdmin) and support-handoff.ts for the impersonation handoff records. Environment validation is apps/web/src/server/env.ts. See authentication.
Emails
Templates are React components in apps/email/src/templates/ (verification.tsx, team-invitation.tsx, layout.tsx), exported from apps/email/src/index.ts. Preview them with pnpm --filter @nextbase/email dev (port 3001, which clashes with the admin app when both run). Sending goes through enqueueEmail in apps/web/src/server/email.ts, which writes an email.send outbox job; sendEmailNow is the nodemailer transport used by the worker. To use the templates from web, add "@nextbase/email": "workspace:*" to apps/web/package.json and render them before calling enqueueEmail.
Background jobs and the worker
apps/web/worker.ts claims rows from app.outbox_jobs with two-minute leases renewed by a heartbeat, retries with exponential backoff and marks jobs dead after 10 attempts. The only job type today is email.send. Add a new type by inserting a job with an idempotency key from a service and handling it in executeJob. runOutboxBatch is exported for the /internal/jobs/run cron and for request-time dispatch with waitUntil on Vercel.
MCP
apps/web/src/mcp/ holds core.ts (protocol and tools), adapter.ts (this kit's session, workspace and project access), pg-store.ts (storage) and express.ts (the Express bridge). apps/web/server.ts mounts the user audience and apps/admin/server.ts mounts the admin audience. See agent connections for how to add a tool.
Tests
| Kind | Where | Command |
|---|---|---|
| Unit and component | *.test.ts and *.test.tsx next to the code (web uses jsdom and Testing Library, setup in apps/web/src/test/setup.ts) | pnpm test |
| Database and RLS | apps/database/src/rls.integration.test.ts, apps/web/src/server/services/workspaces.integration.test.ts, apps/web/src/mcp/pg-store.integration.test.ts | pnpm test:integration (needs pnpm infra:start and pnpm db:migrate) |
| Admin impersonation | apps/admin/app/routes/admin-users.integration.test.ts | pnpm --filter @nextbase/admin test:integration |
| Everything before a push | pnpm verify (format check, lint, typecheck, unit tests, build) |
Single file: pnpm --filter @nextbase/web exec vitest run <path>. TESTING.md asks for a meaningful assertion for new behaviour and both the allowed and denied side of every authorization branch. CI (.github/workflows/test.yml) starts postgres and pgbouncer, runs migrations, seed and db:check, then typecheck, unit tests, integration tests and the build on every pull request, and agent-config.yml fails when the generated agent files are stale (pnpm agents:check).
Agent connections
User MCP and Admin MCP in the TanStack + Neon starter: settings pages, endpoints on the web and admin origins, tools, OAuth behaviour and where the code lives.
Deployment
Provision Neon roles, apply migrations, and deploy the TanStack + Neon starter's web and admin apps as two Vercel projects.