| Outcome | Produced by |
|---|---|
| Deployed API + MCP behind Okta-SAML SSO | self-hosting.md deploy + self-hosting-saml.md |
| A first system admin | the SQL root step (irreducible) |
| Instance settings (name, gating, mode) | temper admin settings |
| An everyone-team every member auto-joins | temper team create … --auto-join-role watcher |
| An org-identity telos-charter cognitive map, born + bound | temper cogmap create → temper cogmap reconcile → temper cogmap bind |
| (optional) The web UI | self-hosting.md#deploy-the-ui-optional |
| (deferred) The Eve steward | vercel-eve.md |
temper binary — a prerequisite for every phase below; seeembed-capable temper binary. Org bootstrap's cogmap create / cogmap reconcilecargo install --path crates/temper-cli --locked --force (seepsql and DATABASE_URL_UNPOOLED for the DB-only steps — running migrations and thepackages/temper-ui/.env.example,| Variable | temper-cloud (api+mcp) | temper-ui | eve (deferred) | Notes |
|---|---|---|---|---|
| Database | ||||
DATABASE_URL | Yes (pooled, runtime) | Yes (same pooled string; read-only nav chrome) | — | One Neon connection string shared by api/mcp/ui |
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 | — | — | Must equal AS_AUDIENCE / MCP_AUDIENCE / UI OIDC_AUDIENCE |
AUTH_PROVIDER_NAME | Yes | — | — | auth0, or saml:<idp-key> in the SAML path (max 32 chars) |
MCP_AUDIENCE | No | — | — | Optional. An instance has ONE audience; both surfaces read AUTH_AUDIENCE. If set, must equal it — enforced at boot, not by discipline. |
MCP_CLIENT_ID | Yes | — | — | Auth0 MCP native app client_id; n/a in the SAML path (client allowlisting is AS_CLIENTS instead) |
MCP_BASE_URL | Yes | — | — | https://<instance> — used in OAuth discovery responses |
| 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 |
AS_ACCESS_TTL_SECONDS | Optional (default 900) | — | — | Access-token lifetime |
AS_REFRESH_TTL_SECONDS | Optional (default 2592000, 30d) | — | — | Refresh-token lifetime |
| 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 |
| 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 today (users re-link). See slack-setup.md |
| Storage / build | ||||
BLOB_READ_WRITE_TOKEN | Yes | — | — | Vercel Blob token for the upload/extract/embed pipeline |
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 self-hosting.md) |
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 runbook) | ||||
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. See machine-credentials.md |
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 |
OIDC_* are unset, the UI falls back to the canonical deployment'sAUTH0_* variables — see self-hosting.md.OIDC_* directly.OIDC_DISCOVERY_URL is not part of this document's source A2 variable enumeration but isself-hosting-saml.md § 6, .env.example) — added hereredirect_uri must be registered, or the IdP refuses the authorize request<PUBLIC_BASE_URL>/api/auth/slack/callback.SLACK_LINK_CLIENT_ID's entry in AS_CLIENTSAS_CLIENTS is fail-closed. On an Auth0-fronted install, add it to the application'shttp://The audience and issuer joins are enforced at boot. Temper parses them once and refuses to
start if they disagree, naming the offending variable and the relation it must satisfy. You do
not have to hold this table in your head — but the values below are what it checks, andJWKS_URLmust be$AS_ISSUER/oauth/jwkson an AS instance.
| Join | Values that must be equal |
|---|---|
| Audience | AS_AUDIENCE = AUTH_AUDIENCE = MCP_AUDIENCE (if set) = UI OIDC_AUDIENCE |
| Issuer | AS_ISSUER = AUTH_ISSUER; UI OIDC_ISSUER resolves the same issuer |
| Provider label | AUTH_PROVIDER_NAME = saml:<idp-key> |
| Reconcile secret | INTERNAL_RECONCILE_SECRET identical on the AS and API env (same Vercel project) |
| Database | DATABASE_URL (pooled) shared api/mcp/ui; DATABASE_URL_UNPOOLED migrations only |
temper admin saml provision renders the AS_* + reconcile block so these are consistent bytemper admin saml provision is an inert emitter — it never touches a running instance. ItINTERNAL_RECONCILE_SECRET that must already be in the env when the backend deploys. Emittingprovision produces two artifacts that land at different--env-out) is consumed pre-deploy, at step 4 (Vercelkb_saml_idp INSERT (--sql-out) can only be applied post-migrate, at step 6 —kb_saml_idp is a table created by the migrations run at step 5, so applying it any earlier is| # | Step | Owner | Detail link |
|---|---|---|---|
| 1 | Provision Neon (PG17, vector + pg_uuidv7, pooled/unpooled) | manual | self-hosting.md § Provision Neon |
| 2 | Register Okta SAML app; capture cert / SSO URL / entity ids / group attribute statement | manual | self-hosting-saml.md + Okta SAML app note below |
| 3 | temper admin saml provision → generate keys, --env-out bundle, --sql-out kb_saml_idp SQL (inert; early for the env keys) | saml-setup.sh (emit) | self-hosting-saml.md |
| 4 | Set Vercel env (matrix + emitted bundle) on api + mcp | manual | Environment matrix |
| 5 | Deploy backend; sqlx migrate run against DATABASE_URL_UNPOOLED | manual | self-hosting.md § Run migrations |
| 6 | Apply the kb_saml_idp row (saml-setup.sh --apply-db, or psql the --sql-out file by hand) | saml-setup.sh (--apply-db) | self-hosting-saml.md |
| 7 | First admin signs in via SAML → JIT kb_profiles row | manual | self-hosting-saml.md |
| 8 | SQL root step: gating team + first admin; VERIFY is_system_admin(<uuid>) = true | system-bootstrap.sh --run-root | org-bootstrap.md § 0 |
| 9 | temper admin settings (instance name, gating team, mode) | system-bootstrap.sh | org-bootstrap.md § 1 |
| 10 | temper team create everyone --auto-join-role watcher | system-bootstrap.sh | org-bootstrap.md § 2 |
| 11 | temper admin saml map-group (after teams exist) | saml-setup.sh (emit / --apply-db) | self-hosting-saml.md |
| 12 | temper admin saml verify | saml-setup.sh | self-hosting-saml.md |
| 13 | Telos-charter: temper cogmap create → temper cogmap reconcile → bind +everyone | system-bootstrap.sh | org-bootstrap.md §§ 3–5 |
| 14 | (optional) UI deploy: confidential OIDC client, API_BASE_URL, SESSION_SECRET | manual | self-hosting.md § Deploy the UI (optional) |
| 15 | Verify: health, temper login, resource round-trip | manual | self-hosting.md § Verify |
| — | → team-self-cognition + Eve steward: DEFERRED | — | vercel-eve.md |
scripts/bootstrap/saml-setup.sh --profile schema-artifact/saml-profile.yaml (step 3, defaultkb_saml_idp SQL for step 6), dosystem-bootstrap.sh --run-root (steps 8–10 and 13) and re-runsaml-setup.sh --profile schema-artifact/saml-profile.yaml --apply-db (steps 6, 11, and 12 —kb_saml_idp row, maps the now-existing teams' IdP groups, and verifies against thesystem-bootstrap.sh (steps 8–10, 13) works unchanged for Auth0/Okta-OAuth installs, which swapIn Okta, create a SAML 2.0 app and capture four values off it: the SSO URL → idp_sso_url/--idp-sso-urlthe signing certificate (PEM) → idp_cert_file/--idp-cert-filethe IdP entity id → idp_entity_id/--idp-entity-ida group attribute statement exposing the user's groups → groups_attr/--groups-attr
(e.g.groups)This note covers only what to pull out of Okta's app screen. The generic SAML-IdP side — the SP
ACS URL and entity id Temper's AS expects the IdP to send assertions to — is documented in
self-hosting-saml.md, and is the same regardless of which IdP you use.
is_system_adminreads gating-team ownership, notsystem_access. It is true only when
the profile is anownermember of the team whose slug equalskb_system_settings.gating_team_slug—kb_profiles.system_access = 'admin'does nothing for
it, andgating_team_slugisNULLby canonical-seed default, which denies everyone
(self-hosting-saml.md — a missinggating_team_slug"fails
silently with 403s"; l0-content-delivery.md § The gotcha
spells out both halves; confirmed in the field 2026-07-02). Set both halves at step 8 — the
gating team and the owner membership — and verify withis_system_admin(<uuid>) = truebefore
moving on, not just that the SQL ran.API_BASE_URLpointed at the UI's own public origin creates a self-proxy loop →508 Loop Detected. It must be the API backend's own distinct origin — its*.vercel.appURL or a
dedicatedapi.subdomain — never the shared public domain the UI also serves
(self-hosting.md).AS_CLIENTSunset rejects every/oauth/authorizecall (fail-closed);INTERNAL_RECONCILE_SECRET
unset silently disables group provisioning while auth still works. The first fails loud, the
second doesn't — nothing errors, groups just never sync, so verify reconcile explicitly rather
than trusting a clean login (AS deployment,
group provisioning).cogmap create/cogmap reconcilerequire anembed-featuretemperbinary. A
non-embedbuild fails with a clearrequires the 'embed' featureerror rather than a cryptic
one, but only at step 13, well after the rest of the install has succeeded — check this
up front instead (org-bootstrap.md).Migrations are a deploy step, not a startup step — the API never auto-migrates. Run sqlx migrate runagainstDATABASE_URL_UNPOOLED(step 5) yourself, and back up the database
first — there is no automatic rollback if a migration fails partway
(self-hosting.md, DEPLOYING.md).
| Steps | Automated by | Status |
|---|---|---|
| 8–10, 13 | system-bootstrap.sh --run-root | Exists today |
| 3, 6, 11, 12 | saml-setup.sh --profile schema-artifact/saml-profile.yaml (--apply-db for 6, 11, 12) | Exists today |
| 1–2, 4–5, 7, 14–15 | — (manual) | Platform-console and human-in-the-loop steps: provisioning Neon and the Okta app, setting Vercel env, deploying, the first SAML login, and the optional UI deploy/verify — none of these are things a script can safely do on an operator's behalf |
app principal needs client_credentials (M2M) supportplan/diff applier semantics. system-bootstrap.sh has no state backend — re-applying ais_system_admin gate covers the L0 kernel only, not team-scoped cogmaps.