Skip to main content

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):

ValueShape
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 unsetThe 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

VariablePurpose
DATABASE_URLPooled Postgres connection (Neon pooler) — runtime queries
DIRECT_URLDirect (non-pooled) connection — migrations
AUTH_SECRETAuth.js session encryption (openssl rand -base64 32)
AUTH_URLCanonical origin, e.g. https://tm.jentrix.ai
NEXT_PUBLIC_APP_URLPublic app URL (absolute links in emails, OAuth metadata)
CRON_SECRETBearer secret Vercel cron sends to /api/cron/* (openssl rand -hex 32)

Sign-in providers (at least one)

VariablePurpose
AUTH_GOOGLE_ID / AUTH_GOOGLE_SECRETGoogle 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_SECRETGitHub OAuth — callback https://<domain>/api/auth/callback/github
AUTH_RESEND_KEY / RESEND_FROMResend — 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.

VariablePurpose
SIGNUP_ALLOWLISTComma/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_EMAILComma/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)

VariableEnables
PUSHER_APP_ID / PUSHER_KEY / PUSHER_SECRET / PUSHER_CLUSTER + NEXT_PUBLIC_PUSHER_KEY / NEXT_PUBLIC_PUSHER_CLUSTERLive multi-user board sync
R2_ACCOUNT_ID / R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY / R2_BUCKET / R2_PUBLIC_BASE_URLFile 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_SECRETPer-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_SOURCESSigned 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_TOKENAvailability 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.

  1. Create a bot with Telegram's @BotFather and copy its token and username to TELEGRAM_BOT_TOKEN and TELEGRAM_BOT_USERNAME (without the leading @).

  2. Generate TELEGRAM_WEBHOOK_SECRET with openssl rand -hex 32 and deploy.

  3. 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\"]}"
    
  4. 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:

RouteScheduleDoes
/api/cron/sweep (both surfaces)every 5 minDue/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 minReconciles missed GitHub Issue/PR/check/workflow events and backfills the delivery graph
/api/cron/daily-digest (both surfaces)08:00 UTC dailyEmails opted-in users their unread notifications from the last 24h
/api/cron/outcomes (full only)06:00 UTC dailyRolls 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

  1. Sign in via each configured provider.

  2. Seed or create a workspace; drag a card in two browser windows to confirm realtime (if Pusher is configured).

  3. 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
    
  4. Run the sweep manually (above) — with VOYAGE_API_KEY newly set, the first sweeps backfill embeddings in batches; semantic features come alive as it progresses.

  5. If using OAuth clients: confirm discovery documents resolve at /.well-known/oauth-protected-resource and /.well-known/oauth-authorization-server.

Notes

  • Neon: use the pooled connection string for DATABASE_URL and the direct one for DIRECT_URL. If a deploy dies on a stuck prisma migrate advisory lock, terminate idle connections holding it and redeploy.
  • Postgres extensions: migrations enable pg_trgm (search) and vector (semantic layer) via CREATE 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 with extension "vector" is not available; the pgvector/pgvector:pg16 image is a drop-in replacement for postgres:16.
  • Custom domain: set AUTH_URL and NEXT_PUBLIC_APP_URL to 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.