Learn Build a Real SaaS with Claude Code Deploying to Vercel

Deploying to Vercel

Intermediate 🕐 21 min Lesson 19 of 25
What you'll learn
  • Create a free Vercel Hobby account and import a Git repo as a new project
  • List every environment variable this app needs in production and identify which third-party dashboard each one comes from
  • Explain why production Supabase/Stripe/Resend credentials must be separate from local development credentials
  • Explain why this project deliberately excludes db:migrate from the Vercel build command
  • Configure a live Stripe webhook endpoint and connect its signing secret to the deployed app
  • Identify why vercel.json's cron declaration requires no separate dashboard setup, only a matching CRON_SECRET

What You Need Before Starting

You need a free Vercel account -- sign up at vercel.com/signup with email or a Git provider, no credit card required. The free Hobby plan is enough for this project: it includes unlimited projects, automatic HTTPS, a global CDN, and continuous deployment from a connected Git repo. You will also need this project pushed to a Git remote (GitHub, GitLab, or Bitbucket), since Vercel's import flow reads from a repo rather than a local folder.

One honesty note before the steps: the deploy process below is documented directly from this repo's own README.md and Vercel's official docs, not executed against a live Vercel account in the environment that built this reference app -- there were no Vercel credentials available there. Everything up to this point (the app itself, the Drizzle schema, the dashboard UI, the API routes, the Vitest and Playwright suites) was built and verified to actually run. This lesson is framed as here is exactly what you will run, written from the same documented process the repo ships with -- not a claim that this specific deploy was watched succeeding live.

Step 1: Import the Repo

From the Vercel dashboard, choose Add New -> Project -> Import Git Repository, authorize Vercel against your Git provider if you have not already, and select this repo. Vercel auto-detects Next.js and pre-fills the build settings -- for this project the defaults (build command next build, framework preset Next.js) need no changes. Every subsequent push to a non-production branch becomes a Preview Deployment; every push to the production branch (usually main) becomes a Production Deployment. That distinction matters for the next lesson on environment scoping.

Step 2: Set Environment Variables

Before the first deploy succeeds, set every variable from .env.example in the Vercel project's Settings -> Environment Variables panel (or via vercel env add <NAME> production from the CLI). This is also the point where it matters that production credentials are different secrets from your local .env file -- a production Supabase project, a production Stripe account (or Stripe in live mode), and a production Resend sender are not the same accounts you used for local development, and none of your local test keys should be reused here:

Variable Where it comes from
DATABASE_URL
Supabase project settings -- use the pooled "Transaction" connection string, not the direct connection, since serverless functions need pgbouncer compatibility
NEXT_PUBLIC_SUPABASE_URL / NEXT_PUBLIC_SUPABASE_ANON_KEY
Supabase project Settings -> API, free tier project
SUPABASE_SERVICE_ROLE_KEY
Same Supabase API settings page -- keep this one server-only, never expose it with a NEXT_PUBLIC_ prefix
STRIPE_SECRET_KEY / NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY / STRIPE_WEBHOOK_SECRET / STRIPE_PRO_PRICE_ID
Stripe Dashboard -- a free Stripe account in test mode for staging, live mode keys only once you are ready to take real payments
RESEND_API_KEY / RESEND_FROM_EMAIL
Resend dashboard -- free tier covers low-volume transactional email for a project this size
NEXT_PUBLIC_APP_URL
Your deployed Vercel URL, e.g. https://your-app.vercel.app
CRON_SECRET
Any random string you generate -- Vercel Cron sends it back automatically as an Authorization header

Local development uses a separate .env file on your machine and never touches these dashboard values directly -- Lesson 20 covers pulling them down safely with vercel env pull when you need to debug against staging or production data.

Step 3: Run Migrations Against the Production Database

This project deliberately does not wire db:migrate into the Vercel build command. Running schema migrations as an automatic side effect of every deploy is risky against a live production database -- a bad migration should not be one accidental push away from running unattended. Instead, run it explicitly, once, before or right after the first deploy:

DATABASE_URL="<production connection string>" npm run db:migrate

Run this from your own machine or a separate CI step, never as part of the Vercel build pipeline itself.

Step 4: Point Stripe's Webhook at the Live URL

In the Stripe Dashboard, add a webhook endpoint at https://<your-domain>/api/billing/webhook, and copy the signing secret it generates into the STRIPE_WEBHOOK_SECRET environment variable you set in Step 2. Without this, app/api/billing/webhook/route.ts will reject every event with a 400 -- the signature check fails by design when the secret does not match what Stripe actually signed with.

Step 5: Confirm the Cron Job Picks Up Automatically

vercel.json in the project root already declares the weekly digest cron:

{
  "crons": [
    { "path": "/api/cron/weekly-digest", "schedule": "0 13 * * 1" }
  ]
}

Vercel reads this file automatically on deploy -- there is no separate cron setup step in the dashboard. The only requirement is that CRON_SECRET is set, since Vercel Cron sends it back as Authorization: Bearer $CRON_SECRET and the route checks for a match before doing any work.

Step 6: Allow-List the Deployed Domain in Supabase

In the Supabase project's Authentication -> URL Configuration, add the deployed domain (e.g. https://your-app.vercel.app) to the redirect allow-list. Skip this and sign-in will fail with a redirect mismatch error the moment a real user tries it -- it is easy to forget because local development against localhost:3000 never needed it.

That is the full path from a pushed repo to a live URL with billing, auth, and a working cron job. The next lesson goes one level deeper on environment variables specifically -- how to keep development, preview, and production secrets cleanly separated as this project grows past a single deploy.

Key takeaways
  • vercel.com/signup is free with no credit card; the Hobby tier covers this project's deploy needs
  • Every variable in .env.example must be set in Vercel project settings before the first successful deploy -- production secrets are not the same values as your local .env file
  • DATABASE_URL in production should be Supabase's pooled Transaction connection string, since the app passes prepare: false specifically for pgbouncer compatibility
  • Migrations are run manually (DATABASE_URL=... npm run db:migrate) rather than wired into the Vercel build, because an automatic migration on every deploy is too risky for production data
  • The Stripe webhook will reject every event with a 400 until its dashboard-configured signing secret matches STRIPE_WEBHOOK_SECRET in Vercel
  • Forgetting to allow-list the deployed domain in Supabase Auth breaks sign-in with a redirect mismatch -- easy to miss since local development never needed it