Sajt · Docs

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.

VariableNotes
CONVEX_DEPLOY_KEYProduction deploy key; lets the build run convex deploy
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEYUse the production Clerk instance key
CLERK_SECRET_KEYProduction Clerk instance key (secret)
CLERK_JWT_ISSUER_DOMAINAlso 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 --prod

Use 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_KEY and 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_FROM on 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. Set VERCEL_API_TOKEN / VERCEL_PROJECT_ID / VERCEL_TEAM_ID on the Convex deployment, and NEXT_PUBLIC_APP_HOST so middleware skips the host lookup for the app's own domain.
  • Platform subdomains. Add a wildcard *.<platform-domain> to the Vercel project (plus apex / www if the dashboard or marketing lives there), set NEXT_PUBLIC_SITES_DOMAIN, and redeploy. Every site is then reachable at <slug>.<platform-domain>; the /site/<slug> path keeps working as a fallback. Dormant until NEXT_PUBLIC_SITES_DOMAIN is set. See docs/domain-system-plan.md and 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:

RouteGate
/deveditornotFound() when NODE_ENV === "production" (mock-data editor harness)
/devpreviewnotFound() 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 build

Push the backend with bunx convex dev --once (one-shot push + typecheck).

On this page