Deployment Guide

Production checklist, deployment paths (Vercel / Docker / self-hosted), env var reference, AI provider configuration, the daily credit-reset cron, monitoring, backups, and post-deploy verification.

For the quick walkthrough see Deployment. This guide is the long-form companion.

Pre-deploy checklist

Code & config

  • Supabase SQL schema applied (src/db/schema.sql) including pgvector extension
  • Supabase Storage bucket uploads created
  • src/config/brand.ts updated (product name, emails, social)
  • src/config/seo.ts updated (default title/description, OG image)
  • Logo assets replaced in public/ (light, dark, mobile, favicon)
  • Pricing tiers in src/config/pricing.ts match what you sell
  • AI model list in src/config/ai-models.ts matches what you'll allow
  • System prompt reviewed (src/app/api/chat/route.ts or src/lib/ai/system-prompt.ts)

Environment

  • All required env vars set in production (table below)
  • Stripe is in live mode, not test
  • Stripe webhook URL points at production, signing secret captured
  • Supabase Auth Site URL + Redirect URLs use production domain
  • OpenAI / Anthropic keys are production keys with usage caps configured

Tests

  • pnpm test passes
  • pnpm build succeeds locally with production env vars

Required environment variables

VarWhere it's usedServer-only?
NEXT_PUBLIC_APP_URLCanonical URLs, redirectsNo
NEXT_PUBLIC_SUPABASE_URLSupabase clientNo
NEXT_PUBLIC_SUPABASE_ANON_KEYSupabase client (client-side)No
SUPABASE_SERVICE_ROLE_KEYServer-only privileged opsYes
STRIPE_SECRET_KEYStripe API callsYes
STRIPE_WEBHOOK_SECRETWebhook signature verificationYes
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEYStripe.js clientNo
NEXT_PUBLIC_STRIPE_PORTAL_URLCustomer portal linkNo
STRIPE_PRO_PRICE_IDPro plan checkoutYes
STRIPE_TOPUP_PRICE_IDCredit top-up checkoutYes
STRIPE_ENTERPRISE_PRICE_IDEnterprise plan checkoutYes
OPENAI_API_KEYOpenAI LLMs + embeddingsYes
ANTHROPIC_API_KEYAnthropic LLMsYes
CRON_SECRETAuth header for cron endpointYes

NEXT_PUBLIC_* vars are bundled into client JS: never store secrets there.

Path 1: Vercel (recommended)

Vercel is the default target: the cron in vercel.json and Next.js 16 App Router are tuned for it.

  1. Push to GitHub.
  2. Vercel → Add New Project → Import the repo.
  3. Framework auto-detected as Next.js. Leave defaults.
  4. Add all env vars under Project Settings → Environment Variables (Production, Preview, Development scopes).
  5. Click Deploy.
  6. Stripe → Developers → Webhooks → Add endpoint:
    • URL: https://yourdomain.com/api/webhooks/stripe
    • Events: checkout.session.completed, customer.subscription.updated, customer.subscription.deleted, customer.subscription.created, invoice.payment_succeeded
    • Copy the signing secret into STRIPE_WEBHOOK_SECRET and redeploy.
  7. Supabase → Auth → URL Configuration:
    • Site URL: https://yourdomain.com
    • Redirect URLs: https://yourdomain.com/auth/callback
  8. Verify the cron runs (see "Daily credit-reset cron" below).

Vercel plan note

The shipped vercel.json declares a daily cron at 00:00 UTC for /api/cron/reset-credits. Vercel Hobby does not include cron jobs: cron functionality requires Vercel Pro or higher. If you stay on Hobby, you must trigger the endpoint from an external scheduler (cron-job.org, GitHub Actions on a schedule, an Upstash QStash schedule, etc.) using the CRON_SECRET for auth.

Daily credit-reset cron

The endpoint at /api/cron/reset-credits runs once a day to refill monthly credit allocations for active subscribers. It's authenticated by the CRON_SECRET env var.

On Vercel Pro+: the schedule in vercel.json (0 0 * * *) handles it: set CRON_SECRET and it just works. Verify the next day in Vercel → Logs (filter by /api/cron/reset-credits).

Off Vercel (or on Hobby): trigger externally:

# Once a day from any cron, GitHub Actions, or Upstash QStash
curl -X POST https://yourdomain.com/api/cron/reset-credits \
  -H "Authorization: Bearer $CRON_SECRET"

Sample GitHub Actions workflow (.github/workflows/reset-credits.yml):

name: Reset credits
on:
  schedule:
    - cron: "0 0 * * *"
jobs:
  reset:
    runs-on: ubuntu-latest
    steps:
      - run: |
          curl -fsS -X POST $URL \
            -H "Authorization: Bearer $SECRET"
        env:
          URL: https://yourdomain.com/api/cron/reset-credits
          SECRET: ${{ secrets.CRON_SECRET }}

AI provider configuration

The template uses the Vercel AI SDK with adapters for OpenAI and Anthropic. To customize:

Restrict / expand the model list

Edit src/config/ai-models.ts. Each entry maps an internal id to a provider, display name, credit cost, and tier access.

Add a new provider (Groq, Together, Mistral, etc.)

  1. Install the AI SDK adapter:
    pnpm add @ai-sdk/groq
    
  2. In src/lib/ai/providers.ts (or wherever the chat route resolves the model), add a provider switch case:
    import { groq } from "@ai-sdk/groq";
    
    if (provider === "groq") return groq(modelId);
    
  3. Add an env var (GROQ_API_KEY) and reference it in the SDK init.
  4. Add the new model entries in src/config/ai-models.ts.

Reference: Vercel AI SDK provider docs.

Embeddings provider

The shipped pipeline uses OpenAI embeddings for documents (text-embedding-3-small). To swap (e.g., to Cohere or local), edit the embed* calls in src/lib/rag/: typically in an embedDocuments.ts or similar file. Match the embedding dimension to your pgvector column type.

Path 2: Docker

Sample Dockerfile:

FROM node:20-alpine AS base
WORKDIR /app
RUN corepack enable

FROM base AS deps
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

FROM base AS build
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ARG NEXT_PUBLIC_APP_URL
ARG NEXT_PUBLIC_SUPABASE_URL
ARG NEXT_PUBLIC_SUPABASE_ANON_KEY
ARG NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
ARG NEXT_PUBLIC_STRIPE_PORTAL_URL
RUN pnpm build

FROM base AS runner
ENV NODE_ENV=production
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/.next/static ./.next/static
COPY --from=build /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]

Add output: "standalone" to next.config.ts:

const nextConfig: NextConfig = { output: "standalone", /* ... */ };

Run with all server-only env vars passed at runtime:

docker run -p 3000:3000 \
  -e SUPABASE_SERVICE_ROLE_KEY=... \
  -e STRIPE_SECRET_KEY=... \
  -e STRIPE_WEBHOOK_SECRET=... \
  -e OPENAI_API_KEY=... \
  -e ANTHROPIC_API_KEY=... \
  -e CRON_SECRET=... \
  -e STRIPE_PRO_PRICE_ID=... \
  -e STRIPE_TOPUP_PRICE_ID=... \
  -e STRIPE_ENTERPRISE_PRICE_ID=... \
  saasforge-ai

Front the container with a TLS-terminating proxy. Schedule the credit-reset via host crontab or systemd timer.

Path 3: Self-hosted Node (VPS)

git clone https://github.com/your-org/saasforge-ai /opt/saasforge-ai
cd /opt/saasforge-ai
pnpm install --frozen-lockfile
cp .env.local.example .env.local   # fill in production values
pnpm build
pnpm add -g pm2
pm2 start "pnpm start" --name saasforge-ai
pm2 save && pm2 startup

Reverse proxy with Caddy (/etc/caddy/Caddyfile):

yourdomain.com {
  reverse_proxy localhost:3000
}

Schedule the credit reset via host cron:

0 0 * * * curl -fsS -X POST https://yourdomain.com/api/cron/reset-credits -H "Authorization: Bearer $CRON_SECRET"

Stripe webhook setup

The webhook handler is at /api/webhooks/stripe. Required events:

  • checkout.session.completed: fulfill new subscriptions and credit top-ups
  • customer.subscription.created
  • customer.subscription.updated: plan upgrades/downgrades
  • customer.subscription.deleted: cancellations
  • invoice.payment_succeeded: credit refresh on renewal
  • invoice.payment_failed: dunning

Test locally with the Stripe CLI:

stripe listen --forward-to localhost:3000/api/webhooks/stripe
stripe trigger checkout.session.completed

Monitoring

ConcernToolFree tier?
Frontend errorsSentryYes
API/server logsVercel logs / Logtail / AxiomYes
Web VitalsVercel Analytics (already wired in template)Yes (Pro)
UptimeBetter Stack / UptimeRobotYes
AI provider quotaOpenAI / Anthropic dashboardsYes
Stripe webhook deliveriesStripe Dashboard → WebhooksYes
Supabase queriesSupabase Dashboard → ReportsYes

Set hard usage caps on OpenAI and Anthropic: a runaway loop without caps can spend hundreds of dollars before you notice.

Backup strategy

DataWhereRecovery
Postgres + vectorsSupabase Daily backups (Pro+)Restore from dashboard; PITR on Pro+
Uploaded documentsSupabase Storage uploads bucketBucket versioning
EmbeddingsPostgres (already backed up)Re-embed via background job if lost
StripeStripe-siden/a
Code + envsGit + secret managerRe-deploy from prior commit

For Supabase free-tier: schedule pg_dump to S3 (works for the small/embedding tables; very large vector tables get expensive).

Post-deploy verification

  1. Hit /: landing page renders, no console errors
  2. Sign up → confirmation email arrives → confirm
  3. Sign in
  4. Send a chat message: response streams back, credits decremented
  5. Upload a PDF: appears in document list, embeddings job completes (check documents + embeddings tables in Supabase)
  6. Ask a RAG question with the doc selected: response cites context, RAG indicator shows
  7. Subscribe to a paid plan with a Stripe test card (4242 4242 4242 4242)
  8. Check Customer Portal opens
  9. Cancel the subscription → webhook fires → status updates
  10. Wait until 00:00 UTC (or invoke manually with CRON_SECRET) → confirm credits reset for active subscribers in credit_ledger

If any step fails: check Vercel logs + Supabase logs + Stripe webhook delivery log + OpenAI/Anthropic dashboard for rate-limit hits.

Custom domain

  1. Vercel → Project → Settings → Domains → Add → set DNS records as instructed.
  2. Set NEXT_PUBLIC_APP_URL to the new domain → redeploy.
  3. Supabase Auth → URL Configuration → update Site URL + Redirect URLs.
  4. Stripe webhooks → update endpoint URL → capture new signing secret if required.
  5. Re-test sign-up + checkout end-to-end.

Rollback

  • Vercel: Deployments → previous deployment → Promote to Production.
  • Docker / VPS: git checkout <previous-tag> → rebuild → restart.

Database schema changes are not rolled back automatically: write reverse migrations or be prepared to repair manually.