# Deploy to production

> Put your app online: a hosted Supabase project, the web app on Vercel, the environment variables, Supabase Auth settings, the two webhooks, and the phone app's production build.

Source: https://shipanysaas-demo.vercel.app/docs/getting-started/deploy-to-production
Published: 2026-10-19

Going live takes six steps: a hosted Supabase project with your schema, the web app on Vercel, its environment variables, Supabase Auth pointed at your domain, two webhooks, and a test run. Plan an afternoon for the first time. This page is the short version; the full guides are in the repository's `docs/going-to-production/` folder, starting with `checklist.mdoc`, including Cloudflare, Docker and your own server instead of Vercel.

## Before you start

You need accounts with:

- [Supabase](https://supabase.com) for the database, sign-in and file storage
- [Vercel](https://vercel.com) to host the web app
- Stripe or Lemon Squeezy for payments
- An email provider: Resend, or any SMTP service
- Your domain, so you can point it at Vercel

## 1. Create the Supabase project and push the schema

Create a project in the [Supabase dashboard](https://supabase.com/dashboard) and keep its project ref and database password. Then, from the repository root:

```bash
SUPABASE_PROJECT_REF=<your-project-ref> pnpm --filter web supabase:deploy
```

This links the hosted project and runs `supabase db push`, which applies every migration in `apps/web/supabase/migrations/`. Check the Table Editor afterwards: you should see `accounts`, `subscriptions`, `invitations` and the other kit tables.

**Never push the seed.** Don't add `--include-seed`, and never run `apps/web/supabase/seed.sql` against a hosted database. It creates test users with published passwords, two of them super admins.

## 2. Deploy the web app to Vercel

In Vercel, choose **Add New Project** and import your repository. Set **Root Directory** to `apps/web`; leave the framework (Next.js), build command and output directory on their defaults. The repository is a monorepo, so the build fails without the root directory.

The first deploy usually fails because the environment variables aren't there yet. That's expected: add them in the next step and redeploy.

## 3. Set the environment variables

Put your production values in `apps/web/.env.production.local` (git ignores it). Run `pnpm dev`, open the developer tool at [http://localhost:3010/variables](http://localhost:3010/variables) and switch it to **Production**: it checks every value, including the ones another setting makes required. Use **Copy** to paste the result into Vercel (**Settings**, then **Environment Variables**), then redeploy.

The ones every app needs:

| Variable | Value |
| --- | --- |
| `NEXT_PUBLIC_SITE_URL` | Your domain with `https://` and no trailing slash |
| `NEXT_PUBLIC_PRODUCT_NAME` | Your product's name |
| `NEXT_PUBLIC_SUPABASE_URL` | Supabase: **Project Settings**, then **API** |
| `NEXT_PUBLIC_SUPABASE_PUBLIC_KEY` | The project's publishable key |
| `SUPABASE_SECRET_KEY` | The project's secret key. It skips row-level security, so it stays on the server |
| `SUPABASE_DB_WEBHOOK_SECRET` | A random string: `openssl rand -base64 32` |
| `NEXT_PUBLIC_BILLING_PROVIDER` | `stripe` or `lemon-squeezy` |
| `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY`, `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET` | From Stripe (step 5 gives you the webhook secret) |
| `LEMON_SQUEEZY_SECRET_KEY`, `LEMON_SQUEEZY_STORE_ID`, `LEMON_SQUEEZY_SIGNING_SECRET` | Instead of the Stripe keys, if you use Lemon Squeezy |
| `MAILER_PROVIDER`, `EMAIL_SENDER` | `resend` or `nodemailer`, and the address your emails come from |
| `RESEND_API_KEY` | Only with Resend; the SMTP settings for `nodemailer` are in `docs/going-to-production/production-environment-variables.mdoc` |
| `NEXT_PUBLIC_AUTH_OAUTH_PROVIDERS` | The social sign-ins you set up, such as `google,github`, or `none`. It defaults to `google`, so set it if you don't use Google |

Values that start with `NEXT_PUBLIC_` are built into the page code, so changing one needs a redeploy. The build fails on some wrong public values, such as an `http://` site URL. Server secrets are checked when they're first used, so a missing Stripe key shows up in the function logs as `Please provide the variable STRIPE_SECRET_KEY`.

## 4. Point Supabase Auth at your domain

In the Supabase dashboard:

1. **Authentication**, then **URL Configuration**: set **Site URL** to `https://yourdomain.com` and add `https://yourdomain.com/auth/callback**` to **Redirect URLs**. Match your domain exactly, including `www` if you use it.
2. **Authentication**, then **Email Templates**: replace each template with the matching file in `apps/web/supabase/templates/` (confirm email, magic link, reset password, invite, change email, one-time code). With Supabase's default templates, a link opened in a different browser than the one that asked for it fails.
3. **Project Settings**, then **Authentication**, then **SMTP Settings**: enter your email provider's SMTP details. Supabase's built-in sender is only for testing and allows very few emails an hour.
4. For social sign-in, turn the providers on under **Authentication**, then **Providers**, and list the same ones in `NEXT_PUBLIC_AUTH_OAUTH_PROVIDERS`.

## 5. Add the two webhooks

Both need your public domain: Vercel preview URLs sit behind a login, so webhooks can't reach them.

**Payments.** In Stripe, add a webhook endpoint for `https://yourdomain.com/api/billing/webhook` with these events: `checkout.session.completed`, `checkout.session.async_payment_succeeded`, `checkout.session.async_payment_failed`, `customer.subscription.updated`, `customer.subscription.deleted` and `invoice.paid`. Copy its signing secret into `STRIPE_WEBHOOK_SECRET` and redeploy. With Lemon Squeezy, point its webhook at the same URL and put its signing secret in `LEMON_SQUEEZY_SIGNING_SECRET`.

**Database.** In Supabase, open **Database**, then **Webhooks**, and create a hook on the `subscriptions` table for the `DELETE` event, sending to `https://yourdomain.com/api/db/webhook` with the header `X-Supabase-Event-Signature` set to your `SUPABASE_DB_WEBHOOK_SECRET`. It cancels the payment subscription when a subscription row is deleted, for example with its account. [Webhooks](https://shipanysaas-demo.vercel.app/docs/billing/webhooks) explains both in more detail.

## 6. Check that it works

1. Open `https://yourdomain.com/api/healthcheck`. It should answer `{"services":{"database":true}}`.
2. Sign up with a real email address. The confirmation email should arrive from your provider, and its link should sign you in.
3. Create a team and invite a second address.
4. Buy a plan with Stripe in test mode first (card `4242 4242 4242 4242`). The plan should show on the billing page, and the webhook in Stripe should list the events as delivered.

Then switch Stripe to live keys and a live webhook. On Supabase's free plan, a project with no activity for a week is paused, so move to a paid plan before real customers depend on it.

## The phone app (All-in)

The phone app's `EXPO_PUBLIC_` values are built into the app when you build it, and the repository only ships development values. Before your first release build, create `apps/native/.env.production` (or set the same values in EAS):

| Variable | Production value |
| --- | --- |
| `EXPO_PUBLIC_SUPABASE_URL` | Your hosted Supabase project's URL |
| `EXPO_PUBLIC_SUPABASE_PUBLIC_KEY` | Its publishable key |
| `EXPO_PUBLIC_API_BASE_URL` | Your deployed web app, starting with `https://` |
| `EXPO_PUBLIC_SITE_URL` | The same web app, where the terms and privacy links open |

Without them the release build crashes on launch. Keep the sign-in settings the same as the web app's. Building and submitting to the App Store and Google Play is covered in `docs/native/building-shipping.mdoc` and `docs/native/app-store-release.mdoc`.

## After launch

- Write your privacy policy, terms and cookie policy in `apps/web/content/legal/`, and have a lawyer check them.
- Add `apps/web/public/.well-known/security.txt` so people know where to report a security problem.
- Set up error monitoring: `docs/monitoring/` covers Sentry.
- Every schema change ships the same way as step 1: see [Migrations](https://shipanysaas-demo.vercel.app/docs/database/migrations).
