Your Next.js template works locally. Pushing it to Vercel breaks the build because your local .env.local isn't uploaded and preview deploys hit production APIs.
Step 1: Push the Template to GitHub
If you cloned a template locally:
git init git add . git commit -m "Initial commit" gh repo create my-product --private --source=. --remote=origin --push Or create a repo in the GitHub UI and push. Most paid templates require keeping the source private per the license: check before pushing publicly.
Step 2: Import the Project to Vercel
- Go to
vercel.com/new - Import your GitHub repository (grant Vercel access when prompted)
- Vercel auto-detects Next.js
- Click Deploy
The first deploy runs without env vars and usually fails. That's expected: set them in Step 4 and redeploy.
Step 3: Configure the Build
Vercel auto-detects most configs, but verify:
- Framework Preset: Next.js
- Build Command:
next build(or whateverpackage.jsonsays) - Output Directory:
.next(handled automatically) - Install Command:
npm install(orpnpm install,yarn install) - Root Directory: path to your Next.js app if it's in a subfolder
For monorepos (Turborepo, etc.), set Root Directory to the Next.js app path (apps/web or similar).
Step 4: Set Environment Variables
This is where most first deploys fail. Your local .env.local is not uploaded. Set variables in Project > Settings > Environment Variables:
NEXT_PUBLIC_SUPABASE_URL=https://... NEXT_PUBLIC_SUPABASE_ANON_KEY=... SUPABASE_SERVICE_ROLE_KEY=... DATABASE_URL=... NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=... CLERK_SECRET_KEY=... STRIPE_SECRET_KEY=... STRIPE_WEBHOOK_SECRET=... For each variable, pick environments: Production, Preview, Development. Most secrets belong in all three with test/sandbox values for Preview and Development, live values for Production.
Mistake to avoid: putting production Stripe keys in Preview. Every preview deploy would charge real cards. Always use test keys in Preview.
Step 5: Add a Custom Domain
- Project > Settings > Domains
- Add your domain (e.g.,
mysaas.com) - Vercel gives you DNS records (usually A and CNAME)
- Add them at your registrar (Cloudflare, Namecheap, Porkbun, etc.)
- Wait for DNS propagation (usually under an hour, sometimes up to 48)
Once verified, your domain serves the production deployment with an automatic SSL certificate.
For www redirect: add both mysaas.com and www.mysaas.com, mark one as primary, and Vercel handles the redirect.
Step 6: Wire Up Preview Deployments
Vercel automatically creates a preview deploy for every PR:
- Builds the branch
- Deploys to a unique URL (
my-product-git-feature-abc.vercel.app) - Posts the URL as a comment on the PR
- Updates on new commits
For these to work with auth and payments, Preview env vars must use test/sandbox credentials. Otherwise every PR preview hits production data.
Step 7: Set Up Cron Jobs
Many SaaS apps need scheduled jobs. Vercel Cron handles this. Create vercel.json in your project root:
{ "crons": [ { "path": "/api/cron/publish-scheduled-posts", "schedule": "* * * * *" }, { "path": "/api/cron/send-digest", "schedule": "0 9 * * *" }, { "path": "/api/cron/sync-usage", "schedule": "0 */6 * * *" } ] } Each cron hits a GET endpoint on the schedule (standard cron syntax). Secure each endpoint:
// app/api/cron/publish-scheduled-posts/route.ts import { NextRequest, NextResponse } from 'next/server'; export async function GET(req: NextRequest) { const authHeader = req.headers.get('authorization'); if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) { return new NextResponse('Unauthorized', { status: 401 }); } // Do the scheduled work const posts = await db.post.findMany({ where: { status: 'scheduled', publishAt: { lte: new Date() } }, }); for (const post of posts) { await db.post.update({ where: { id: post.id }, data: { status: 'published' } }); } return new NextResponse('OK'); } Set CRON_SECRET in env vars. Vercel automatically sends it in the Authorization header.
Free tier includes basic cron; high-frequency cron (every minute) requires Pro.
Step 8: Configure ISR (Incremental Static Regeneration)
For pages that should cache but occasionally update (blog posts, product pages), use ISR:
export const revalidate = 60; // re-generate this page every 60 seconds export default async function BlogPost({ params }: { params: { slug: string } }) { const post = await db.post.findUnique({ where: { slug: params.slug } }); return <article>{post.content}</article>; } The first request after revalidate seconds gets new content (or regen happens in background). Other requests get the cached version.
For on-demand revalidation (after publishing a post):
import { revalidatePath } from 'next/cache'; revalidatePath(`/blog/${slug}`); This invalidates the cache for that path immediately. Combine with your publish flow so new posts appear instantly.
Step 9: Edge Config (for Feature Flags)
For values that change frequently but should be globally available (feature flags, A/B test cohorts, kill switches), use Edge Config:
import { get } from '@vercel/edge-config'; export async function MyComponent() { const isFeatureEnabled = await get('newDashboard'); return isFeatureEnabled ? <NewDashboard /> : <OldDashboard />; } Edge Config reads are <1ms globally. Writes are slow but rare. Perfect for kill switches and flags. Not necessary for v1 of most products: add when you need it.
Step 10: Monitor Performance
Vercel Analytics tracks Core Web Vitals out of the box (Project > Analytics). Free tier includes basic stats; Pro has detailed segmentation.
Pair with:
- Sentry for errors (frontend and API routes)
- PostHog for product analytics
- Better Stack for uptime alerting
These are third-party but Vercel has first-class integrations.
Common Gotchas
Forgetting environment variables. First deploy fails because .env.local vars aren't on Vercel. Add them in Settings > Environment Variables.
Production keys in Preview. Every PR preview charges real cards or hits production data. Use test/sandbox creds in Preview.
Image optimization not working. Use next/image and list external hosts in next.config.js:
module.exports = { images: { remotePatterns: [{ protocol: 'https', hostname: '**' }] } }; Webhook URLs pointing to localhost. Production webhooks (Stripe, Razorpay, Clerk) need your Vercel domain. Update URLs in each provider's dashboard when you deploy.
Cron not firing. Check schedule syntax. * * * * * is every minute; 0 9 * * * is 9 AM daily. Cron times are UTC. Verify in Vercel dashboard under Crons.
API routes timing out. Free tier has a 10-second timeout on serverless functions. Pro raises it to 60 seconds. For longer jobs, use background queues (Inngest, Trigger.dev) not API routes.
Massive serverless function size. Too many imports in an API route exceeds Vercel's 50 MB unzipped limit. Deploy fails with size error. Fix: move heavy imports into separate files or use route segment config.
Cold starts on infrequent endpoints. Serverless functions cold-start on first hit after idle. For latency-critical paths, use Edge functions or schedule a warmup cron.
DNS propagation delays. New domains can take up to 48 hours to fully propagate. Don't promise a launch time that depends on instant DNS.
Confusing Production and Preview. Every push to main goes to Production. Every push to a feature branch goes to Preview. Don't push to main without verifying.
This is a shorter version of a guide I wrote at Frontkit Studio. The full post has the complete walkthrough.
