The word deployment suggests a single action — push a button, app goes live. It is not one action. It is a sequence of roughly eight decisions, and almost all of them are cheap if you make them before launch and expensive if you make them after. Where does the database live? What happens when a deploy breaks production at 23:40? Why does Gmail put your password-reset emails in spam? None of these questions are hard, but each one will cost you a bad evening if you meet it for the first time with users watching. This article walks the whole sequence, start to finish, using the launch of this very site as the worked example where it helps.
One assumption before we start: the code itself is ready. Inputs are validated, errors are handled, secrets are out of the repository. If you are not sure, the production-grade code checklist covers those eight disciplines in detail — it is the pillar that comes before this one. This article picks up where it stops: the code is good, and now it has to be live, monitored, and recoverable.
More launches are wounded by environment variables than by any other single cause, and the wound always has the same shape: development values quietly riding into production. The Stripe key is still the test key, so checkout works perfectly and charges nobody. NEXTAUTH_URL still points at localhost, so every OAuth login bounces off a redirect mismatch. The database URL still points at your machine, so the build succeeds — building does not always need a database — and the app falls over at the first request. None of these produce an error that says what is actually wrong. They produce confusing symptoms two layers downstream.
The audit is boring and takes twenty minutes. Open your .env.local and go line by line with three questions: does production need this variable at all, is the production value different from the development value, and where will production read it from. The answer to the third question is always the hosting platform's dashboard — never a committed file. While you are there, generate fresh values for anything secret. Your development NEXTAUTH_SECRET has been sitting on your laptop for months; production deserves its own.
Then make the machine enforce it, because you will not remember this audit in six months. A small Zod schema that runs at boot turns a silent misconfiguration into a loud, immediate crash with a message that names the exact variable:
import { z } from "zod";
const isProd = process.env.NODE_ENV === "production";
const schema = z.object({
DATABASE_URL: z.string().url(),
NEXTAUTH_SECRET: z.string().min(32),
NEXTAUTH_URL: z.string().url(),
STRIPE_SECRET_KEY: isProd
? z.string().startsWith("sk_live_", "Test Stripe key in production")
: z.string().startsWith("sk_"),
RESEND_API_KEY: z.string().startsWith("re_"),
});
export const env = schema.parse(process.env);Import env instead of process.env everywhere, and the dev-key-in-prod mistake becomes impossible to ship. A deploy that crashes on boot with "Test Stripe key in production" is annoying. A launch week where checkout silently charged nobody is a different category of problem.
warning
Anything prefixed NEXT_PUBLIC_ is inlined into the client JavaScript bundle at build time. Two consequences: a secret put there is published to every visitor and must be rotated immediately, and changing the value requires a rebuild — editing it in the dashboard does nothing until the next deploy.
next dev and next build are not the same program with a flag flipped. Development mode compiles pages lazily, tolerates problems it will warn about later, and never tries to prerender anything. The production build type-checks the whole project, statically generates every page it can, and fails hard on things development shrugged at. If the first time your code meets next build is on the hosting platform, you will debug build failures through a web log viewer, which is nobody's favourite way to spend an evening.
npm run build
npm run start
# now open http://localhost:3000 and click through the appRead the build output, not just the exit code. Next.js prints a route table marking each route static or dynamic, and surprises in that table are findings. A page you expected to be static showing up as dynamic usually means something in it reads cookies or headers. And the classic first-build failure — a connection error during prerendering — means a page fetches data at build time from a database your build machine cannot reach. That error is worth dwelling on, because it points at the first real architectural decision of your launch.
Your localhost Postgres does not follow the app to production. You need a managed database, and the field is crowded: Neon, Supabase, Railway, the cloud giants. I compared the realistic options for a solo developer in the managed Postgres comparison — the short version is that you want serverless-friendly managed Postgres with a built-in connection pooler and a free tier honest enough to launch on. Which provider matters less than understanding two production realities the marketing pages skip.
The first is connection limits. Serverless functions do not share one long-lived database connection the way a traditional server does — under traffic, many function instances each open their own, and a small Postgres instance allows a few dozen connections total. This is why every serverless-era provider ships a pooler in front of the database. Use the pooled connection string they give you, not the direct one, or your first traffic spike becomes a wall of "too many connections" errors that looks exactly like an outage.
The second is free-tier auto-pause. Free databases that sit idle get suspended — Supabase pauses projects after about a week without activity. This site runs on that exact free tier, and the launch design treats the pause as a fact of life rather than a surprise. Defense one is architectural: no public page touches Postgres at build or render time, so even a sleeping database cannot take the site down — content comes from the CMS, and the database only matters for admin surfaces. Defense two is a daily keepalive cron, which on Vercel is a few lines of configuration:
{
"crons": [
{ "path": "/api/keepalive", "schedule": "0 6 * * *" }
]
}The route behind it runs a SELECT 1 against the database and returns 200. Two details matter: verify the CRON_SECRET header inside the route so strangers cannot hit it, and let it fail loudly into your error tracker — a failing keepalive is an early warning that something deeper is wrong with the database connection. You will not always need this trick, but the principle behind it generalises: know your database tier's failure mode before launch, and decide on purpose how the app behaves when the database is unreachable.
Vercel is the default platform for Next.js for an unexciting reason: the same company builds both, so every framework feature works there on day one with zero configuration. Image optimization, ISR, middleware, preview deployments, cron jobs — connected and working before you have read a single piece of documentation. For a solo developer, the free Hobby tier carries a real project surprisingly far, and the operational work it removes is work you would otherwise do badly at 1 a.m.
Be honest about what the default costs you, though. At scale the bill grows along axes you do not control — bandwidth, image transformations, function duration — and some of the framework's best features are at their best specifically on Vercel, which is a soft form of lock-in worth naming. The alternative is self-hosting on a VPS with Docker, where five to ten euros a month buys you a fixed bill and long-running processes, and you inherit the ops work in exchange. I put real numbers and a decision rule on this trade-off in Vercel vs self-hosting. The honest summary: start on Vercel, and move when the bill outgrows a VPS or when you need something serverless cannot give you, like a persistent WebSocket server. Migrating a Next.js app off Vercel later is a weekend, not a rewrite — which means choosing it now is not a trap.
The baseline workflow in 2026 is git-connected auto-deploy: connect the repository to the platform once, and every push to main triggers a build and, if the build passes, a deploy. This is not just convenient — it is a discipline with teeth. It forces main to stay deployable, it makes the deploy log identical to the git log, and it eliminates the entire category of "which files did I forget to upload" bugs that defined the FTP era. If your deploy process has a manual step, that step will eventually be forgotten, and it will be forgotten on the worst possible day.
The click-by-click setup — importing the repository, framework detection, the dashboard tour — is covered in the definitive Vercel deployment guide, so I will not repeat it. What I will repeat are the two places people get hurt. First, environment variables must exist on the platform before the first deploy — a missing one either fails the build or, worse, lets it succeed with undefined baked in. Second, if you use Prisma, the platform's build needs prisma generate to run (a postinstall script is the standard home for it), and you need a deliberate answer for when migrations run — prisma migrate deploy as a build step is the simple honest default for a solo project.
And then there is the feature nobody advertises loudly enough: preview deployments. Every branch and pull request gets its own complete, live deployment at its own URL — not a staging server you maintain, a disposable copy of the entire app built from that exact commit. Push a risky change to a branch and you can test it on your actual phone, send the URL to a friend, and watch it run in the real production environment without touching production. For a solo developer this is the closest thing to a free QA department you will ever get. I treat the preview URL as a mandatory stop: nothing merges to main that I have not clicked through on a preview first.
tip
Give preview deployments their own database — a Neon branch, or a separate development project — and set that URL in the preview environment scope. Never point previews at production Postgres. A seed script run against the wrong environment is the kind of mistake you only make once, because once is enough.
At this point the app is live on a platform subdomain, and it needs a real name. The mechanics are short: buy the domain, add it in the platform dashboard, and create the DNS records it tells you to — an A or ALIAS record for the apex, a CNAME for www. SSL, which a decade ago was a paid annual chore, is now provisioned automatically through Let's Encrypt; your only job is to not interfere and to wait out DNS propagation, which is usually minutes and occasionally hours. The one decision to make deliberately: pick apex or www as canonical and redirect the other, permanently, so search engines see one site instead of two.
Then comes the part every launch checklist skips: email authentication. The moment your app sends email from your domain — password resets, contact-form notifications, a newsletter — you need three TXT records: SPF, DKIM, and DMARC. Without them, Gmail and Outlook quietly route your mail to spam, and you will discover this weeks later through the most demoralising support message there is: "I never got the reset email." The records take twenty minutes to set up with a sender like Resend, and the full walkthrough — what each record actually asserts, how to verify them, what a sane starter DMARC policy looks like — is in the domain, SSL and email DNS guide. Do it the same afternoon you configure the domain, while you are already in the DNS panel. Deliverability problems are invisible until they have already cost you users.
The difference between a hobby deploy and a production app is not whether it breaks. Everything breaks. The difference is whether you find out from a dashboard within minutes or from a polite stranger's email three days later. Observability sounds like an enterprise word, but for a solo developer it is three small tools, each with a free tier, all installable in under an hour total — and the hour has to happen on day one, because the most error-rich period in your app's life is its first week in front of real browsers, real extensions, and real phones you have never tested on.
The three layers: error tracking, uptime, analytics. Error tracking (Sentry is the usual answer) catches every unhandled exception with a stack trace, the URL, and the browser — turning "something is broken somewhere" into "this line throws on Android Chrome when the form is submitted empty." An uptime monitor pings your site every few minutes from outside and emails you when it cannot get through; it exists to catch the failures your error tracker cannot see, because the app is too down to report anything. And privacy-first analytics — Plausible, Umami, or the platform's own — is not vanity at this stage: when an error fires, the first triage question is how many people walked through the broken door, and analytics is how you answer it.
The full setup — which alerts to turn on, which to silence, and how to tune thresholds so that one person can actually live with the noise — is in observability for a solo SaaS. The goal is not a wall of dashboards like a network operations centre. The goal is exactly enough signal that you trust silence to mean things are fine — which, for one person, is the entire point of monitoring.
Here is a question to answer now, while you are calm: a deploy goes out, and ten minutes later the error tracker lights up — what exactly do you do? If your answer involves git revert, waiting for a rebuild, and hoping, you have a five-to-ten-minute outage built into every incident. The good news is that git-connected platforms make a much better answer one click away: every previous deployment still exists, fully built, and you can promote the last good one back to production instantly. No rebuild, no waiting. Roughly thirty seconds from "this is bad" to "production is the old version again," after which you can debug at leisure instead of under fire.
That is this site's entire rollback strategy, and I mean that as a recommendation, not a confession. No feature flags, no blue-green choreography — the previous deployment is the rollback, and for a solo project that is correctly sized rather than lazy. The asterisk is the database: instant rollback only works if yesterday's code can run against today's schema. The rule that keeps this true is to never ship a destructive migration and the code that depends on it as one inseparable unit — expand first (add the nullable column), deploy the code, contract later (drop the old column) once nothing references it. The full playbook, including the migration patterns and what to do when a bad migration has already run, is in rollback without feature flags. Read it before launch. It is the article you cannot absorb during an incident.
After the final deploy and before you announce anything, walk through your app like a paranoid stranger. Not automated tests — those already passed, and they test what you thought to test. This is manual, in production, with real data, on the real domain, and it exists to catch the gap between "the code works" and "the system works": the form whose notification email goes to an address nobody reads, the redirect that works everywhere except on the apex domain, the image that loads on Wi-Fi and times out on mobile data. Write the checklist down in the repository, because you will run it again after every significant deploy. Mine looks like this:
- Open every public route on both a phone and a desktop — home, blog index, a few posts, contact, the legal pages, and one deliberately wrong URL to see the 404 page
- Submit every form end-to-end and confirm the result actually arrives where it should — the email in the right inbox, the record in the right table
- Run the full auth loop with a real inbox: sign up, log out, log in, reset the password from the email link
- If you take payment: switch to live mode, buy the cheapest real product with your own real card — then refund yourself in the dashboard
- Open the browser console on the key pages and confirm it is clean; throw one deliberate test error and confirm it lands in the error tracker
- Verify http redirects to https, and that www and the apex domain resolve to one canonical address
The payment item deserves its own sentence, because people resist it. Test mode proves your code; only live mode proves your account — business verification status, the webhook endpoint configured against the production URL, payout details, tax settings. A real charge with your own card exercises the entire pipeline from button to bank, and the refund costs you nothing or near nothing. Every developer who skips this step and later discovers their webhook was still pointing at a preview URL learns the same lesson at a much worse time.
The week after launch has a specific job, and the job is correctness, not growth. Check the error tracker once or twice a day, and read it for new error types rather than raw counts — twenty occurrences of one known issue is a task, one occurrence of something you have never seen is a discovery. Watch real-user Core Web Vitals, because lab numbers on your machine always flattered you slightly; the field data from cheap Android phones on hotel Wi-Fi is the truth. Submit the sitemap to Google Search Console and check back midweek for crawl errors. And spot-check the boring plumbing — form deliveries, webhook logs, the cron's execution history — because silent failure is the failure mode of everything you set up this week.
Just as important is what to ignore. Ignore traffic numbers; a launch-week chart is noise with a hopeful slope. Ignore a single uptime blip that recovered on its own. Ignore the urge to add caching layers and scaling infrastructure for load you do not have — that is procrastination wearing a hard hat. Keep a plain launch-log file in the repository and write down each oddity and each fix; in six months, when you launch the next thing, that file will be worth more than this article. By day seven, if the error tracker is quiet and the forms deliver, you are allowed to believe it: the app is not just deployed, it is in production.
This article is deliberately the map and not the territory. Each of the five decisions it walks past has its own deep dive, and they are written to be read in the order you will meet them. Start with the platform decision in Vercel vs self-hosting, then settle the database question with the managed Postgres comparison. The week you connect the domain, work through the domain, SSL and email DNS setup in one sitting.
Then come the two that distinguish an app that is merely live from one that is operated: observability for a solo SaaS for the day-one monitoring stack, and rollback without feature flags for the recovery story you want settled before you ever need it. Together with this playbook they cover the whole distance from "works on my machine" to "live, monitored, and recoverable."
And the single most useful thing you can do after reading: do not save deployment for the end of your project. Take whatever you are building right now — half-finished is fine — run the pre-flight from the top of this article, and get it onto a preview URL this week. The first deploy is the only scary one. Every deploy after that is a push to a branch, a click through a preview, and a merge — and boring deploys, in the end, are the entire goal.
