Deployment
In one line: Push to
maindeploys to prod; push to any other branch deploys to a preview URL. That's the entire pipeline.
"Deployment" in 2026 means giving your hosting provider a GitHub repo and letting them auto-build on every push. You will not write a Dockerfile, you will not set up CI, you will not configure a load balancer. The only real decisions are: which branch is production, and which env vars exist in which environment.
The deploy pipeline
Stack A (Vercel)
- Push to a
mainbranch → Vercel builds and deploys to your production domain. - Push to any other branch (or open a PR) → Vercel deploys to a unique preview URL (
my-app-git-feature-x.vercel.app). - Merge the PR → production updates.
That's it. No build minutes to manage, no manual promote step. Free tier covers a real solo project.
Stack B (Modal)
modal deploy main.py→ live function endpoint, versioned.- Use
modal deploy main.py --env devfor a separate dev environment. - Cron, queues, and GPU jobs all defined in
@app.function()decorators in the same file.
Modal doesn't have "preview deploys" the same way Vercel does, but --env flags get you the same separation.
Environment variables, per environment
This is where most solo deployments go wrong. Different environments need different keys. In Vercel's project settings → Environment Variables, you'll see three columns:
| Variable | Development | Preview | Production |
|---|---|---|---|
ANTHROPIC_API_KEY | dev key (low cap) | dev key (low cap) | prod key (real cap) |
STRIPE_SECRET_KEY | sk_test_... | sk_test_... | sk_live_... |
STRIPE_WEBHOOK_SECRET | test webhook | test webhook | live webhook |
NEXT_PUBLIC_SUPABASE_URL | dev project | dev project | prod project |
RESEND_API_KEY | test domain | test domain | real domain |
Rule: anything that costs money or sends email must be a test key outside of Production. Preview URLs are shareable; if a stranger clicks one and it triggers a real Stripe charge or a real email, you've got a problem.
The production-data-in-preview question
A real choice: should preview deploys point at the production DB or a dev DB?
| Option | Pro | Con |
|---|---|---|
| Preview → prod DB | Test with real data; no fixtures needed | A bad migration in preview wrecks prod |
| Preview → dev DB | Safe to break | Dev DB diverges from prod over time |
| Preview → branch DB | Best of both; one DB branch per PR | Requires Neon / Supabase branching, more setup |
For solo v0: preview → dev DB. Once you're charging real customers, preview → branch DB via Supabase Branching or Neon Branching. Never preview → prod.
The custom domain (5-minute job)
- Buy a domain (Namecheap, Cloudflare, Porkbun). $12/year is fine.
- In Vercel project → Settings → Domains → Add. Paste the domain.
- Vercel tells you to add one or two DNS records at your registrar. Copy-paste.
- Wait 1–60 minutes for DNS propagation. TLS provisions automatically.
If you bought the domain through Cloudflare and Vercel: configure DNS only at one of them. The "both think they're authoritative" failure mode is the most common source of "my domain works for me but not for my friend."
The 60-second rollback
When production breaks, you want a fast rollback before you debug. Vercel UI → Deployments tab → find a previous successful deploy → "Promote to Production." Live in 30 seconds.
Modal: modal app rollback my-app to the previous version.
Pin a sticky note (or a Notes.app entry) with the exact words "When prod is on fire: Vercel → Deployments → Promote" so you don't waste minutes finding it under stress.
Cron and background jobs
A common solo-AI need: nightly batch jobs (re-embed, refresh cache, send a daily email).
- Stack A: Use Vercel Cron Jobs (configure in
vercel.json). Free tier covers a few jobs at hourly or daily frequency. Calls a route handler — same security model as any other route. - Stack B: Modal has cron built in:
@app.function(schedule=modal.Cron("0 8 * * *"))
def daily_digest():
...
If you outgrow either, Render and Fly.io both have cron. Don't reach for Airflow.
File storage
Three legit options for solo AI:
- Supabase Storage — already in your stack if you're on Supabase. Simple, integrated, fine for under 1GB.
- Cloudflare R2 — S3-compatible, free egress, $0.015/GB stored. The right answer for files larger than a few MB or anything user-uploaded at volume.
- Direct in Postgres
bytea— fine for tiny stuff (<1MB), don't scale past that.
For the typical solo AI tool (text-in, text-out), you won't need file storage at v0.
You merge a PR. The preview env has STRIPE_SECRET_KEY and RESEND_API_KEY set to production values by accident (you forgot to set test keys when adding the variable). A teammate or beta tester opens the preview URL and triggers a flow that charges a real card and sends an email to a real prod user.
The structural fix isn't "be more careful." It's: when adding any secret to Vercel, always add it as three separate values for Dev / Preview / Production, and double-check that Preview is the test version before clicking save. Better yet, name your test-tier secrets clearly: STRIPE_SECRET_KEY=sk_test_... and visibly different from sk_live_... in the dashboard.
The most common solo-deploy disaster is the "I'll deploy when I'm ready" deploy. Five interacting bugs hit at once: env vars missing, build script breaks on the Linux runner, domain DNS not propagated, webhook URL wrong. Deploy an empty project on day one. Push frequent small changes. By launch, deploys are boring.
Common mistakes
- One set of env vars used for all environments. Live Stripe key fires from a preview URL; live email goes from a feature branch. The fix is per-environment scoping in Vercel/Modal, and naming conventions that make
testvslivevisually distinct. - Pointing preview deploys at the prod DB. A migration test deletes a prod column. The fix is preview → dev DB at minimum, branch DB once you have customers.
- Locking the deploy behind a manual promote step. Solo speed depends on push-to-deploy. The fix is to let
mainauto-deploy to prod; preview branches catch issues before merge. - Never testing the rollback. First time you try it is at 11pm under pressure. The fix is to do one practice rollback the week you ship — promote yesterday's deploy, verify it works, promote today's back.
- Skipping a custom domain "until later."
something.vercel.appis impossible to share verbally and embarrassing on a launch tweet. The fix is to buy the $12 domain on day one.
Page checkpoint
Self-check:
- Is
mainset to auto-deploy to production? - Are test keys (Stripe, email, sometimes LLM) used in Development and Preview, with live keys only in Production?
- Have you done a practice rollback at least once?
What's next
→ Continue to Observability where we'll set up Langfuse and a cost dashboard so problems show up before users notice them.