You have been running npm run dev for weeks. The app works on your machine. Payments process, emails send, logins work. Then you push to Vercel and something breaks -- a missing environment variable, a database that worked locally but times out in production, or a cold-start error you have never seen before.
This is the most common "done but not launched" trap. You are not missing features. You are missing the 45 minutes it takes to set up production correctly.
Here is the exact sequence.
If you are using the Next.js SaaS Boilerplate, your code is already on GitHub. Go to vercel.com, create a new project, and import the repository. Vercel detects Next.js automatically and sets the build command to npm run build -- you do not need to change the framework preset.
Once the repo is connected:
main triggers a production deploymentThat second point is not a nice-to-have. It means every pull request gets a live URL before anything touches production. You get a staging environment for free.
Your local .env file never goes to Vercel -- that is intentional, because it contains secrets. You have to add each variable manually in your Vercel project under Settings -> Environment Variables.
At a minimum, add these:
JWT_SECRET=<a long random string -- never reuse the local one>
DATABASE_URL=<your Neon DB production pooled connection string>
NEXT_PUBLIC_BASE_URL=https://yourdomain.com
RESEND_API_KEY=<from resend.com>
CLOUDINARY_CLOUD_NAME=<from cloudinary.com>
CLOUDINARY_API_KEY=<from cloudinary.com>
CLOUDINARY_API_SECRET=<from cloudinary.com>
STRIPE_SECRET_KEY=<your Stripe live secret key>
STRIPE_PUBLISHABLE_KEY=<your Stripe live publishable key>
STRIPE_WEBHOOK_SECRET=<from Stripe webhook dashboard>
Two variables need extra attention.
DATABASE_URL for serverless. Neon gives you two connection strings: a regular one and a pooled one. For Vercel, which runs serverless functions that spin up and down constantly, you want the pooled connection string from the "Pooled connection" section of your Neon dashboard. It ends in ?sslmode=require. Using the regular string means every cold start opens a new database connection, and you will hit Neon's connection limit under any real load.
Neon also has a first-party Vercel integration that sets DATABASE_URL automatically when you link the projects -- worth using if you want to skip this manual step.
NEXT_PUBLIC_BASE_URL. Set this to your production URL including https://. Variables prefixed with NEXT_PUBLIC_ are embedded at build time, not at request time. If this is wrong, every OG image, email confirmation link, and canonical URL in your app points to the wrong domain.
Your Neon DB schema does not exist in production yet. Migrations you ran locally created tables in your local or development database branch -- not in production.
Before real users land, run this once against your production database:
DATABASE_URL="<your production connection string>" npm run db:migrate
Vercel does not run this automatically. It is a manual step you repeat whenever a schema change reaches production.
The db:push command (npm run db:push) is a shortcut that skips migration files and syncs the schema directly. It is fine in early development, but switch to db:migrate before you have real user data -- migration files give you a reversible history of schema changes.
In your Vercel project -> Settings -> Domains, add your domain. Vercel gives you two DNS records to add at your registrar: an A record and a CNAME. SSL is provisioned automatically within a few minutes.
After the domain is live, update NEXT_PUBLIC_BASE_URL in your Vercel environment variables to match the real domain, then redeploy by pushing a commit to main.
If you are using Stripe, your local webhook listener was hitting localhost. Production needs its own endpoint.
Go to Stripe Dashboard -> Developers -> Webhooks -> Add endpoint. Use:
https://yourdomain.com/api/webhooks/stripe
Select the events your app handles. At minimum: checkout.session.completed, customer.subscription.updated, customer.subscription.deleted, and invoice.payment_failed.
Copy the webhook secret Stripe generates and add it as STRIPE_WEBHOOK_SECRET in Vercel, then redeploy.
Without this step, Stripe will process payments but your app will never know about them. Plan upgrades, subscription cancellations, and failed payment recovery all break silently.
Run three end-to-end checks after your first production deploy:
If any of those fail, the problem is almost always a missing or incorrect environment variable -- not the code itself.
For a broader pre-launch checklist beyond the deploy, see The SaaS Launch Checklist.
A working local dev environment is not a launched product. The distance between the two is smaller than most founders expect -- usually two or three hours of environment setup, not another sprint of features.
If you are starting fresh, the Next.js SaaS Boilerplate gives you a codebase that deploys to Vercel on day one, with auth, payments, and email already configured. You spend your first weekend on your actual product idea, not on infrastructure.
Get it live. Real users will tell you what to build next.