Project structure
The TanStack + Supabase monorepo layout and where to make common changes.
The repository is a pnpm workspace (apps/* and packages/*) orchestrated by Turborepo. Four apps and one shared package.
The apps
| Path | What it is |
|---|---|
apps/web | The product: React Router 8 framework-mode app (appDirectory: "src") served by Express 5 on port 3000. Public marketing pages, auth, /app/*, the public API routes and the User MCP. In development Express mounts Vite in middleware mode; in production it serves build/client and imports build/server/index.js, bundled to dist/index.js by tsup. |
apps/admin | The administration app, same stack, port 3002 locally and its own deployment. Serves /admin/*, admin auth pages, the marketing media upload route and the Admin MCP. Shares the Supabase project with apps/web; WEB_APP_URL points it at the product for support sign-in links. Described in docs/admin-app.md. |
apps/database | The Supabase project: config.toml (stack engine enabled), hand-written migrations, pgTAP tests, local seed and the generated types exported as database/types. The Supabase CLI 2.121.0 is a dependency here. |
apps/email | React Email template catalogue (emails/*.tsx) with a preview server on port 3001. Not imported by the apps. |
packages/typescript-config | Shared strict tsconfig.base.json. |
apps/admin is a trimmed copy of apps/web: the same server/index.ts shape, the same src/.server/ modules (auth.ts, env.ts, integrations.ts, mail.ts, request-context.ts, plus web-app-url.ts), the same src/mcp/ code with a different audience, and only the marketing feature module it needs for content administration. Conventions documented for apps/web below apply to both unless noted.
Where to make changes
Routes and pages
Routes are registered explicitly in apps/web/src/routes.ts and apps/admin/src/routes.ts with index, route and layout. A file in src/routes/ does nothing until it is listed there. Route modules export loader, action, meta and a default component. API resource routes are .ts files named api-*.ts.
In the web app, /app/* pages nest under routes/app-layout.tsx; navigation entries live in src/components/app-shell.tsx (productItems) and src/components/site-header.tsx for public pages. / and /terms are prerendered (react-router.config.ts), so keep their loaders free of per-user data. In the admin app, /admin/* pages nest under routes/admin-layout.tsx and the navigation is in its own src/components/app-shell.tsx. The repository's new-page skill in .agents/skills/ walks through adding a page.
Server and data access
Loaders and actions get RequestServices from context.get(servicesContext) (src/.server/request-context.ts): supabase() for user-scoped reads and writes under RLS, serviceSupabase() only after requireUser or requireAdmin plus an explicit authorization check. Server-only code belongs in src/.server/ or *.server.ts files, which React Router strips from client bundles. Mutations are React Router actions with a hidden intent field, validated with Zod (shared schemas in src/features/core/core-utils.ts), returning { error } or { success }.
Feature data access for marketing content sits in src/features/marketing/*.server.ts (the public reads in the web app, the CMS writes in the admin app). Optional integrations are read through src/.server/integrations.ts; core env validation is in src/.server/env.ts.
Schema and migrations
pnpm --filter database exec supabase migration new <snake_case_name>
pnpm --filter database exec supabase db reset
pnpm gen-types-local
pnpm test-dbMigrations in apps/database/supabase/migrations/ are the only source of truth; supabase/schemas/init.sql is a stale snapshot, so do not diff against it. Never edit a shipped migration. Every new table needs RLS, named policies, explicit grants and indexes. Add pgTAP coverage in supabase/tests/NN_*.sql and commit the regenerated apps/database/lib/database.types.ts. The db-migration skill covers this flow and reads the local database address from supabase status --output-format json.
Authentication
Guards are in src/.server/auth.ts of each app; auth pages are routes/login.tsx, routes/sign-up.tsx (web only), routes/forgot-password.tsx, routes/reset-password.tsx, routes/auth-callback.tsx and routes/auth-confirm.ts. Local Auth settings (confirmations, redirect URLs for both ports, providers) are in apps/database/supabase/config.toml. See authentication.
Emails
App emails are built as HTML and text in src/.server/mail.ts and sent through Resend when RESEND_API_KEY and EMAIL_FROM are set. The templates in apps/email/emails/ are a design catalogue; to send one from an app, add @nextbase/email as a workspace dependency, render it with @react-email/render and pass the HTML to sendTransactionalEmail. Supabase Auth emails are configured in the Supabase project, not here.
Billing and background work
Stripe lives in apps/web/src/.server/billing/ (stripe.ts, payment-gateway.ts) with the webhook route at routes/api-stripe-webhook.ts. There is no separate worker or queue in this kit; the Express servers handle everything, and the Stripe webhook on the web app is the only inbound background trigger. The admin app's /admin/billing reads the same tables.
MCP
Each app's MCP and OAuth server is its src/mcp/, mounted by its server/index.ts before React Router. The web app serves the User MCP, the admin app the Admin MCP. Follow the mcp-tool skill and agent connections.
Local environment
setup.sh (pnpm setup) and scripts/sync-supabase-env.ts (pnpm supabase:sync-env) are the only scripts that touch .env files: the sync script reads env.API_URL, env.ANON_KEY and env.SERVICE_ROLE_KEY from supabase status --output-format json and writes them into apps/web/.env and apps/admin/.env. Because config.toml enables the CLI stack engine, there are no fixed local ports to hard-code anywhere.
Tests
- Vitest (jsdom) unit tests sit next to code as
*.test.ts(x)inapps/web/srcandapps/admin/src. Runpnpm test, or one file withpnpm --filter web exec vitest run <file>(or--filter admin). *.integration.test.tsfiles skip unless theMCP_TEST_SUPABASE_*variables point at a local project.- Playwright specs are in
apps/web/e2e/;pnpm test:e2estarts the web and admin dev servers unlessCIis set, and the seeded-account spec signs in on the admin app. - pgTAP tests are in
apps/database/supabase/tests/;pnpm test-dbneeds a running local project. pnpm checkrunsformat:check,lint,typecheck,testandbuildacross all apps. Each app build also runs itsscripts/check-built-auth.mjsandscripts/package-vercel.mjs.
AI coding tool configuration
AGENTS.md (root and per app, including apps/admin/AGENTS.md), .agents/config.json and .agents/skills/ are the hand-edited sources. CLAUDE.md, GEMINI.md, .mcp.json, .cursor/, .vscode/mcp.json and the other tool files are generated by pnpm agents:sync; CI runs pnpm agents:check. Edit the sources, not the generated files. The supabase-local MCP server URL in .agents/config.json should be the MCP_URL of your running local project.