Deployment
This page is for running your own Jentrix server. If you are using a deployment someone else operates, you need none of it — start at Getting started instead.
The reference production setup: Vercel (app + cron) and Neon (Postgres).
The full deployment runs at https://tm.jentrix.ai; the product deployment is a
second Vercel project with its own database and STACKS_SURFACE=mvp (see
below).
Surface modes
One codebase serves two deployment shapes, chosen by STACKS_SURFACE, read
server-side at build time (deliberately not a NEXT_PUBLIC_* var):
| Value | Shape |
|---|---|
mvp (exact match) | The product deployment: TASKS-first boards, projects, agent sessions, artifacts, and a 62-tool MCP catalog. The agent-operations planes are not built. |
| anything else, or unset | The full deployment: every plane, the Control Tower, and the 242-tool catalog. |
The value narrows four manifests and nothing else — which workspace routes render, which account routes render, which API routes are built, and which docs are served. Hidden workspace routes redirect to the workspace home rather than 404, so nothing is reachable-but-half-rendered. What the product deployment carries is documented in MVP surface; the surfaces only the full one adds are in Platform UI.
Everything below applies to both unless a section says otherwise.
How the build works
pnpm build runs prisma generate && prisma migrate deploy && next build — migrations apply during every deploy, against DIRECT_URL (the non-pooled connection). A reachable database is therefore required at build time; there is no separate migration step to remember.
Environment variables
Required
| Variable | Purpose |
|---|---|
DATABASE_URL | Pooled Postgres connection (Neon pooler) — runtime queries |
DIRECT_URL | Direct (non-pooled) connection — migrations |
AUTH_SECRET | Auth.js session encryption (openssl rand -base64 32) |
AUTH_URL | Canonical origin, e.g. https://tm.jentrix.ai |
NEXT_PUBLIC_APP_URL | Public app URL (absolute links in emails, OAuth metadata) |
CRON_SECRET | Bearer secret Vercel cron sends to /api/cron/* (openssl rand -hex 32) |
Sign-in providers (at least one)
| Variable | Purpose |
|---|---|
AUTH_GOOGLE_ID / AUTH_GOOGLE_SECRET | Google OAuth — callback https://<domain>/api/auth/callback/google. The Cloud-console consent screen must be published (or users added as test users); an unpublished screen surfaces as invalid_client / redirect_uri_mismatch even with correct credentials |
AUTH_GITHUB_ID / AUTH_GITHUB_SECRET | GitHub OAuth — callback https://<domain>/api/auth/callback/github |
AUTH_RESEND_KEY / RESEND_FROM | Resend — magic-link sign-in + invite/notification email |
Invite-only sign-up
Sign-up is gated: authenticating creates a new account only for an email that (a) already has a user, (b) has a pending, unexpired workspace invitation, or (c) matches SIGNUP_ALLOWLIST. Everyone else is denied (AccessDenied, surfaced on the login page) and no magic-link email is sent. Existing users always sign in normally.
Blocked visitors aren't a dead end: the login page offers a Request access form. Submissions are recorded (deduplicated per address) and emailed to SIGNUP_REQUEST_NOTIFY_EMAIL; approving one is just sending a normal workspace invitation from Members. Invitations expire after 7 days — the cron sweep then removes them and notifies the inviter in-app, so a lapsed invite can be re-sent.
| Variable | Purpose |
|---|---|
SIGNUP_ALLOWLIST | Comma/space-separated full emails (founder@acme.com) and/or domain suffixes (@acme.com) allowed to self-serve without an invitation. Empty = strictly invite-only |
SIGNUP_REQUEST_NOTIFY_EMAIL | Comma/space-separated addresses emailed when someone submits the login-page access-request form. Unset = requests are still recorded, but nobody is notified |
Bootstrapping a fresh deployment: with an empty database there are no users and no invitations, so nobody can sign up. Put the first admin's email in
SIGNUP_ALLOWLIST, have them sign in (creating their account + personal workspace), then they invite everyone else from Members. You can leave the allowlist in place or clear it afterward.
Optional services (features no-op without them)
| Variable | Enables |
|---|---|
PUSHER_APP_ID / PUSHER_KEY / PUSHER_SECRET / PUSHER_CLUSTER + NEXT_PUBLIC_PUSHER_KEY / NEXT_PUBLIC_PUSHER_CLUSTER | Live multi-user board sync |
R2_ACCOUNT_ID / R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY / R2_BUCKET / R2_PUBLIC_BASE_URL | File attachments (Cloudflare R2 or any S3-compatible store) |
VOYAGE_API_KEY (+ optional STACKS_EMBEDDINGS_MODEL, default voyage-3.5-lite) | Semantic layer: better search_tasks ranking, duplicate suggestions on bug panels, and, on full deployments, semantic duplicate lookup over MCP |
(full only) TELEGRAM_BOT_TOKEN / TELEGRAM_BOT_USERNAME / TELEGRAM_WEBHOOK_SECRET | Per-workspace task and human-action Telegram notifications with reply-as-task-comment |
(full only) GITHUB_APP_ID / GITHUB_APP_PRIVATE_KEY / GITHUB_APP_WEBHOOK_SECRET / GITHUB_APP_SLUG (+ optional client id/secret) | GitHub Issue, pull-request, and CI synchronization — see GitHub sync |
(full only) VERCEL_API_TOKEN (+ optional VERCEL_TEAM_ID, VERCEL_WEBHOOK_SECRET) | Vercel deployment ingestion and brokered rollback actions |
(full only) STACKS_CI_INGEST_SOURCES | Signed generic CI ingestion; JSON configuration is keyed per source |
(full only) OTEL_EXPORTER_OTLP_ENDPOINT (+ optional OTEL_EXPORTER_OTLP_HEADERS, OTEL_SERVICE_NAME) | Export of redacted platform traces; without it the collector drops spans after local processing — see Quality & release plane |
(full only) SLACK_SIGNING_SECRET / DOC_CONNECTOR_TOKEN | Availability of the Slack and document connector adapters; outbound credentials remain in the credential broker |
Never set in production: STACKS_WEBHOOK_ALLOW_PRIVATE (disables the webhook SSRF guard — dev-only).
Telegram bot setup (full deployments)
The per-member connect UI lives at Settings → Connections, which a product deployment does not build — these steps apply to full deployments only.
-
Create a bot with Telegram's
@BotFatherand copy its token and username toTELEGRAM_BOT_TOKENandTELEGRAM_BOT_USERNAME(without the leading@). -
Generate
TELEGRAM_WEBHOOK_SECRETwithopenssl rand -hex 32and deploy. -
Register the production webhook once:
curl -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/setWebhook" \ -H "content-type: application/json" \ -d "{\"url\":\"${NEXT_PUBLIC_APP_URL}/api/telegram/webhook\",\"secret_token\":\"${TELEGRAM_WEBHOOK_SECRET}\",\"allowed_updates\":[\"message\",\"callback_query\"]}" -
In Jentrix, open a workspace's Settings → Connections → Telegram notifications, choose Connect Telegram, and finish the one-time link in Telegram. Each member opts in independently for each workspace. Task notifications are immediate; Control Tower items that need a human are synced by the five-minute sweep. Decisions still happen in Jentrix through Open Control Tower — Telegram never approves or rejects a gate directly.
The webhook verifies Telegram's secret header and deduplicates update_id.
Outbound sends use a transactional outbox: the first attempt runs after the
response and the five-minute sweep retries transient failures. A Telegram
403 disables that member's workspace connection until they re-enable it.
Cron jobs
vercel.json schedules four jobs; Vercel calls them with Authorization: Bearer $CRON_SECRET. A product deployment builds only two of them — /api/cron/github and /api/cron/outcomes are not among its API routes, and the sweep's plane legs (harness gate/advance, projection reconciliation, experiment guardrails, memory quarantine) have nothing to do there:
| Route | Schedule | Does |
|---|---|---|
/api/cron/sweep (both surfaces) | every 5 min | Due/SLA reminders; Control Tower alerts; webhook, Telegram, and connector delivery retries; idempotency/receipt and credential/policy cleanup; embedding refresh; lease and stuck-run detection; harness gate/advance and projection reconciliation; experiment guardrails; memory quarantine/expiry; gating-evidence retention |
/api/cron/github (full only) | every 5 min | Reconciles missed GitHub Issue/PR/check/workflow events and backfills the delivery graph |
/api/cron/daily-digest (both surfaces) | 08:00 UTC daily | Emails opted-in users their unread notifications from the last 24h |
/api/cron/outcomes (full only) | 06:00 UTC daily | Rolls up Jentrix-native + DORA delivery outcome metrics over a trailing 90-day window, then mines delivery playbook drafts from that window's successful delivery runs and harness playbook drafts from that window's completed harness runs |
(/api/cron/webhooks-retry still exists for manual runs; the sweep covers it in normal operation.)
After a deploy, hit the sweep once and check the response counters:
curl -H "Authorization: Bearer $CRON_SECRET" https://tm.jentrix.ai/api/cron/sweep
Running agents
A deployment that carries the agent-operations planes also needs the harness runner and, on the managed path, the execution plane — their authentication modes, worktree configuration, and Temporal/sandbox topology are documented in Harness automation → Running the runner in production.
The @jentrix/runner session host that connected sessions need is a
different thing entirely and requires none of that — it is a local npm install,
covered in CLI install.
Post-deploy checklist
-
Sign in via each configured provider.
-
Seed or create a workspace; drag a card in two browser windows to confirm realtime (if Pusher is configured).
-
Mint a PAT and call the MCP endpoint:
curl -s https://your-deployment.example.com/api/mcp \ -H "Authorization: Bearer tm_..." -H "content-type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | head -c 400 -
Run the sweep manually (above) — with
VOYAGE_API_KEYnewly set, the first sweeps backfill embeddings in batches; semantic features come alive as it progresses. -
If using OAuth clients: confirm discovery documents resolve at
/.well-known/oauth-protected-resourceand/.well-known/oauth-authorization-server.
Notes
- Neon: use the pooled connection string for
DATABASE_URLand the direct one forDIRECT_URL. If a deploy dies on a stuckprisma migrateadvisory lock, terminate idle connections holding it and redeploy. - Postgres extensions: migrations enable
pg_trgm(search) andvector(semantic layer) viaCREATE EXTENSION IF NOT EXISTS— Neon supports both out of the box. Anywhere else (CI service containers, self-hosted, Docker dev) the server must have pgvector installed or the migration fails withextension "vector" is not available; thepgvector/pgvector:pg16image is a drop-in replacement forpostgres:16. - Custom domain: set
AUTH_URLandNEXT_PUBLIC_APP_URLto the final domain before configuring OAuth providers, and register callback URLs for every origin you actually use (production domain, previews, localhost for dev) — Google rejects mismatches hard. - The reference agents do not deploy to Vercel — run them on an always-on host pointed at the production MCP URL.