Project structure
The Next.js + Neon monorepo at a glance and where to make changes for routes, the admin app, data, schema, auth, emails, jobs, MCP and tests.
The kit is a pnpm workspace with Turborepo. Four apps and one shared package live under apps/ and packages/. Everything is TypeScript, formatted with Prettier and linted with oxlint.
The tree
| Package | Name | What it is |
|---|---|---|
apps/web | web | The customer Next.js 16 app on port 3000: marketing pages, auth, workspaces and projects, server actions, domain logic, the User MCP, CLIs and Playwright tests |
apps/admin | admin | The administrator Next.js 16 app on port 3001: admin sign-in, the /app-admin panel, feedback and roadmap moderation, the Admin MCP and the impersonation handoff |
apps/database | database | Drizzle schema, Postgres migrations, migrate and seed scripts, generated types, the Docker Compose stack |
apps/email | email | React Email templates and the render helpers the web app imports |
packages/typescript-config | Shared tsconfig.base.json |
apps/database/drizzle/ holds the historical MySQL migrations from the PlanetScale version. They are never applied; leave them alone.
How the admin app shares code
apps/admin/tsconfig.json maps @/* to ./src/* first and ../web/src/* second, so an import such as @/lib/auth/session or @/components/ui/button resolves to the web app's file unless the admin app has its own copy. The admin app therefore has no lib/, no rsc-data/ and only a handful of components; everything it owns is routes under src/app/, administrative queries and actions under src/data/admin/, and the admin sidebar and breadcrumbs under src/components/. Its next.config.ts sets the Turbopack root to the repository so those cross-app imports compile. Keep customer-facing code in apps/web; put administrative mutations only in apps/admin.
Where to make changes
Routes and pages
- Public marketing pages:
apps/web/src/app/(external-pages)/. Nav links are incomponents/navigation-menu/external-navbar/constants.ts. - Signed-in customer area:
apps/web/src/app/(dynamic-pages)/(authenticated-pages)/. Workspace pages sit under(application-pages)/workspace/[workspaceSlug]/, project pages under(application-pages)/project/[projectSlug]/, and account settings underuser/. Workspace pages have parallel@navbarand@sidebarslots withdefault.tsxfallbacks. - Auth pages:
(login-pages)and(misc-login-pages). Public feedback and roadmap:(updates-pages). The impersonation landing page isapps/web/src/app/auth/impersonation/page.tsx. - Admin panel:
apps/admin/src/app/(dynamic-pages)/(authenticated-pages)/app-admin/(admin-pages)/withusers,workspaces,settings,payment-gatewayandmarketing/{authors,blog,tags,changelog}, plus@navbarand@sidebarslots. Feedback moderation isapps/admin/src/app/feedback/, roadmap moderationapps/admin/src/app/roadmap/, and the admin sign-in pageapps/admin/src/app/login/page.tsx. - Route handlers:
apps/web/src/app/api/. Better Auth is atapi/auth/[...all](and again in the admin app), the Stripe webhook atapi/stripe/webhooks, uploads underapi/storage/uploads, the realtime token atapi/realtime/token, the impersonation redemption atapi/impersonation, and the cache refresh the admin app calls atapi/internal/admin-revalidate. The admin app addsapi/admin/marketing/mediafor CMS uploads. - Sidebar links:
apps/web/src/components/sidebar-workspace-nav.tsxandsidebar-platform-nav.tsxin the web app;apps/web/src/components/sidebar-admin-panel-nav.tsxrenders the Open admin panel link;apps/admin/src/components/sidebar-admin-panel-nav.tsxis the admin app's menu.
A page exports metadata, awaits params, parses them with zod from utils/zod-schemas/params.ts, and wraps dynamic content in <Suspense>. cacheComponents is on in both apps, so cached reads use "use cache" with tags from typed-cache-tags.ts. Admin data must stay live and permission-scoped; never prerender it into public caches.
Server and data access
apps/web/src/data/{anon,user}/andapps/admin/src/data/admin/are"use server"modules with queries and next-cool-action actions.authActionClientandadminActionClientcome fromapps/web/src/lib/safe-action.ts.apps/web/src/rsc-data/wraps those queries with Reactcache()for server components, for examplegetCachedWorkspaceBySlug.- Reusable rules live in
lib/domain/(policies with tests,events.tsfor audit and outbox rows) andlib/services/. - Admin mutations that change public content call
refreshWebAfterAdminMutationfromapps/web/src/lib/admin-revalidation.tsso the web app revalidates. - Import
db,pool,schemaandnewIdfrom thedatabasepackage. Don't create new pools.
Schema and migrations
- Edit
apps/database/src/schema.ts, thenpnpm db:generate, review the SQL inapps/database/drizzle-postgres/,pnpm db:check-migrations,pnpm db:migrate,pnpm gen-types, update the seeds, thenpnpm db:verify-clean. - Migrations are additive and must replay on an empty database. Never edit an applied migration or its snapshot.
- Seeds:
apps/database/src/seed.ts(minimal) andapps/web/src/cli/seed.ts(full fixtures). Both must stay idempotent. - The
db-migrationskill in.agents/skills/db-migration/SKILL.mddescribes the whole loop.
Authentication
apps/web/src/lib/auth/ holds the Better Auth server and client, session helpers, actors, workspace permissions and the impersonation handoff helpers. The admin app reuses all of it. See authentication.
Emails
Templates are in apps/email/emails/*.tsx. Add a render helper in apps/email/src/index.tsx and a render test, then send through apps/web/src/lib/email/send-email.ts. Preview templates with pnpm --filter email dev; the preview server defaults to port 3001, the same port as the admin app, so pass another port when both are running.
Background jobs
There are no queues. Mutations write to outbox_events, and apps/web/src/cli/dispatch-outbox.ts claims batches of 50 and publishes them to Centrifugo (pnpm realtime:dispatch, or realtime:dispatch:once for a single pass). apps/web/src/cli/cleanup-storage.ts removes expired unattached uploads. Realtime helpers are in lib/realtime/, storage in lib/storage/uploads.ts and lib/storage/provider.ts.
MCP
apps/web/src/mcp/ holds the implementation. The web app mounts it for the user audience in app/mcp, app/oauth and app/.well-known; the admin app mounts the same handler for the admin audience in its own app/mcp, app/oauth and app/.well-known. See agent connections.
Tests
- Unit tests sit next to the code as
*.test.ts(x)and run withpnpm test(Vitest). Tests for the shared auth code and the handoff routes live inapps/web, includingapps/web/src/app/api/impersonation/route.test.ts. - Integration tests are in
apps/web/src/integration/*.integration.test.tsand need the Docker stack:pnpm test:integration. - Playwright specs are in
apps/web/e2e/(anon,user,admin,invitations,lifecycle,password-updation,visual, with_helpersand_setups), configured inapps/web/playwright/playwright.config.mts. The config builds both apps and starts the web app on port 3100 withapps/web/.env.testand the admin app on port 3101; theadminproject and the admin setup run against the admin origin. - Repository gates:
scripts/check-provider-purity.mjs,scripts/check-postgres-migrations.mjs,scripts/verify-clean-migrations.shandscripts/check-ultimate-parity.mjs, all run bypnpm verify:localand in.github/workflows/ci.yml.
AI coding tool configuration
Edit only AGENTS.md (root and per app, including apps/admin/AGENTS.md), .agents/config.json and .agents/skills/, then run pnpm agents:sync. CLAUDE.md, GEMINI.md, .mcp.json, .cursor/, .vscode/mcp.json and the other tool files are generated, and CI runs pnpm agents:check to catch drift.
Agent connections
The User MCP and Admin MCP servers in the Next.js + Neon starter: endpoints on the web and admin origins, tools, OAuth behaviour and how to add a tool.
Deployment
Put the Next.js + Neon starter in production: a Neon database, migrations, two Vercel projects for the web and admin apps, and post-deploy checks.