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:
4 unmodified lines5677894 unmodified lines "type": "module", "engines": { "node": ">=22.12.0" "node": "24.x" }, "scripts": {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:
123454676891011const url = process.env.SQLITE_DB_URL ?? "file:local.db";
export default { schema: "./infra/db/schema/index.ts", out: "./drizzle", dialect: "sqlite", dialect: url.startsWith("file:") ? "sqlite" : "turso", dbCredentials: { url: process.env.SQLITE_DB_URL ?? "file:local.db", url, authToken: process.env.SQLITE_DB_AUTH_TOKEN, },};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.
bun install
bun run format
bun beignet check
bun run buildCommit 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:
4 unmodified lines567894 unmodified lines.env.env.local.env.deploy.locallocal.dblocal.db-*Create .env.deploy.local in the app root. Replace the example hostname, secret,
database URL, and token with your production values:
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=jsonGenerate 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:
bun --env-file=.env.deploy.local run beignet db statusOn 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:
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 --connectThe 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:
| Setting | Value |
|---|---|
| Framework | Next.js |
| Root directory | The directory containing this app's package.json |
| Install command | bun install --frozen-lockfile |
| Build command | bun run build |
| Node.js version | 24.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:
curl --fail-with-body https://your-app.example/api/health
curl --fail-with-body https://your-app.example/api/readyBoth endpoints should return HTTP 200. /api/health reports process health;
/api/ready includes a database check. Then verify the user-facing flow:
- Sign up at the production URL and sign out and back in.
- Create a Todo, reload the page, and confirm it remains visible.
- Sign in as a second account and confirm the first account's Todos are absent.
- 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
| Symptom | Check |
|---|---|
Drizzle cannot open a libsql:// URL | Use dialect: "turso", and provide both database URL and token to the migration command. |
| Preflight reports pending migrations | Run migration status against the same production environment, review the SQL, and apply the missing migrations once. |
| Sign-in rejects the origin or redirects to localhost | Match APP_URL, BETTER_AUTH_URL, and trusted origins to the public HTTPS origin, then redeploy. |
| The app reports a missing table | Check that runtime and migration credentials target the same database and that migration history is committed. |
| Data disappears between requests or deployments | Confirm the deployed URL is libsql://..., not file:local.db. Vercel's local filesystem is not your persistent database. |
| Readiness fails after a successful build | Inspect runtime logs, database credentials, and provider health. Build success does not prove providers can connect. |
Work with your agent
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.