Deployment
How Sajt deploys to Vercel + Convex, the post-deploy auth check, going live on gated integrations, and the dev-only routes that 404 in production.
Sajt is two deployables: the Next.js app on Vercel and the Convex
backend. Sources: DEPLOY.md, SETUP.md, docs/ARCHITECTURE.md,
docs/domain-system-plan.md, docs/current-state.md.
How it deploys
The repo is Vercel-ready. vercel.json runs a plain bun run build, and the
frontend talks to whatever NEXT_PUBLIC_CONVEX_URL points at. The Convex backend
deploys separately with npx convex deploy (or convex dev during development).
To make backend + frontend deploy atomically on every push, set
CONVEX_DEPLOY_KEY (production scope) in Vercel and change the build command to
Convex's integrated form:
npx convex deploy --cmd 'bun run build'This deploys Convex and injects the production NEXT_PUBLIC_CONVEX_URL
automatically. NEXT_PUBLIC_CONVEX_SITE_URL is derived from it at runtime
(lib/site/convexSiteUrl.ts), so it does not need to be set on Vercel.
Every push to main triggers a production deploy (the Vercel project is linked to
the GitHub repo via vercel link + vercel git connect).
Required Vercel env vars
Names and purpose only — full list in Environment variables.
| Variable | Notes |
|---|---|
CONVEX_DEPLOY_KEY | Production deploy key; lets the build run convex deploy |
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY | Use the production Clerk instance key |
CLERK_SECRET_KEY | Production Clerk instance key (secret) |
CLERK_JWT_ISSUER_DOMAIN | Also set on the Convex prod deployment |
OPENROUTER_API_KEY (optional) | Set on the Convex prod deployment, not Vercel — it is used by a Convex action |
The auth gotcha — verify this post-deploy
Convex auth needs a Clerk JWT template named exactly convex
(convex/auth.config.ts, applicationID: "convex"). Without it, client sign-in
works but Convex never receives identity, so the deployed app shows the
signed-out state. Set the issuer on the Convex production deployment:
npx convex env set CLERK_JWT_ISSUER_DOMAIN https://<your-app>.clerk.accounts.dev --prodUse the production Clerk instance keys on Vercel and confirm the Convex issuer
matches that instance. If TestFlight / Xcode logs show
Clerk has been loaded with development keys, the deployed web app is still
serving pk_test / sk_test keys — replace them with the production instance
keys, confirm the issuer, and redeploy. See
Auth & permissions.
Going live on gated integrations
Most integrations ship in dev / mock mode and only activate once their credentials are set (the dev-fallback principle). To go live:
- Billing & payments (Stripe). Set
STRIPE_SECRET_KEYand the webhook secrets (STRIPE_BILLING_WEBHOOK_SECRET,STRIPE_CONNECT_WEBHOOK_SECRET,STRIPE_WEBHOOK_SECRET) on the Convex deployment. Plan gating and the credit ledger already work; the live operator setup is the remaining step (docs/billing-setup.md). - Email (Resend). Set
RESEND_API_KEY+RESEND_FROMon the Convex deployment; until then transactional email is silently skipped and invites use copy-link (docs/email-setup.md). - Custom domains. Customers connect a domain from their site's Settings →
Custom domain: it is added to this Vercel project (Vercel terminates TLS),
they add the shown
_sajt-verify.<domain>TXT record, and the app verifies via DNS-over-HTTPS. SetVERCEL_API_TOKEN/VERCEL_PROJECT_ID/VERCEL_TEAM_IDon the Convex deployment, andNEXT_PUBLIC_APP_HOSTso middleware skips the host lookup for the app's own domain. - Platform subdomains. Add a wildcard
*.<platform-domain>to the Vercel project (plus apex /wwwif the dashboard or marketing lives there), setNEXT_PUBLIC_SITES_DOMAIN, and redeploy. Every site is then reachable at<slug>.<platform-domain>; the/site/<slug>path keeps working as a fallback. Dormant untilNEXT_PUBLIC_SITES_DOMAINis set. Seedocs/domain-system-plan.mdand Publishing.
A live custom domain is never auto-downgraded by polling (to avoid flapping a site offline on a DNS blip); only an explicit remove takes it down.
Dev-only routes (404 in production)
These exist for local development and design review and are not served on the production domain:
| Route | Gate |
|---|---|
/deveditor | notFound() when NODE_ENV === "production" (mock-data editor harness) |
/devpreview | notFound() when NODE_ENV === "production" (mock-data preview harness) |
/landing-lab, /landing-lab/* | notFound() when VERCEL_ENV === "production" (visible on preview deployments, hidden on prod) |
See the full route map in Routes.
Native shells
The macOS / Windows desktop app (Tauri) and the iOS shell (Capacitor) load this
same deployed web app in a native window — they ship separately from the
Vercel deploy. Web changes reach them with only a redeploy. When pointing a shell
at a new origin, add that origin to the Tauri capability allow-list
(src-tauri/capabilities/default.json). Details in docs/desktop-app.md and
docs/ios-app.md.
Verify before shipping
Run the project's checks locally first:
bun run test
bun run lint
bunx tsc --noEmit
bun run buildPush the backend with bunx convex dev --once (one-shot push + typecheck).