Distribution
This section assumes pnpm build already dropped dist/, and supabase functions deploy already shipped your Edge Functions into the Supabase project. From here on, we look at the four paths that move those static artifacts and functions to a user’s browser.
build-and-package was where the tool stopped doing things automatically. Picking a host, attaching a custom domain, registering the Stripe production webhook, declaring CSP connect-src — none of that is automated. Production keys, DNS, and payment consoles all expect a human identity to act, and any automation that crosses that line drags leaks or misconfigured production endpoints behind it.
Four paths at a glance
Section titled “Four paths at a glance”| Path | Auto-build | Env injection | Edge Functions | Best fit |
|---|---|---|---|---|
| Vercel | On git push | Dashboard or vercel env | On Supabase | Default recommendation, least overhead |
| Netlify | On git push | Dashboard or netlify env:import | On Supabase | Vercel alternative |
| Cloudflare Pages | On git push | Dashboard or wrangler pages secret | On Supabase or Cloudflare Workers | Edge infrastructure integration |
| Self-host | Manual or CI | Hosting environment directly | On Supabase | When corporate policy blocks external hosting |
The four paths are not exclusive. Staging on a Vercel preview deploy and production on self-host is a fine split. Migrating off one onto another is also common.
Splitting the Supabase project
Section titled “Splitting the Supabase project”Before the host choice, the call to make first is splitting the Supabase project. Running production and staging on the same project means one wrong line in an RLS policy lands directly on real user data.
Supabase organization├── saas-base-production ← real users, real Stripe live key└── saas-base-staging ← developer testing, Stripe test keyEach project carries its own SUPABASE_URL, ANON_KEY, SERVICE_ROLE_KEY, and STRIPE_*_KEY. Production env on the host points to the production Supabase project, and the preview/staging env points to the staging one.
The migrations apply the same SQL to both.
supabase link --project-ref staging-refsupabase db push # staging first# verify, thensupabase link --project-ref production-refsupabase db push # then productionThe rule from architecture about never building policies through the UI sits right here. The same migration has to land in both environments identically, or drift starts leaking in.
Vercel
Section titled “Vercel”vercel link # link the projectvercel env pull .env.local # pull remote env into localvercel deploy --prod # production deployAfter one vercel link, every git push ships a preview deploy on its own. Production deploy comes from pushing to the main branch or an explicit vercel deploy --prod.
Env injection lives in the dashboard under Settings → Environment Variables, or through the CLI.
vercel env add VITE_SUPABASE_URL productionvercel env add VITE_SUPABASE_ANON_KEY productionvercel env add VITE_STRIPE_PUBLISHABLE_KEY productionSUPABASE_SERVICE_ROLE_KEY and STRIPE_SECRET_KEY do not go here. Both belong on the Edge Function side through supabase secrets set. Putting them on the hosting env, even if the SPA bundle never sees them, opens a build-time exposure risk.
Custom domain lives in the dashboard under Domains. One DNS A or CNAME record, and Vercel issues the SSL certificate on its own.
Netlify
Section titled “Netlify”netlify linknetlify env:import .env.productionnetlify deploy --prodnetlify env:import swallows .env.production wholesale. .env.production carries only production keys and stays in .gitignore. Deleting that file after import is the safer move.
The build command sits in netlify.toml.
[build] command = "pnpm build" publish = "dist"Custom domain and SSL follow the same path as Vercel.
Cloudflare Pages
Section titled “Cloudflare Pages”wrangler pages deploy distCloudflare Pages auto-deploys on git push, or you can push manually with wrangler pages deploy dist. Both routes work fine.
Env injection lives in the dashboard or in wrangler.
wrangler pages secret put VITE_SUPABASE_URL --project-name=saas-baseThe Cloudflare advantage is integration with Workers. If you want to move Edge Functions off Supabase onto Cloudflare Workers, swap to the Hono adapter under api/adapter-hono/. Setting VITE_API_MODE=hono flips the client to fetch('/api/...') calls that resolve through Workers routes.
That branch gets picked in phase 4 (/4-design-saas) at the API adapter decision step. The decision lands in .claude/state/api-adapter.json. Once it is set, swapping later means changing the adapter in one place while the features code does not move.
Self-host
Section titled “Self-host”Drop the dist/ from pnpm build onto nginx, S3 plus CloudFront, or your own static-hosting infrastructure.
# example: nginxsudo cp -r dist/* /var/www/saas-app/sudo nginx -s reload
# example: S3 + CloudFrontaws s3 sync dist/ s3://saas-app-bucket/aws cloudfront create-invalidation --distribution-id <id> --paths "/*"For auto-deploy, run the same script from GitHub Actions or GitLab CI. The env lives in the CI’s secrets.
Two extra seats matter for self-host: SPA fallback and cache policy. SPAs use client-side routing, so a direct hit on /app/settings needs nginx to return index.html.
location / { try_files $uri $uri/ /index.html;}For caching, index.html with no-cache plus JS/CSS chunks with long-cache (immutable, one year) is the safer combination. Vite hashes the chunk filenames, so a new deploy ships different filenames anyway.
CSP connect-src
Section titled “CSP connect-src”Declare connect-src in <meta http-equiv="Content-Security-Policy"> inside index.html. Arbitrary external URL fetches get blocked. Security baseline S8.
<meta http-equiv="Content-Security-Policy" content=" default-src 'self'; connect-src 'self' https://*.supabase.co https://api.stripe.com https://m.stripe.network; script-src 'self' https://js.stripe.com; frame-src https://js.stripe.com https://hooks.stripe.com; img-src 'self' data: https:;">Stripe Checkout opens through js.stripe.com and hooks.stripe.com, which is why both are listed in script-src and frame-src. If Supabase realtime is in use, add wss://*.supabase.co to connect-src.
Measurement tools (Sentry, PostHog, Mixpanel) are yours to add. Which one lands gets decided in phase 4, and this is one of the spots that fills in when you fork.
Registering the Stripe production webhook
Section titled “Registering the Stripe production webhook”The Stripe production webhook gets registered once.
- Stripe dashboard → Developers → Webhooks → Add endpoint.
- Endpoint URL:
https://<project-ref>.supabase.co/functions/v1/stripe-webhook. - Events to send:
checkout.session.completed,customer.subscription.created,customer.subscription.updated,customer.subscription.deleted,invoice.payment_succeeded,invoice.payment_failed. - Copy the Signing secret (
whsec_...) shown right after registration. - Store it as a Supabase secret.
supabase secrets set STRIPE_WEBHOOK_SECRET=whsec_...The stripe.webhooks.constructEvent(body, sig, secret) call inside the stripe-webhook Edge Function verifies the signature against this secret. A mismatch rejects every event. The most common cause of “webhook arrives, DB never updates” sits right there.
A production trigger like stripe trigger is in the no-auto-run list (S5). It only runs when the user calls it on purpose. Locally, stripe listen --forward-to localhost:54321/functions/v1/stripe-webhook forwards events, and production only handles real payment events — that combination is the safer one.
The real list of events and which tables get updated by them depends on your pricing model. The six above are the standard seat for a subscription SaaS; one-time payments or usage-based billing need different events. This is one of the spots that fills in when you fork.
Environment variable checklist
Section titled “Environment variable checklist”build-and-package already had one. Here it is again, split by hosting.
- Client (
VITE_*): on the hosting env for Vercel, Netlify, or Cloudflare Pages. On the build environment’s env for self-host. - Server (
SUPABASE_SERVICE_ROLE_KEY,STRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRET): in the Supabase project throughsupabase secrets set. Never on the hosting env.
What matters more than anywhere else is not mixing the production and staging keys. If a preview deploy points at the production Stripe key, test payments turn into real ones. Split the Stripe test keys (pk_test_..., sk_test_...) and the live keys (pk_live_..., sk_live_...) by environment, confirm which one is in the env one more time, and only then move into a production deploy.
End of expert tier
Section titled “End of expert tier”That closes the saas-base expert tier. Intro named the three guardrails, architecture covered the FSD six layers, the API adapter, the RLS SSOT, and the three-step Stripe split. Verification covered the six-dimension review and the S1 through S8 baseline. Build-and-package showed the two-track build. This section split the four paths that move those artifacts to a user’s browser.
Step-by-step walkthroughs, screenshots, and hosting console entries for non-developer readers live in the Beginner manual, where the same topics are paced differently.
Updates comes next. How the /update-from-base channel pulls in new skills, updated hooks, and AI_AUTOMATION.md changes from the base into your fork, plus the area split between base-owned, user-owned, and mixed.