Deployment
Provision Neon roles, apply migrations, and deploy the TanStack + Neon starter's web and admin apps as two Vercel projects.
The live demo runs two Vercel projects, one rooted at apps/web and one at apps/admin, against a single Neon database. This page covers what is specific to this kit; the shared Vercel deployment guide has the platform-generic steps.
What the repository ships for deployment
apps/web/vercel.json, apps/web/index.mjs, apps/admin/vercel.json and apps/admin/index.mjs are the only deployment files. There is no Dockerfile or production Compose file. The README's "Deployment" section describes a generic three-process layout (migrate, worker, web) for hosts that run long-lived Node processes; the commands in it are the package scripts listed at the end of this page.
1. Create the Neon database and roles
Create a Neon project and a database named nextbase (the local scripts assume that name). Then create the same roles the local apps/database/docker/postgres-init.sql creates, with your own passwords:
migration_owner: login, owns the database and thepublicschema. Used only for migrations and the admin scripts.auth_runtime,app_runtime,worker_runtime: login,NOSUPERUSER,NOBYPASSRLS,NOINHERIT. They must not inheritneon_superuser.function_owner:NOLOGINandBYPASSRLS; it only owns the reviewedSECURITY DEFINERhelpers. Grant it tomigration_ownerwithSET TRUE.
Grant CONNECT on the database and USAGE on public to the three runtime roles. Neon's SQL editor or psql as the project owner can run the same statements as the init file, with the passwords replaced.
Neon gives you two endpoints. Use the direct endpoint for DATABASE_DIRECT_URL and the pooled endpoint for DATABASE_URL, AUTH_DATABASE_URL and WORKER_DATABASE_URL. Both use port 5432 on Neon (locally the equivalents are 35432 and 36432). Append ?sslmode=verify-full to all four.
The demo uses a Singapore Neon project; pick the region closest to your Vercel region.
2. Apply migrations and check the roles
From your machine, with the production direct connection:
DATABASE_DIRECT_URL='postgresql://migration_owner:...@<direct-host>:5432/nextbase?sslmode=verify-full' pnpm db:migrate
DATABASE_DIRECT_URL='postgresql://migration_owner:...@<direct-host>:5432/nextbase?sslmode=verify-full' pnpm db:checkdb:migrate is forward-only and checksum-protected. db:check fails if a runtime role has BYPASSRLS or superuser or is a member of neon_superuser or function_owner, if a tenant table lost forced RLS, or if an app function is executable by PUBLIC. Run both before the first deploy and after every release that adds a migration. Migrations never run during web or worker startup. The MCP migration (0003_user_admin_mcp.sql) is part of the baseline set, so a fresh database gets it on the first run.
Don't run pnpm db:seed against production unless you want the demo workspace. To make yourself an admin, run pnpm admin:grant -- you@example.com with the same DATABASE_DIRECT_URL after you have signed up on the live site.
3. Production environment variables
Set every value from .env.example in each Vercel project's environment settings. The names are listed on environment variables. The ones that differ between the two projects or most often go wrong:
| Variable | Web project | Admin project |
|---|---|---|
APP_BASE_URL | The product origin, for example https://app.example.com. Better Auth base URL and User MCP issuer | The admin origin, for example https://admin.example.com. Admin MCP issuer |
TRUSTED_ORIGINS | The product origin, plus preview origins if you use them | The admin origin |
WEB_APP_URL | The product origin | The product origin (where impersonation handoffs land) |
ADMIN_APP_URL | The admin origin (where support sessions return) | The admin origin |
BETTER_AUTH_SECRET | At least 32 random characters. Changing it signs everyone out | The same value, or its own; cookies are host-only either way |
DATABASE_DIRECT_URL, DATABASE_URL, AUTH_DATABASE_URL, WORKER_DATABASE_URL | The four Neon connection strings from step 1 | The same four |
SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, EMAIL_FROM | Your provider. For Resend use smtp.resend.com, port 587, and your API key as the password. EMAIL_FROM must be a verified sender | The same; the admin app also enqueues auth emails |
CRON_SECRET | At least 32 characters. Vercel sends it to /internal/jobs/run | Not used; the admin app has no cron route |
NODE_ENV | production | production |
Configure OAuth provider callbacks under the web APP_BASE_URL. /api/v1/webhooks/stripe exists but answers 501 until you implement billing, so there is no webhook to register yet.
4. Deploy the web app on Vercel
Create a Vercel project from your repository with Root Directory set to apps/web. apps/web/vercel.json does the rest:
framework: express, withindex.mjsas the function entry. It imports the bundledbuild/runtime/server.jsand mounts it on an Express app.buildCommand: pnpm build && cp -R build/client/. public/.pnpm buildrunsreact-router build, bundlesserver.tsandworker.tswith esbuild, and then runsscripts/check-built-auth.mjs, which boots the bundle withVERCEL=1and asserts that/appredirects anonymous visitors to/login.- The function includes
build/**and hasmaxDuration: 300. - A cron calls
/internal/jobs/runevery 15 minutes withAuthorization: Bearer {CRON_SECRET}.
On Vercel there is no long-running worker. When process.env.VERCEL is set, the server dispatches a bounded outbox batch for each new email with waitUntil, and the cron retries whatever is still pending. This lets Neon suspend after its idle timeout. The /mcp/* and /oauth/* routes run inside the same function, so the User MCP endpoint is {APP_BASE_URL}/mcp/user.
Deploy a preview first with its own APP_BASE_URL and TRUSTED_ORIGINS, then promote to production.
5. Deploy the admin app on Vercel
Create a second Vercel project from the same repository with Root Directory set to apps/admin. apps/admin/vercel.json is the same shape as the web one (framework: express, index.mjs, build/**, maxDuration: 300) without the cron. Its build runs react-router build and bundles server.ts; the admin server imports the web app's server modules by relative path, so the whole repository must be available to the build, which is the default for a monorepo root directory on Vercel.
Give it the admin values from the table above. The demo runs at https://tanstack-neon-admin.usenextbase.com. After deploy, / redirects to /app-admin, which sends anonymous visitors to /login. The Admin MCP endpoint is {admin APP_BASE_URL}/mcp/admin.
Deploy order for a release that touches both apps: migrations first, then the web app, then the admin app. The two apps share the database and the auth code, so keep them on the same commit.
6. Smoke checks after deploy
GET {APP_BASE_URL}/healthzreturns{"status":"ok"}on both origins;GET /readyzreturns{"status":"ready"}once both database pools answer.- Open
/sign-upon the web origin, create an account, receive the verification mail from your real sender, and sign in. If nothing arrives, check the SMTP values and, on Vercel, the function logs for "Immediate email dispatch failed" and the cron logs for/internal/jobs/run. - Create a workspace. This exercises
withActorTransactionand RLS through the pooled connection. - Run
pnpm admin:grant -- you@example.comagainst production, sign in again on the web origin, then sign in on the admin origin and open/app-admin/users. Start an impersonation with a reason; you should land on the web origin with the banner, and Return to admin should bring you back. - Open
/mcp/connections/useron the web origin and/mcp/connections/adminon the admin origin; the server address shown must be the matching origin.GET /mcp/userandGET /mcp/adminwithout a token should return401with aWWW-Authenticatechallenge. Then follow connect an agent and runprojects_list.
Watch dead outbox jobs (status = 'dead' in app.outbox_jobs), readiness, error rate and Neon connection usage during the first rollout. Rollback is a redeploy of the previous build; migrations are forward-only, so write additive migrations and remove old columns in a later release.
Other Node hosts
If you run the apps as long-lived processes instead of Vercel functions, the README describes three commands from one build:
pnpm --filter @nextbase/database migrate # deployment job, before anything else
pnpm --filter @nextbase/web worker:start # NEXTBASE_WORKER_MODE=daemon node build/runtime/worker.js
pnpm --filter @nextbase/web start # node build/runtime/server.js on PORT
pnpm --filter @nextbase/admin start # the admin server, on its own PORT and originWithout VERCEL set, the servers listen on PORT and drain connections for up to 30 seconds on SIGTERM, and the daemon worker polls the outbox instead of relying on a cron. Deploy migrations first, then a worker compatible with the new job types, then the web and admin servers. You supply the container or process manager; the repository doesn't include one.