Deployment
This guide covers how Sustentus applications are deployed. Each app in the monorepo deploys independently to Vercel as its own project, with its own environment variables and custom domain. Shared packages are built and bundled with the apps that consume them — they are not deployed separately.
For the full pipeline from commit to production (pre-commit, CI, and Vercel preview/production builds), see CI/CD pipeline.
Per-app Vercel configuration
Each app is a separate Vercel project. Set the root directory and build command, then add the app’s environment variables in the project settings.
| App | Root directory | Build command | Output directory |
|---|---|---|---|
| Agent | apps/agent | cd ../.. && pnpm agent:build | .next |
| Marketing | apps/marketing | cd ../.. && pnpm marketing:build | .next |
| Docs | apps/docs | cd ../.. && pnpm docs:build | .next |
| Storybook | apps/storybook | cd ../.. && pnpm storybook:build | storybook-static |
Install command is pnpm install for all projects.
Continuous deployment
Vercel deploys automatically: pushing to a PR branch publishes a preview build, and merging to main promotes to production. To roll back, open Deployments, find a previous deployment, and click Promote to Production.
Custom domains
In Project Settings > Domains, add the domain and configure DNS; the SSL certificate is provisioned automatically.
The web app is served at two domains on the one project: platform.sustentus.com and
demo.sustentus.com. The second is the public demo front door — the app decides which host it is
answering on from the DEMO_HOSTS allowlist, which is read at build time, so setting or
changing it requires a redeploy. See Demo environment for the full
runbook.
Environment variables
Manage variables in the Vercel dashboard or via the CLI:
vercel env add CLERK_SECRET_KEY production
vercel env pull .env.localVariables by app
Marketing:
# Required
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_...
CLERK_SECRET_KEY=sk_...
# Optional
NEXT_PUBLIC_POSTHOG_KEY=phc_...
NEXT_PUBLIC_POSTHOG_HOST=https://...Agent:
# Required
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_...
CLERK_SECRET_KEY=sk_...
AI_GATEWAY_API_KEY=...
MONGODB_URI=mongodb+srv://...
MONGODB_DATABASE_NAME=sustentus-...
# Optional
PLATFORM_APP_URL=https://...
AGENT_AUTHORIZED_PARTIES=https://ai.sustentus.com,https://platform.sustentus.comThe two Clerk variables must point at the same Clerk instance as the platform — that is what makes
a platform session valid in the agent app on a shared root domain. Without them the app builds and
then fails on every request, because the Clerk middleware cannot initialise. The Vercel project is
agentic-interface, rooted at apps/agent.
AI_GATEWAY_API_KEY is the Vercel AI Gateway credential, and it is needed on two projects —
agentic-interface (the assistant and its availability probe) and web (the BRD agent and the
onboarding concierge) — for Production and Preview on each. Declaring it in turbo.json →
globalEnv governs what turbo passes into build tasks under strict env mode and nothing else; a
serverless function reads its own Vercel project’s runtime environment, which turbo.json does not
populate. A missing key is silent rather than loud: the SDK falls back to the deployment’s OIDC
identity and, failing that, throws, which the availability probe reports as unknown — and
unknown renders nothing. Read the variable directly (vercel env ls --scope sustentus) rather
than inferring it from a quiet UI. See AI Gateway for the account, the
balance, and the smoke-test procedure.
The two Mongo variables are newer: since agentic-tenant-context the agent resolves the
caller’s tenant and reads that tenant’s data through @sustentus/services/server, so it opens a
database connection like the platform does. They carry the same runtime-not-build-time caveat as
the gateway key — both are in turbo.json → globalEnv already, which governs the build and
populates nothing at request time — so set them on agentic-interface for Production and
Preview, pointing at the same databases the web project uses. Without them a tenant user’s
context resolves to “no workspace” and a staff or partner caller is told their tenant directory
cannot be listed: honest empty states rather than a crash, which is exactly why the misconfiguration
is quiet. Read the variables directly rather than inferring them from a working page.
PLATFORM_APP_URL is the origin the platform app (web) is served from: since
agent-shell-layout-polish the agent’s header carries a labelled link back to the platform, and
this variable is its target. It is parsed with the same rules as the console’s CONSOLE_APP_URL —
a bare host is completed to https, http for localhost — and it is a runtime read with the same
caveat as the variables above, so set it on agentic-interface for Production and Preview.
Unset, it fails safe: the header simply renders no platform link, which is why this
misconfiguration is quiet too.
AGENT_AUTHORIZED_PARTIES is the agent app’s counterpart to the console’s
CONSOLE_AUTHORIZED_PARTIES, added by ai-foundation-hygiene: a comma-separated allowlist of the
origins whose Clerk sessions this deployment will accept, passed to clerkMiddleware as
authorizedParties. It is deliberately opt-in and empty by default. Sharing authentication across
subdomains of one root domain is exactly what lets a platform session sign the user in here without
satellite configuration, and it is the same property a compromised sibling subdomain would abuse —
naming the origins shuts that door in production, while leaving it unset keeps preview deployments
working, since they are served from per-commit hostnames no allowlist can predict. Set it on
Production only, and note that until you do, the variable buys nothing: the code reads it, the
security benefit starts when a value exists. The same is true of CONSOLE_AUTHORIZED_PARTIES on
tenant-management.
Documentation and Storybook: none required (static documentation / component showcase).
Scheduled jobs (cron)
The web app runs scheduled work through Vercel cron jobs , declared in apps/web/vercel.json. Crons run only against the production deployment.
| Schedule | Path | Job |
|---|---|---|
0 1 * * * | /api/cron/demo-reset | Purges every isDemo: true tenant back to the approved storyline, so the demo opens clean each morning. |
0 2 * * * | /api/cron/csm-portfolio-snapshot | Writes the daily per-CSM portfolio snapshot so the go live trend is a real day-over-day delta. |
The order is deliberate: the reset runs an hour before the snapshot, so each morning’s snapshot describes the freshly reset world rather than the residue of yesterday’s walkthrough.
Cron routes are protected by a shared secret. Set CRON_SECRET for the web app; Vercel sends it as Authorization: Bearer <CRON_SECRET> on each cron request, and the route rejects anything else with 401, so it is unreachable from a normal user session.
vercel env add CRON_SECRET productionBuild verification
Test a production build locally before deploying:
pnpm marketing:build
cd apps/marketing
pnpm start
# Visit http://localhost:3001Static export (docs)
The documentation site can be exported as a static site by setting output: "export" in apps/docs/next.config.ts; pnpm docs:build then emits to apps/docs/out/ for upload to any static host.
Storybook hosting
pnpm storybook:build outputs to apps/storybook/storybook-static/, which can be deployed to any static host. Chromatic additionally provides Storybook hosting, visual testing, and component review:
pnpm add -D chromatic --filter=@sustentus/storybook
npx chromatic --project-token=<token>