Skip to content

Deploying to Vercel

Terminal window
npx flare deploy

runs the Vercel CLI (vercel deploy --prod) from the app’s own node_modules, so there is nothing to install globally and every project pins its own version. Anything you add is forwarded straight through:

Terminal window
npx flare deploy --yes # don't ask
npx flare deploy --scope my-team # a team other than your default
npx vercel deploy # a preview instead of production

vercel on its own works exactly as well — flare deploy is a shortcut, not a wrapper, and it deliberately does nothing the CLI doesn’t. This is the opposite of the Cloudflare deploy, which sequences migrations, secrets and zone rules itself, because on Vercel every one of those belongs to the platform.

Terminal window
npx vercel link # connect this directory to a Vercel project
npx flare deploy

Vercel builds on its own machines, running the build script from package.json:

"build": "prisma generate && next build"

prisma generate has to be there. The generated client is written into node_modules, which is not committed and not cached between builds, so a build without it fails on the first import.

Set these on the Vercel project (Settings → Environment Variables), not in a committed file:

Variable
DATABASE_URL Postgres, pooled. Neon’s pooled string, not the direct one
BETTER_AUTH_SECRET 32+ random bytes. Generate a new one — never reuse your laptop’s
BETTER_AUTH_URL The production URL, e.g. https://shop.example.com
R2_ENDPOINT, R2_BUCKET, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY File uploads
UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN Rate limits shared across instances
RESEND_API_KEY, MAIL_FROM Email. Without them it prints to the log instead of sending

.env.example in your app lists the same set, which is what to copy from.

Nothing in the deploy touches your database. That is deliberate: Vercel can build the same commit several times over, in parallel, and a migration that runs from a build step runs an unpredictable number of times.

Point DATABASE_URL at production and apply them:

Terminal window
DATABASE_URL="postgres://…" npx flare migrate

which runs prisma migrate deploy — it applies what is already written in prisma/migrations/ and never invents anything, so it is safe to run repeatedly and safe to run against a database that matters. Writing a new migration is prisma migrate dev, on your own machine, against your own database.

Run migrations before the deploy when they add something the new code needs, and after when they drop something the old code still uses. That ordering is the whole of zero-downtime schema change, and no tool can decide it for you.

A new Postgres database is empty, and nothing in the build fills it. So the deploy goes green, you open the app, and sign-up answers 500 — because the users table does not exist yet. Vercel’s logs show the status and not the cause, which makes this a slow one to work out.

Once, from your own machine:

  1. Get the connection string under the name Flare reads. Vercel’s Postgres integration sets POSTGRES_PRISMA_URL (pooled) and POSTGRES_URL_NON_POOLING (direct). Flare and Prisma read DATABASE_URL, so a local .env holding only the Vercel names fails with PrismaConfigEnvError: Cannot resolve environment variable: DATABASE_URL. Pass it for the one command rather than editing .env, so there is nothing to remember to undo:

    Terminal window
    DATABASE_URL="<your POSTGRES_PRISMA_URL>" npx flare migrate
  2. Apply with migrate deploy, which is what flare migrate runs. Do not reach for prisma migrate dev when that fails: it wants a shadow database it can create and drop, which a managed Postgres will not permit, so it errors on permissions. It is also the wrong command here — its job is to write a migration, against your own database.

    If prisma/migrations/ is empty there is nothing to apply. Write the first one locally with npx flare gen migration init, commit it, and run the above. npx prisma db push is the shortcut: it makes the database match the schema with no migration history, which is fine for a prototype and a problem the first time two people need the same change.

  3. Seed, if the app needs rows to be usable.

    Terminal window
    DATABASE_URL="<your POSTGRES_PRISMA_URL>" npx flare seed

    Seeds are not idempotent unless you wrote them that way — running this twice inserts twice.

  4. Check .env still points at your own database, if you edited it rather than passing the variable inline.

After that the schema travels with the code: commit the migration, deploy, and run flare migrate against production in the order described above.

The storage helpers speak S3, pointed at a Cloudflare R2 bucket, on both stacks. R2 charges nothing for bandwidth out, and an app that serves files is mostly bandwidth out — what it costs has the arithmetic. Vercel Blob works if you would rather keep everything in one account; you would reimplement lib/storage.ts against its SDK.

Neon’s free tier suspends a database that has been idle for five minutes, and the first query after that waits for it to wake. It reads like a slow function and isn’t: it is the plan, and the fix is a paid tier with no scale-to-zero, not anything in your code.

Realtime. realtimeChannel().publish() delivers nothing here, because Cloudflare’s half of it is a Durable Object per channel and Vercel has no equivalent. It warns once per channel rather than failing quietly, and the write that triggered it still succeeds. The observability page links to Vercel’s own dashboard rather than charting your traffic in the app. Choosing a stack is the honest list.