Nextbase Docs
Nextbase Docs
HomeBlogWelcome to Nextbase v5!Choose your starterGetting startedLocal Supabase per agent
Next.js + NeonInstallationEnvironment variablesAuthenticationAgent connectionsProject structureDeployment
AuthenticationWorkspaces and the admin area
Building with agentsEmails with Resend
TroubleshootingNextbase v5
The 8 starter kitsNext.js + Neon

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.

The demo runs the website and the admin app on Vercel as two projects, and the database on Neon. This page covers what is specific to this kit. Platform steps that are the same for every Node-hosted starter are in the Vercel deployment guide. The repository's own notes on the admin deployment are in docs/admin-app.md.

Migrations run before the deploy, never at startup

Neither app applies migrations when it boots. You run pnpm db:migrate against the direct endpoint yourself, then deploy. The mcp_records migration must be in place before either app serves OAuth connections.

Create the Neon database

Create a Neon project. The README asks for two connection strings:

  • DATABASE_URL: the pooled endpoint, used by both apps at runtime, ideally with an application-only role.
  • DATABASE_DIRECT_URL: the direct (non-pooled) endpoint, used only for migrations, ideally with a role that owns the schema.

If you create a role named app_runtime for the apps, migration 0003 grants it access to mcp_records and adds a row-level policy for it. If that role doesn't exist, the migration skips that block and the table owner is used. Fresh installs don't need any extra roles.

Keep Neon's default of suspending compute after five idle minutes. The kit avoids always-on pollers for that reason.

Apply the migrations

From your machine or a CI job with both URLs set:

DATABASE_URL="<pooled url>" DATABASE_DIRECT_URL="<direct url>" pnpm db:migrate

apps/database/src/migrate.ts opens a single connection to DATABASE_DIRECT_URL and applies apps/database/drizzle-postgres/. Run it again for every release that adds a migration, before the new builds go live. Don't run the seeds against production; they create demo accounts with a known password.

The optional PostgreSQL schema validation workflow in .github/workflows/postgres-schema.yml replays the migrations on a clean container when you trigger it by hand.

Prepare email and storage

Email: set EMAIL_PROVIDER=resend, RESEND_API_KEY and a MAIL_FROM on a domain you have verified in Resend. See emails with Resend. Verification emails must work before anyone can finish signing up.

Storage: create two buckets on an S3-compatible service and set the S3_* variables on both apps. MinIO is only for local development. apps/web/next.config.ts derives a next/image remotePatterns entry from S3_ENDPOINT, so uploaded images render from your bucket without editing the config; the admin app's next.config.ts allows any HTTPS image host.

Create two Vercel projects

Import the same repository into Vercel twice. The repository has no vercel.json; docs/admin-app.md records the settings the demo uses:

ProjectRoot directoryFrameworkDemo hostname
Webapps/webNext.jsnextjs-neon.usenextbase.com
Adminapps/adminNext.jsnextjs-neon-admin.usenextbase.com

Each project deploys independently from the same commits. Vercel picks pnpm from the root packageManager field; the equivalent local commands are pnpm --filter web build and pnpm --filter admin build. Both apps need Node 24.

Add the production environment variables to each project. Use the exact names from the environment variables page.

Required on the web project:

VariableValue
DATABASE_URLNeon pooled connection string
BETTER_AUTH_URLThe web origin, for example https://app.example.com
NEXT_PUBLIC_APP_URLThe same web origin
NEXT_PUBLIC_WEB_APP_URLThe same web origin
NEXT_PUBLIC_ADMIN_APP_URLThe admin origin, for example https://admin.example.com
BETTER_AUTH_SECRETA new random string, 32 characters or more; the same value on both projects
REVALIDATE_SECRETA random string; the same value on both projects
ENFORCE_PRODUCTION_SECRETStrue
EMAIL_PROVIDER, RESEND_API_KEY, MAIL_FROMYour Resend setup
S3_ENDPOINT, S3_REGION, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_BUCKET_PUBLIC, S3_BUCKET_PRIVATE, S3_FORCE_PATH_STYLEYour bucket

Required on the admin project:

VariableValue
DATABASE_URLThe same Neon pooled connection string
BETTER_AUTH_URLThe admin origin
NEXT_PUBLIC_APP_URLThe same admin origin
NEXT_PUBLIC_SITE_URLThe same admin origin
NEXT_PUBLIC_ADMIN_APP_URLThe same admin origin
NEXT_PUBLIC_WEB_APP_URLThe web origin
BETTER_AUTH_SECRETThe same value as the web project
REVALIDATE_SECRETThe same value as the web project
ENFORCE_PRODUCTION_SECRETStrue
S3_*The same bucket as the web project, for marketing CMS uploads

Optional on either project:

  • BETTER_AUTH_TRUSTED_ORIGINS for preview domains that should be able to sign in.
  • VALKEY_URL if you run Valkey or Redis. Without it the kit uses Postgres for the auth cache, rate limits and locks, which works on Neon.
  • Realtime on the web project: NEXT_PUBLIC_CENTRIFUGO_URL as a wss:// URL, plus CENTRIFUGO_URL, CENTRIFUGO_API_URL, CENTRIFUGO_API_KEY and CENTRIFUGO_TOKEN_HMAC_SECRET. Leave NEXT_PUBLIC_CENTRIFUGO_URL unset to disable browser realtime; the app works without it.
  • Stripe: STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET and the price IDs on the web project, with a webhook pointing at /api/stripe/webhooks on the web origin. Give the admin project STRIPE_SECRET_KEY too if you want the Sync products action on the payment gateway page to pull the Stripe catalog.
  • OPENAI_API_KEY, PostHog, Google Analytics, Sentry and Vercel Analytics as needed.

DATABASE_DIRECT_URL is not needed by either app process. Keep it in the place you run migrations from.

Auth callbacks and origins

Better Auth builds its URLs from BETTER_AUTH_URL, and the trusted origins are BETTER_AUTH_URL, NEXT_PUBLIC_APP_URL and BETTER_AUTH_TRUSTED_ORIGINS. On each project all of these must agree with the domain users actually open on that app. A preview deployment with a different origin needs its own values or an entry in BETTER_AUTH_TRUSTED_ORIGINS.

Sessions are host-only cookies. Administrators sign in on the admin host at /login; a web session does not carry over. Impersonation crosses the boundary with a one-minute, single-use token that the admin app sends to <NEXT_PUBLIC_WEB_APP_URL>/auth/impersonation and the web app redeems at /api/impersonation, so NEXT_PUBLIC_WEB_APP_URL on admin and NEXT_PUBLIC_ADMIN_APP_URL on web must both be right or the handoff and the return trip fail.

Email links (verification, reset, invitations) are built from the web app's NEXT_PUBLIC_APP_URL. If you enable a social provider, register the callback for your web BETTER_AUTH_URL origin with that provider.

The MCP servers use each app's NEXT_PUBLIC_APP_URL as their origin, so there is no separate MCP_APP_URL in this kit. Your User MCP is <web origin>/mcp/user and your Admin MCP is <admin origin>/mcp/admin.

Realtime dispatcher in production

Realtime events are written to the outbox_events table and published to Centrifugo by apps/web/src/cli/dispatch-outbox.ts. Locally pnpm local:start keeps it running. The repository has no cron route or scheduled job for production, so if you want realtime in production, run pnpm --filter web realtime:dispatch:once on a schedule from a job runner you control, or run the dispatcher as a long-lived process somewhere that can reach both Neon and Centrifugo. Without a dispatcher, the app still works; the browser just doesn't receive live invalidation events.

Deploy and check

Run pnpm verify:local before you push. Then deploy both projects, open the site and check:

  1. /login and /sign-up load on the web origin. Create a test account, open the verification email from Resend, and sign in.
  2. Complete onboarding, create a workspace and add a project.
  3. /mcp/connections/user on the web origin shows a server address on your domain. /.well-known/oauth-protected-resource/mcp/user returns JSON. Opening /mcp/user in a browser returns 401 with a WWW-Authenticate challenge, which is correct.
  4. Follow connect an agent to approve a connection and call projects_list.
  5. Promote your own account to application administrator as described in authentication. Sign in at /login on the admin origin, open /app-admin, and check /mcp/connections/admin there. The web app's sidebar should now show Open admin panel.
  6. From the admin users page, impersonate the test account. You should land on the web app's /dashboard with the impersonation banner, and stopping should return you to the admin users page. Edit a blog post in the admin CMS and confirm the public page on the web origin refreshes; if it doesn't, REVALIDATE_SECRET or NEXT_PUBLIC_WEB_APP_URL on the admin project is wrong.
  7. If you enabled Stripe, send a test webhook and confirm the billing page updates.

If sign-in loops or emails link to the wrong host, compare BETTER_AUTH_URL and NEXT_PUBLIC_APP_URL of the app you are on with the address in the browser first. More cases are in troubleshooting.

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.

TanStack + Neon

What the TanStack + Neon starter ships, what you need to run it, and where to go next.

On this page

Create the Neon databaseApply the migrationsPrepare email and storageCreate two Vercel projectsAuth callbacks and originsRealtime dispatcher in productionDeploy and check