Deploying to Vercel
- 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:
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.
- 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