| Database | | | | |
DATABASE_URL | Yes (pooled, runtime) | — | — | One Neon connection string shared by api/mcp |
DATABASE_URL_UNPOOLED | Yes (deploy step only) | — | — | Direct Neon connection string; migrations only |
| Auth (issuer / audience / provider) | | | | |
AUTH_ISSUER | Yes | — | — | Auth0 tenant, or AS_ISSUER value in the SAML path |
JWKS_URL | Yes | — | — | Auth0 JWKS, or https://<instance>/oauth/jwks in the SAML path |
AUTH_AUDIENCE | Yes | — | — | The API/default audience, accepted at both doors; must equal AS_AUDIENCE / UI OIDC_AUDIENCE |
AUTH_PROVIDER_NAME | Yes | — | — | auth0, or saml:<idp-key> in the SAML path (max 32 chars) |
MCP_AUDIENCE | Yes, to serve MCP clients | — | — | The MCP resource indicator, https://<instance>/mcp. Independent of AS_AUDIENCE — no equality rule. Unset, it defaults to AUTH_AUDIENCE, and strict MCP clients refuse an /api resource. Same value on the AS, MCP and API functions: the MCP server relays each call to the API with the caller's token. See Self-hosting with SAML |
MCP_CLIENT_ID | Yes | — | — | External IdP: the MCP native app's client_id. SAML path: <mcp-client-id>, a key you choose that must also appear in AS_CLIENTS; dynamic registration answers 503 without it |
MCP_BASE_URL | Yes | — | — | https://<instance> — used in OAuth discovery responses |
TEMPER_API_BASE_URL | Yes | — | — | The relay's target for every tool act; pin to this deployment's own API URL in the MCP function's environment and never point it elsewhere — the relay presents the bearer plus TEMPER_MCP_SERVICE_SECRET to that origin |
TEMPER_MCP_SERVICE_SECRET | Yes | — | — | One generated secret on both api and mcp functions (openssl rand -base64 32); the API trusts the relay's attribution carrier only beside it, refuses to boot on overlap with any other shared secret or on a value under 16 characters. Unset on the API, every relayed act attributes @web — the MCP surface still works, the door is simply unarmed |
MCP_PROXY_SECRET | Yes (external-IdP path) | — | — | ≥ 32 chars (openssl rand -base64 48); encrypts the loopback redirect proxy's state token, derived from this secret alone. New — an upgraded deployment will not have it. Until it is set, a loopback sign-in (MCP CLI clients) answers 503 naming this variable; browser clients with an HTTPS callback are unaffected. Set it and redeploy — a Vercel function's environment is bound to its deployment. Rotating it fails sign-ins already in flight for up to 10 minutes; they succeed on retry. n/a in the SAML path — the Temper AS runs the whole flow and the relay answers 404 there |
| SAML Authorization Server (AS) block | | | | |
AS_ISSUER | Yes (SAML path) | — | — | Setting this flips the instance into AS mode |
AS_AUDIENCE | Yes (SAML path) | — | — | Must equal AUTH_AUDIENCE |
AS_SIGNING_KEY_PKCS8 | Yes (SAML path) | — | — | Ed25519 signing key, PKCS#8 PEM — secret |
AS_SIGNING_KID | Yes (SAML path) | — | — | Key id published in the JWKS |
AS_CLIENTS | Yes (SAML path) | — | — | JSON client_id → [redirect_uris] allowlist; unset = fail-closed. The whole registry — edit it, never replace it from an example. Remote callbacks match exactly |
AS_ACCESS_TTL_SECONDS | Optional (default 900) | — | — | Access-token lifetime |
AS_REFRESH_TTL_SECONDS | Optional (default 2592000, 30d) | — | — | Refresh-token lifetime (slides on rotation) |
AS_REFRESH_CHAIN_MAX_SECONDS | Optional (default 7776000, 90d) | — | — | Absolute refresh-chain lifetime from the last full login — the real bound on IdP-removed reach |
AS_REFRESH_REPLAY_GRACE_SECONDS | Optional (default 10) | — | — | How soon after a rotation a spent refresh token may be presented again and be read as a client retry rather than a stolen chain. Later, the chain is ended. 0 = strictest; max 3600, and out-of-range or non-numeric values fall back to the default. Not emitted by temper admin saml provision; set it only to move the default |
AS_SAML_ASSERTION_MAX_SECONDS | Optional (default 3600, 1h) | — | — | The widest assertion validity window your IdP issues. The retention sweep will not delete a consumed assertion id until this long after the row's expires_at, so it must not sit below your IdP's NotOnOrAfter. A present-but-unusable value fails the sweep rather than substituting; unset or blank uses the default. Read by temper-api, unlike the AS_* rows above. Not emitted by temper admin saml provision |
| Group provisioning / reconcile channel (SAML Phase 2) | | | | |
INTERNAL_RECONCILE_SECRET | Yes (SAML path; shared AS+API) | — | — | Same value on both; unset disables reconcile (auth still works) |
INTERNAL_RECONCILE_URL | Yes (SAML path; AS side) | — | — | Full URL of the API's /internal/saml/reconcile |
INTERNAL_RESOLVE_URL | Yes (SAML path; AS side) | — | — | Full URL of the API's /internal/principal/resolve; same secret. New — an upgraded deployment will not have it. Unset ⇒ refresh chains carry no owner, an admin revoke ends none of them, and every request still returns 200. See the upgrade note in Self-hosting with SAML |
| Slack account link (optional; needed only to run the @temper mention agent) | | | | |
SLACK_LINK_CLIENT_ID | Yes (Slack path) | — | — | OAuth client the link flow authorizes as. Auth0: a native/PKCE app's client_id. SAML path: a client_id present in AS_CLIENTS |
SLACK_LINK_SECRET | Yes (Slack path; shared API+agent) | — | — | Shared secret gating /internal/slack/link-state; same value on the mention agent. Unset ⇒ the endpoint is disabled (auth still works) |
PUBLIC_BASE_URL | Yes (Slack path) | — | — | https://<instance> — the origin the link redirect_uri is built from³. All four unset together is the supported "no Slack" state |
SLACK_VAULT_ENC_KEY | Yes (Slack path) | — | — | 32-byte base64 AEAD key (openssl rand -base64 32) encrypting each stored per-user refresh token. Malformed ⇒ the whole Slack flow disables. Rotation is flag-day (users re-link). Declare it in SLACK_VAULT_RETIRED_KEY_FINGERPRINTS, or every mint answers 500 |
SLACK_VAULT_RETIRED_KEY_FINGERPRINTS | Optional | — | — | Fingerprints of deliberately retired vault keys (16 hex characters each, comma-separated, plus optionally unstamped). Copy them from the boot log line slack vault key fingerprint: … before rotating. Not secret |
| Storage / build | | | | |
BLOB_READ_WRITE_TOKEN | Yes | — | — | Vercel Blob token for the binary-blob flow (temper blob …, /api/blobs, the MCP blob tools) and the upload/extract/embed pipeline. Unset, the blob endpoints answer a disabled refusal; BLOB_STORE_ID (OIDC) is the alternative credential. A deployment that does not want the flow at all sets BLOB_ENABLED=false — an explicit opt-out winning even with credentials present: the endpoints refuse naming the knob and the MCP tools are not advertised |
SQLX_OFFLINE | Yes (build) | — | — | Must be true |
| Optional / situational (api+mcp) | | | | |
ENABLE_SWAGGER | Optional | — | — | Exposes /swagger-ui in non-production |
PORT | Optional | — | — | Platform-injected by Vercel |
CORS_ORIGINS | Situational | — | — | Only for a separate cross-origin browser client — the bundled UI same-origin-proxies and does not need it |
| UI connectivity | | | | |
API_BASE_URL | — | Yes | — | The API's own origin, not the UI's public origin (loop-detection warning in the base playbook) |
APP_URL | — | Yes | — | The UI's own public origin |
| UI OIDC client | | | | |
OIDC_ISSUER | — | Yes¹ | — | Must resolve the same issuer as AUTH_ISSUER / AS_ISSUER |
OIDC_CLIENT_ID | — | Yes¹ | — | temper-ui in the SAML AS path |
OIDC_CLIENT_SECRET | — | Yes¹ (omit in the SAML AS path) | — | The AS registers temper-ui as a public PKCE client — no secret |
OIDC_AUDIENCE | — | Situational | — | Required for Auth0; omit for Okta custom AS / the SAML AS (carried implicitly) |
OIDC_PUBLIC_CLIENT | — | Yes (SAML AS path) | — | Declares the secret-less PKCE path; without it the UI fails fast at startup |
OIDC_DISCOVERY_URL² | — | Yes (SAML AS path) | — | Points the UI at the AS's RFC 8414 metadata — the AS has no /.well-known/openid-configuration |
| Session / storefront | | | | |
SESSION_SECRET | — | Yes | — | ≥32 bytes of entropy (64-char hex or 44-char base64) |
STOREFRONT_ENABLED | — | Optional | — | Set falsy to disable the public marketing route group on app-only installs |
| Eve (DEFERRED — not a step in this playbook) | | | | |
TEMPER_MCP_URL | — | — | Yes | The temper-mcp endpoint, e.g. https://<instance>/mcp |
TEMPER_API_URL | — | — | Yes | The temper REST base, e.g. https://<instance> |
TEMPER_M2M_CLIENT_ID | — | — | Production | The agent's machine-principal client id. Register it first — authentication is fail-closed, with no just-in-time create. On this (AS-mode) install, mint it with temper admin machine issue, which returns a tmpr_… id |
TEMPER_M2M_CLIENT_SECRET | — | — | Production | The one-time secret from issue. Never in code; rotate with rotate-secret |
TEMPER_M2M_TOKEN_URL | — | — | Production | The issuer's token endpoint. On this install that is your own instance's /oauth/token — Temper is the Authorization Server (AS_ISSUER set) |
TEMPER_M2M_AUDIENCE | — | — | External IdP only | Omit it here. Auth0 requires an audience; Temper's own AS ignores a request-supplied one entirely and mints with AS_AUDIENCE |
TEMPER_CONNECT_CONNECTOR | — | — | Fallback | Vercel Connect connector id. Used only when TEMPER_M2M_CLIENT_ID is unset, and it cannot mint an app token against an Auth0-fronted instance |
TEMPER_TOKEN | — | — | Dev only | Pre-obtained token; drives eve dev. Not for production |
STEWARD_MODEL | — | — | Optional | The agent's primary model (default minimax/minimax-m3). Resolved at build time, so a change needs a redeploy; an unknown id fails the build |
STEWARD_MODEL_FALLBACKS | — | — | Optional | Comma-separated, tried in order after the primary fails. Covers availability (5xx, rate limit), never quality |