For operators — someone who has already stood up the core Temper deployment
(API, MCP, database, IdP) and wants to add the optional browser front-end.Outcome#
By the end of this playbook you will have the temper-ui SvelteKit app deployed
as a second Vercel project, pointed at your running API instance, with OIDC
login working through your identity provider. The UI is fully config-driven: no
source edits, no fork.Prerequisites#
A running Temper deployment — see
self-host Temper. You need the API origin and the
IdP tenant from that deployment. An OIDC-capable identity provider — Auth0, Okta, or any provider that
publishes a standard OpenID Connect discovery document.
A second Vercel project — the UI deploys separately from the API, from
the same repository (root directory packages/temper-ui).
Two couplings, both env-driven#
Browser-facing API/MCP/OAuth traffic is reverse-proxied by the UI's
server (hooks.server.ts) to API_BASE_URL, rather than via a hardcoded
vercel.json rewrite. Requests to /api/*, /mcp, /oauth/*, and
/.well-known/* on the UI origin are forwarded server-side to your API host.
Because this is a same-origin proxy (the browser only ever talks to the UI
origin), the UI does not require CORS_ORIGINS on the API for its own
traffic.⚠️ API_BASE_URL must be the API backend's own origin, not the UI's
public origin. If the UI and API share a public domain (e.g. the UI
serves both temperkb.io and proxies temperkb.io/api), pointing
API_BASE_URL at that shared domain makes the proxy forward to itself —
an infinite loop the platform terminates with 508 Loop Detected. Set it
to the distinct origin where the API actually runs (its own *.vercel.app
URL, or a dedicated api. subdomain). The UI guards against this and
returns a clear 500 rather than looping, but the value still needs to be
correct for the proxy to work.
Login is generic OIDC Authorization Code + PKCE. Endpoints are resolved
from OIDC_ISSUER's discovery document
(/.well-known/openid-configuration), so any OIDC provider works. Logout
uses the standard RP-initiated end_session_endpoint.
Content-Security-Policy — nothing to configure, one thing to know#
The UI ships a restrictive content-security policy (set in svelte.config.js,
kit.csp), on every page it renders. Operators do not need to configure
anything: because the reverse proxy above keeps all browser API/MCP/OAuth
traffic same-origin, connect-src 'self' is correct for every deployment
shape — including one whose API lives on a different host. The fonts are
self-hosted (font-src 'self'), and no directive in the policy admits any
external origin: everything the page loads is same-origin.The one thing to know: if you ever modify the UI so the browser talks
directly to another origin — bypassing the reverse proxy, e.g. client-side
fetches to a public API host — you must widen connect-src in svelte.config.js
to match. The same goes for adding any other external asset origin.Register a confidential OIDC client#
In your identity provider, register a Regular Web Application (confidential
client) for the UI, distinct from the CLI/MCP native apps:Allowed callback / redirect URI: https://<ui-host>/auth/callback
Allowed logout / post-logout redirect URI: https://<ui-host>
Grant types: Authorization Code + Refresh Token (the UI requests the
offline_access scope)
Environment variable contract (UI project)#
| Variable | Required | Notes |
|---|
API_BASE_URL | Yes | The API backend's own origin (not the UI's public origin — see the loop warning above), e.g. https://<api-host> — used by server loaders and the browser-facing reverse proxy |
OIDC_ISSUER | Yes¹ | Issuer base URL, e.g. https://<tenant>.auth0.com or https://<org>.okta.com/oauth2/<asId>. Discovery resolved from <issuer>/.well-known/openid-configuration |
OIDC_CLIENT_ID | Yes¹ | The UI confidential web-app client_id |
OIDC_CLIENT_SECRET | Yes¹ | The UI confidential web-app client secret |
OIDC_AUDIENCE | Situational | Required for Auth0 (the API identifier); omit for Okta custom auth servers, which carry it implicitly |
APP_URL | Yes | The UI's own public origin, e.g. https://<ui-host> — used to build the redirect and post-logout URIs |
TEMPER_EDGE_PROXY_SECRET | Situational | Set when the API project's firewall rate-limits per IP. The UI's server sends it as x-temper-edge-proxy on every request it makes to the API (the reverse proxy and the page loaders), and the API project's rules exempt requests that carry it, because all of that traffic reaches the API from this function's few egress addresses. It confers no access, but a leaked value skips the API project's per-IP rules, so keep it out of anything a client sees. Generate with openssl rand -base64 32; the same value goes on the API project's MCP function and in its firewall rule. See Rate-limit an instance at the edge |
SESSION_SECRET | Yes | ≥32 bytes of entropy (64-char hex or 44-char base64) — derives the JWE session-cookie key |
¹ Back-compat fallback: if OIDC_* are unset, the UI falls back to the
canonical deployment's AUTH0_DOMAIN / AUTH0_CLIENT_ID /
AUTH0_CLIENT_SECRET / AUTH0_AUDIENCE (with OIDC_ISSUER derived as
https://<AUTH0_DOMAIN>). Self-hosters should set the OIDC_* variables
directly; the fallback exists so the hosted temperkb.io project keeps working
unchanged. A non-Auth0 provider is exercised end to end in
self-host with Okta.Further reading#
Modified at 2026-11-15 01:09:09