Skip to content

Get it to your users

This chapter is about moving the SaaS you built on your machine into someone else’s browser. The range goes from showing it to one or two people in beta, all the way to taking real payments in production. Decide up front how far you mean to go, then pick the section that matches.

One thing to flag before you start. What the base does on its own ends at the build (pnpm build) and Edge Function deployment (supabase functions deploy). Which host the build output lands on, how you set up the custom domain, where you register the Stripe production webhook endpoint — those are decisions and clicks for you. If a tool stepped into that seat, it would be handling your production keys and your payment account, which is not safe.

StepWhoHow
Vite SPA buildbase, automaticpnpm build or the deploy-saas skill
Edge Function deploybase, automaticsupabase functions deploy <name>
Onto a hostyouOne of Vercel, Netlify, Cloudflare Pages, self-host
Stripe production webhookyouEndpoint URL on the Stripe dashboard
Split a Supabase production projectyouA new project and key set on the Supabase dashboard

Pick the one section below that matches your situation and follow that path.

RouteWhere it fitsWhat you need in placegit push auto-deploy
VercelLeast frictionA Vercel accountyes
NetlifyAn alternative to VercelA Netlify accountyes
Cloudflare PagesCloudflare-stack integrationA Cloudflare accountyes
self-hostCompany policy forbids outside hostingA server that can serve static filesmanual or via CI

For a first time, Vercel is almost always enough. One git push produces a preview deploy, and a push to main goes to production. Custom domain and SSL are automatic.

When the deploy-saas skill finishes, a docs/deployment/DEPLOYMENT.md file lands. This chapter is the longer-form companion to that file.

Whichever host you pick, decide this first. Move the Supabase project you have been working with into a staging seat, and create a new project for production.

Supabase organization
├── my-saas-production ← real users, real Stripe live keys
└── my-saas-staging ← developer testing, Stripe test keys

This split is what stops one mistaken RLS policy from reaching real user data. Each project carries its own SUPABASE_URL, ANON_KEY, and SERVICE_ROLE_KEY, and the host’s production env and preview env point at different projects.

The same SQL goes to both as migrations.

Terminal window
supabase link --project-ref staging-ref
supabase db push # staging first
# verify, then
supabase link --project-ref production-ref
supabase db push # then production

The same migration lands the same way in both environments — that is what keeps drift out.

The shortest path, and usually the right starting point.

Terminal window
npm i -g vercel # install the CLI once
vercel link # link the project
vercel env pull .env.local # pull remote env into local (optional)
vercel deploy --prod # production deploy

Once vercel link is in place, git push triggers preview deploys on its own. Production deploys come from a push to main or an explicit vercel deploy --prod.

Env injection lives on the dashboard under Settings → Environment Variables. The CLI works too:

Terminal window
vercel env add VITE_SUPABASE_URL production
vercel env add VITE_SUPABASE_ANON_KEY production
vercel env add VITE_STRIPE_PUBLISHABLE_KEY production
vercel env add VITE_API_MODE production # edge-functions

SUPABASE_SERVICE_ROLE_KEY and STRIPE_SECRET_KEY do not go here. Those land on the Edge Function side via supabase secrets set. Putting them in host env, even though they would not ship in the SPA bundle, still leaves them exposed at build time.

Custom domain lives on the dashboard under Domains. Add an A or CNAME DNS record; Vercel issues SSL automatically.

Terminal window
npm i -g netlify-cli
netlify link
netlify env:import .env.production
netlify deploy --prod

netlify env:import ingests the whole .env.production file. .env.production is the file with only the production keys, and it has to be in .gitignore. Deleting that file after the import is the safer move.

The build command lives in netlify.toml:

[build]
command = "pnpm build"
publish = "dist"

Custom domain and SSL flow the same way as Vercel.

Terminal window
npm i -g wrangler
wrangler login
wrangler pages deploy dist --project-name=my-saas

Cloudflare Pages can auto-deploy on git push or take a manual push via wrangler pages deploy dist. Both routes work.

Env injection through dashboard or wrangler.

Terminal window
wrangler pages secret put VITE_SUPABASE_URL --project-name=my-saas
wrangler pages secret put VITE_SUPABASE_ANON_KEY --project-name=my-saas
wrangler pages secret put VITE_STRIPE_PUBLISHABLE_KEY --project-name=my-saas

Custom domain lives on the dashboard under Custom domains. If Cloudflare also handles your DNS, the whole thing stays in one seat — convenient.

When company policy blocks outside hosting, or when you want it on your own infra.

Terminal window
pnpm build
# Move dist/ to your server's nginx or s3 static hosting seat

dist/ is a bundle of static files — index.html, vendor chunks, per-route chunks, and a CSS bundle. As an SPA with one entry point, all nginx needs is try_files $uri /index.html.

Env variables get baked into the bundle at build time, so different environments need different builds. Either build per environment, or build in a CI pipeline that injects env from outside at build time.

For the webhook to work on the hosted side, you register a new endpoint on the Stripe dashboard. This is a different seat from stripe listen in local development.

  1. On the Stripe dashboard, head to Developers → Webhooks in the left menu.
  2. Confirm the top-right Test mode toggle is in production (if this is a production deploy). Endpoints registered in test mode never receive production payment events.
  3. Hit Add endpoint and paste in the endpoint URL.
  4. The URL is the public URL of the Edge Function: https://<project-ref>.supabase.co/functions/v1/stripe-webhook.
  5. Under “Listen to”, pick the events your SaaS needs. Typically checkout.session.completed, customer.subscription.updated, customer.subscription.deleted, and invoice.paid.
  6. After registering, copy the endpoint’s Signing secret (whsec_...).
  7. Set that secret on the production Supabase project.
Terminal window
supabase link --project-ref production-ref
supabase secrets set STRIPE_WEBHOOK_SECRET=whsec_...

Confirm the webhook is registered cleanly by going to the endpoint page on the Stripe dashboard and hitting Send test webhook. A verification-passed line in the Edge Function logs is the healthy shape.

Every external service the SaaS reaches (Supabase, Stripe, analytics) has to be listed in Content Security Policy’s connect-src. The base ships a skeleton in index.html:

<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
connect-src 'self' https://*.supabase.co https://api.stripe.com;
script-src 'self' https://js.stripe.com;
">

If your SaaS reaches additional external domains (analytics, CDN, etc.), add them here. This is one of the spots that fills in when you fork.

Mistakes in production around these are nearly unrecoverable. Treat them as priority one from day one.

  • STRIPE_SECRET_KEY (sk_live_...) — rotates from the Stripe dashboard. If exposed, rotate immediately.
  • SUPABASE_SERVICE_ROLE_KEY — rotates from the Supabase dashboard. Rotating cuts every client off.
  • Both above only live under supabase secrets set. Never inside any .env* file.
  • STRIPE_WEBHOOK_SECRET differs per endpoint. Test mode and production hold different values.

Do not commit .env* files to git. Only .env.example belongs in git. Before you start, confirm .gitignore covers .env*.

This base is a commercial template licensed from goldtagworks. The SaaS you build on top is yours to license and sell however you want. Your users have no relationship with goldtagworks — what they see is your SaaS only.

The base itself comes with two constraints, though.

  • Do not redistribute saas-base’s source or this manual. Only the SaaS you built on top is what you ship.
  • Leave LICENSE and COMMERCIAL-LICENSE.md in your working copy. They do not need to ship inside the dist/ build output. What reaches users is your SaaS, not the base.

The distribution license of your SaaS is yours to set. Running a SaaS usually leans harder on the Terms of Service and Privacy Policy seats than on the license seat. Both have to be in place before you take real payments. The LICENSE and COMMERCIAL-LICENSE.md the base carries are about your right to use the base — not about your SaaS’s terms with its users.

That covers every deployment route the base assumes. Once the flow is familiar, confirm one more time that LICENSE and package.json reflect your SaaS info, and that .env* files never got committed.

If something stuck while deploying, start in 09-troubleshooting.md, and keep the auto-generated docs/deployment/DEPLOYMENT.md open alongside.