Deployment
Provision Supabase, apply the migrations and publish the TanStack + Supabase starter's web and admin apps to Vercel or any Node 24 host.
The kit deploys as two Node 24 Express servers: the product (apps/web) and the administration app (apps/admin). Both share one Supabase project. The live demo runs each on its own Vercel project, and the repository ships the packaging for that, but any platform that can run a Node 24 process behind a TLS proxy works. Follow the Vercel deployment guide for the platform-generic steps; this page lists what is specific to this kit. The repository's deployment.md and docs/admin-app.md have the same material in their own words.
1. Provision Supabase
Create a project in the Supabase dashboard and note the project ref, API URL, anonymous key and service-role key.
Link the CLI and push the migrations from apps/database:
cd apps/database
pnpm exec supabase link --project-ref <project-ref>
pnpm exec supabase db pushThis includes 20261014114357_user_admin_mcp.sql, which creates mcp_records and the two service-only functions the MCP servers need. Apply it before the apps go live, or agent connections will fail. The seed file is not applied by db push; never load the local test accounts into production.
Then in the dashboard, under Auth and URL Configuration:
- Set Site URL to the web app's
APP_URL(the product origin). - Add Redirect URLs for
/auth/callbackand/auth/confirmon the web origin and again on the admin origin. These are the only auth routes registered inapps/web/src/routes.tsandapps/admin/src/routes.ts. - Configure SMTP for confirmation, magic-link and recovery emails. See emails with Resend. The apps'
RESEND_API_KEYdoes not cover these. - Enable social providers only if you add provider buttons to the apps; the current login and sign-up pages offer password and magic link only.
If you want the admin JWT claim refreshed by the access-token hook, enable custom_access_token_hook in the project's Auth hooks. The function already exists after db push. It is optional: make_user_app_admin writes the claim directly.
2. Production environment variables
Set these in each deployment's secret manager. Names must match apps/web/.env.example and apps/admin/.env.example; see environment variables for the full list.
| Variable | Web app | Admin app |
|---|---|---|
NODE_ENV | production | production |
APP_URL | The product origin, for example https://app.example.com. No path, query, hash or credentials. | The admin origin, for example https://admin.example.com. |
WEB_APP_URL | Not used | The product origin (the web app's APP_URL). Required for /health/ready; used for support sign-in links. |
SUPABASE_URL | The project's API URL. | Same value. |
SUPABASE_ANON_KEY | The project's anonymous key. | Same value. |
SUPABASE_SERVICE_ROLE_KEY | The project's service-role key. Server-only. | Same value. |
HOST, PORT | Where Express binds on a Node host, for example 0.0.0.0 and 3000. Not used on Vercel. | Same, for example 0.0.0.0 and 3002. |
APP_URL matters more than it looks: it is the only origin accepted for form actions behind the proxy (isCanonicalOriginRequest), the origin for auth email links, and the issuer and resource for that app's MCP server (${APP_URL}/oauth plus ${APP_URL}/mcp/user on web or ${APP_URL}/mcp/admin on admin). A preview deployment with a different origin needs its own APP_URL and its own Supabase redirect URL entries. The demo follows the repository's <subdomain>-admin.usenextbase.com pattern for the admin origin.
Add optional integration variables as you turn features on: STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET (set them on admin too if you use its billing pages); RESEND_API_KEY and EMAIL_FROM (plus EMAIL_REPLY_TO); API_KEY_PEPPER with at least 32 random characters and API_CORS_ORIGIN; OPENAI_API_KEY with OPENAI_CHAT_MODEL and OPENAI_IMAGE_MODEL; ANALYTICS_ENABLED=true with POSTHOG_KEY. Server secrets must never get a public-variable prefix.
3. Build
From the repository root:
pnpm install
pnpm buildpnpm build builds every app through Turborepo. Each app build runs react-router build, bundles the Express entry with tsup to dist/index.js, boots that server and posts to a sign-in route from a foreign origin as a CSRF smoke test (scripts/check-built-auth.mjs, expecting 400), then packages a Vercel Build Output in .vercel/output (scripts/package-vercel.mjs). The packaging step traces the runtime files with @vercel/nft, refuses to include any .env* file, writes a nodejs24.x function with the app's index.mjs as the handler, and runs the smoke test again against the packaged bundle. Use pnpm web#build or pnpm admin#build to build one app.
Outputs per app (apps/web and apps/admin):
build/client: static assetsbuild/server: React Router server bundledist/index.js: Express entry point.vercel/output: Vercel Build Output API v3 bundle
4. Deploy
Both apps ship a vercel.json with "framework": null and "buildCommand": "pnpm build", and an index.mjs that wraps the Express app for the Vercel function. When VERCEL is set, server/index.ts exports the app instead of calling listen, and the server trusts one proxy hop.
Create two Vercel projects from the same repository. Set the root directory of one to apps/web and of the other to apps/admin so each picks up its own vercel.json, and keep access to the workspace source enabled so pnpm build can resolve the shared packages. Map the product domain to the web project and the admin domain to the admin project. Add the environment variables from step 2 to each project (HOST and PORT are not needed), deploy, and confirm the logs show the packaging step completing. The function runtime is nodejs24.x, which matches .nvmrc.
Any platform that runs Node 24 containers and exposes TCP ports works (Fly.io, Railway, Render, AWS ECS and similar). Run one service per app:
- Use Node 24.
- Run
pnpm installandpnpm buildin the build phase (orpnpm web#build/pnpm admin#build). - Start the web app with
pnpm --filter web startand the admin app withpnpm --filter admin start; each runsNODE_ENV=production node dist/index.jsin its own directory. - Map each service's public HTTPS origin to its
APP_URL, set the admin service'sWEB_APP_URLto the web origin, and terminate TLS at the proxy. - Point health checks at
/health/liveand/health/readyon both services.
5. Make yourself an administrator
Sign up on the deployed web app and confirm the email. Then, from the Supabase SQL editor:
select public.make_user_app_admin('<your user uuid>');Sign out and in again, then open the admin origin's /login and /admin. Details are on the authentication page.
6. Stripe webhook (optional)
Add a webhook endpoint in Stripe pointing to ${APP_URL of web}/api/stripe/webhook and copy its signing secret to STRIPE_WEBHOOK_SECRET. Billing pages show a disabled state until STRIPE_SECRET_KEY is set.
7. Smoke checks
GET /health/liveon both origins returns200with{ "status": "ok" }.GET /health/readyon both origins returns200with{ "status": "ready" }. A503means one ofAPP_URL,SUPABASE_URL,SUPABASE_ANON_KEYorSUPABASE_SERVICE_ROLE_KEYis missing, orWEB_APP_URLon the admin app.- Open
/loginand/sign-upon the web app, create a test account, confirm its email, accept the terms and create a workspace. Add a project. - Test a password reset. Check that links return to your origin, not to localhost.
- Open
/mcp/connections/useron the web app and confirm the server address shows your webAPP_URL.GET ${APP_URL}/mcp/userwithout a token should return a401challenge;GET ${APP_URL}/.well-known/oauth-protected-resource/mcp/usershould return JSON. Connect an agent following connect an agent and callprojects_list. - As an administrator, sign in on the admin origin, open
/adminand/mcp/connections/admin, and generate a support sign-in link from/admin/usersto confirm it lands on the web app's/auth/confirm. - Run
pnpm checkon the commit you deployed.
See troubleshooting if login returns to the wrong page or the MCP endpoint rejects an approved agent.