# Hosting and deploys

This page is for the person who hosts the site, or for your AI agent. Most owners never need it.

> **Caution.** Wrong environment values can take the site offline or send email from the wrong address. Back up first. Do not paste secret keys into chat logs. Ask your agent to do this with you.

## Who should do this

- Turning on a domain
- Setting environment variables on Vercel (or another host)
- First-time Supabase, Resend, or Twilio setup

Owners still click merge. The agent should not dispatch production database jobs unless you say so.

## Where the site runs

Vercel is the usual host. Merging to `main` deploys production. There are no pull-request preview deploys.

The app also runs on any host that can run Node 24 with `pnpm build` and `pnpm start`. Details for Docker and other hosts live in the internal `docs/HOSTING.md` in the product repo. The environment **names** are the same everywhere.

Health check: `GET /api/health` returns `{"ok":true}`.

## Environment variables

Set these on the host (Vercel → Environment Variables). Names:

| Name | What it is |
| --- | --- |
| `NEXT_PUBLIC_SITE_URL` | Public address, such as `https://www.example.com`. Baked in at **build** time. |
| `NEXT_PUBLIC_SUPABASE_URL` | Supabase project URL. Build time. |
| `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY` | Supabase publishable key. Build time. |
| `SUPABASE_SECRET_KEY` | Supabase secret key. Runtime. |
| `NPM_TOKEN` | GitHub token with `read:packages`, so the build can install `@xylo/core`. Never commit it. |
| `RESEND_API_KEY` | Email. |
| `EMAIL_SENDER` | From-address, such as `Brightside Studio <hello@example.com>`. |
| `STAFF_NOTIFY_EMAILS` | Who hears about new leads, comma-separated. |
| `STAFF_NOTIFY_PHONES` | Staff mobiles in E.164 (`+1…`), comma-separated. |
| `NEXT_PUBLIC_SMS_ENABLED` | `true` only after Twilio is approved. Build time. |
| `TWILIO_ACCOUNT_SID` / `TWILIO_AUTH_TOKEN` | Texting. |
| `TWILIO_MESSAGING_SERVICE_SID` | `MG…`, preferred. |
| `TWILIO_FROM_NUMBER` | Only if you are not using a messaging service. |
| `CRON_SECRET` | Random, 32+ characters. Used by the follow-up job. |
| `STRIPE_SECRET_KEY` | `sk_test_…` or `sk_live_…`. See [Stripe setup](./stripe.md). |
| `STRIPE_WEBHOOK_SECRET` | `whsec_…`. |
| `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` | `pk_…`. Build time. |
| `NEXT_PUBLIC_CAPTCHA_SITE_KEY` / `CAPTCHA_SECRET_TOKEN` | Optional Turnstile, only if spam gets through. |
| `CLIENT_IP_HEADER` | Optional. Host’s visitor-IP header. |
| `XYLO_PUBLIC_SUPABASE_URL` | Optional. Only if phones cannot use the public Supabase URL. |
| `POWERSYNC_URL` | Optional. Only if the native app syncs offline. |

Leave Twilio and Stripe **unset on Preview**, so preview URLs never text real people or take real money.

`NEXT_PUBLIC_*` values are baked in at build time. Changing one means rebuild (a new deploy).

## What a merge does

1. You merge the pull request to `main` with a merge commit.
2. Vercel builds and deploys.
3. If the pull request added files under `supabase/migrations/`, [migrations](./backups.md) start on their own: backup, rehearse, then apply.

Code can go live a minute before migrations finish. New code is written to work with the old schema for that window.

## First-time launch extras

The full launch runbook is internal (`docs/LAUNCH.md`). In short: create the owner’s Supabase project, turn off public sign-up, set site URL and `/auth/confirm`, paste the email templates, add Resend, then deploy. Invite the first owner with `pnpm owner:invite`. Everyone else is invited from admin → Staff.

## Follow-up job

Supabase `pg_cron` calls `/api/cron/nurture` every 15 minutes with `CRON_SECRET`. Vercel also calls it once a day as a fallback. Backups do **not** run on Vercel. They run in GitHub Actions. See [Backups and database changes](./backups.md).
