Skip to content

Deploy a Next.js app

Deploy the SQLite starter to Vercel with a hosted libSQL database on Turso. Keep authentication and Todos in persistent storage, apply migrations before serving traffic, and verify the deployed app through its health endpoints and UI.

Before you start

Complete the quickstart with the Next.js and SQLite starter. Run commands from that app's root with Bun. Commit your app, lockfile, and drizzle/ migration history to a Git repository.

You will need a Vercel project and a Turso database. Follow the Turso quickstart to create a database and obtain its libsql:// URL and auth token. Create a Vercel project from your repository and choose the production hostname you will use for authentication. See Next.js on Vercel for repository import and project setup.

Use a new, empty database for this walkthrough. Migrations create the schema; they do not copy accounts or Todos from your local local.db. Keep local development and production databases separate.

This path covers the starter's web app. If you have added uploads, queues, cron routes, or other integrations, configure their production providers and runtime entrypoints too.

Prepare the app

Pin the Node runtime in package.json so the deployment uses Node 24:

package.json

Vercel reads the Node version from engines.node; use a supported major version rather than an open-ended minimum. See Vercel's Node.js versions.

The starter already uses @libsql/client at runtime. Configure Drizzle Kit's turso dialect so its migration command can connect to your hosted database:

drizzle.config.ts

Your schema stays SQLite. Local file: URLs keep the sqlite dialect; hosted URLs use turso. This selects the migration connection for each environment. Drizzle documents this setup in Drizzle with Turso. Keep SQLITE_DB_URL=file:local.db in your development .env.local.

bash
bun install
bun run format
bun beignet check
bun run build

Commit these changes and the lockfile. Resolve failing checks before deploying.

Configure production values

Add the following ignore rule before creating a local release environment file:

.gitignore

Create .env.deploy.local in the app root. Replace the example hostname, secret, database URL, and token with your production values:

dotenv
NODE_ENV=production
APP_URL=https://your-app.example
BETTER_AUTH_URL=https://your-app.example
BETTER_AUTH_TRUSTED_ORIGINS=https://your-app.example
BETTER_AUTH_SECRET=replace-with-a-random-secret-of-at-least-32-characters
DEVTOOLS_ENABLED=false
SQLITE_DB_URL=libsql://your-production-database.turso.io
SQLITE_DB_AUTH_TOKEN=replace-with-your-production-database-token
LOG_LEVEL=info
LOG_FORMAT=json

Generate a secret with openssl rand -hex 32, and keep it stable across deployments. Changing it can invalidate existing sessions. Use the exact HTTPS origin for APP_URL, BETTER_AUTH_URL, and the trusted origin; do not add a wildcard for arbitrary preview deployments.

Add the same values to the Vercel project's Production environment. Never prefix secrets with NEXT_PUBLIC_ or commit .env.deploy.local. Configure Preview environments with a separate database and credentials if you enable them, and give each preview its correct auth URL and trusted origin.

Migrate the production database

Run these commands from a trusted release machine with production credentials. The explicit environment file selects the production database rather than your development .env.local. Bun's --env-file option overrides its default environment-file loading. Check for conflicting exported variables in your shell before running the release commands.

First inspect the target and migration history without writing:

bash
bun --env-file=.env.deploy.local run beignet db status

On a new database, expect pending migrations and exit code 2. Review the checked-in SQL in drizzle/, confirm the database URL targets the intended environment, then apply it once:

bash
bun --env-file=.env.deploy.local run beignet db migrate
bun --env-file=.env.deploy.local run beignet db status
bun --bun --env-file=.env.deploy.local run beignet preflight --connect

The second status check should report no pending migrations. Connected preflight validates production configuration, checks migration status, and boots providers to check dependency health. A failure stops this release; fix it before sending traffic to the new app.

The preflight command uses --bun to load the app with Bun, including its TypeScript path aliases. The deployed Next.js app still uses the Node version selected above.

The starter has logging but uses a no-op error reporter. Preflight can report BEIGNET_PREFLIGHT_ERROR_REPORTING_NOT_CONFIGURED as a warning. Configure error reporting when you need production exception capture; a successful exit does not mean every operational capability is configured.

Serialize migration runs. Do not put migrations in server startup, readiness requests, or every Vercel preview build. For later releases, review backward compatibility with the currently serving app and follow the production migration checklist.

Deploy and verify

Use these Vercel project settings:

SettingValue
FrameworkNext.js
Root directoryThe directory containing this app's package.json
Install commandbun install --frozen-lockfile
Build commandbun run build
Node.js version24.x, matching engines.node

Deploy the commit you checked, with the production environment values already configured. A successful build proves compilation; verify runtime behavior after the deployment becomes available. Replace the hostname in these requests:

bash
curl --fail-with-body https://your-app.example/api/health
curl --fail-with-body https://your-app.example/api/ready

Both endpoints should return HTTP 200. /api/health reports process health; /api/ready includes a database check. Then verify the user-facing flow:

  1. Sign up at the production URL and sign out and back in.
  2. Create a Todo, reload the page, and confirm it remains visible.
  3. Sign in as a second account and confirm the first account's Todos are absent.
  4. Confirm devtools are disabled in production.

Keep the production checklist alongside this recipe for auth, rate limits, errors, observability, and any providers you add later.

Troubleshooting

SymptomCheck
Drizzle cannot open a libsql:// URLUse dialect: "turso", and provide both database URL and token to the migration command.
Preflight reports pending migrationsRun migration status against the same production environment, review the SQL, and apply the missing migrations once.
Sign-in rejects the origin or redirects to localhostMatch APP_URL, BETTER_AUTH_URL, and trusted origins to the public HTTPS origin, then redeploy.
The app reports a missing tableCheck that runtime and migration credentials target the same database and that migration history is committed.
Data disappears between requests or deploymentsConfirm the deployed URL is libsql://..., not file:local.db. Vercel's local filesystem is not your persistent database.
Readiness fails after a successful buildInspect runtime logs, database credentials, and provider health. Build success does not prove providers can connect.

Work with your agent

text
Prepare this Beignet Next.js SQLite app for Vercel and Turso using the
deploy-nextjs guide. Read the app instructions and installed package guidance.
Inspect existing providers, migrations, and deployment configuration. Configure
the Turso migration dialect, pin Node 24, and ignore the local release env file.
Run format, beignet check, and a production build. List the environment values
I need to supply without exposing secrets. Show the target database and pending
migrations before applying any production changes. Include health, readiness,
sign-in, persistence, and two-account checks in the deployment handoff.