--- title: "Deploy a Next.js app" description: "Deploy the SQLite starter to Vercel and Turso with production configuration, reviewed migrations, readiness checks, and persistent data." url: "https://www.beignetjs.com/guides/deploy-nextjs" markdown_url: "https://www.beignetjs.com/markdown/guides/deploy-nextjs.md" framework_version: "0.0.65" documentation_scope: "current-checkout" source_revision: "75f3e5c03f08daca9bba10940ebc777570febfd0" source_modified: true --- # 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](https://www.beignetjs.com/markdown/getting-started.md) 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](https://docs.turso.tech/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](https://vercel.com/docs/frameworks/full-stack/nextjs) 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](https://www.beignetjs.com/markdown/runtime-recipes.md) too. ## Prepare the app Pin the Node runtime in `package.json` so the deployment uses Node 24: ```diff --- a/package.json +++ b/package.json @@ -5,5 +5,5 @@ "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](https://vercel.com/docs/functions/runtimes/node-js/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: ```diff --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,9 +1,11 @@ +const 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](https://orm.drizzle.team/docs/tutorials/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: ```diff --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ .env .env.local +.env.deploy.local local.db local.db-* ``` 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](https://vercel.com/docs/deployments/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](https://bun.sh/docs/runtime/environment-variables) 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`](https://bun.sh/docs/runtime#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](https://www.beignetjs.com/markdown/error-reporting.md) 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](https://www.beignetjs.com/markdown/deployment.md#safe-database-migrations). ## 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: ```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](https://www.beignetjs.com/markdown/deployment.md) 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 ```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. ```