The Heimdall mental model.
Before any code: what an app is, who an end-user is, why there are two API surfaces, and which one your code should hit. Five minutes of reading saves an hour of confusion.
1
What is a Heimdall app?
An app in Heimdall is the identity boundary for one of your products. If you run a SaaS called Acme, you create one Heimdall app called acme and every user of Acme — the people who sign up for Acme, sign in to Acme, hold roles inside Acme — lives inside that app.
Apps are owned by a workspace (your team). One workspace can own many apps — useful if you run several products, or if you want a separate acme-staging for non-prod environments. Apps are cryptographically isolated: each app has its own JWKS (JSON Web Key Set) for signing tokens, so a token signed for acme cannot authenticate against acme-staging.
What apps don't do: they don't talk to each other. Two apps in the same workspace share no identity, no roles, no permissions. If you want unified login across two of your products, that's one app with two clients — not two apps.
2
The two API surfaces
Heimdall serves two different HTTP surfaces. They look superficially similar (both speak JSON, both use Bearer auth) but they exist for different actors:
For your customer-product
Consumer API
Path: /:appSlug/v1/* on api.heimdall.productcraft.co.
This is what your customers' end-users authenticate against. Your frontend (or your customer's frontend) hits /auth/signup, /auth/signin, /me, etc. Tokens are signed with the app's own JWKS.
Auth lanes here: per-app EndUser JWT (the user's session), per-app M2M JWT (the customer's own backend), or PAK on the mint endpoints (workspace-level authority for sensitive flows).
For your workspace owners
Heimdall-admin
Path: /v1/apps/* on api.heimdall.productcraft.co.
This is what your ops people (or your CI) hit to configure Heimdall. Create apps, manage app membership, mint M2M credentials, set auth config, read audit logs.
Auth lanes: platform cookie (signed-in workspace user) on every route, plus PAK on the RBAC routes (role + permission CRUD) for IaC use cases.
Which one do you hit? If you're building the product itself (your customer signs up here, signs in here), it's the Consumer API. If you're building ops tools (CI provisions a new app for staging; a Terraform module manages your role catalogue), it's Heimdall-admin.
A common mistake: trying to manage Heimdall apps from the Consumer API (it doesn't expose app CRUD) or trying to sign in an end-user via Heimdall-admin (it doesn't verify end-user tokens). Pick the surface that matches the actor.
3
Identity primitives
Three identity primitives live in Heimdall:
- EndUser — a person who signed up for one of your apps. Lives in
accountwithapp_idset. Tokens carry atype: 'end_user'claim. EndUsers hit the Consumer API. - M2M client — a service principal for backend-to-backend traffic. Lives in
m2m_client. Authenticates via OAuth 2.0 client_credentials grant. Tokens carry atype: 'm2m'claim + ascopes[]array. - PlatformUser — you, the workspace owner. Lives in
platform_auth.account(different database). Signs in viaauth.productcraft.co. Hits Heimdall-admin with a cookie.
Plus one credential type that's not a principal:
- PAK (Platform API Key) —
pcft_live_*. A workspace-scoped credential minted by a PlatformUser. Used for Heimdall-admin from CI and for the Consumer API mint endpoints (request-verification, request-password-reset).
4
Per-app JWKS
Every app has its own signing keys. Your service verifies tokens against the app's JWKS, not a global one.
When you create an app, Heimdall provisions a fresh RSA keypair for it. The public half is published at the app's JWKS endpoint; the private half stays inside heimdall-api, encrypted at rest. Tokens for that app (EndUser sessions, M2M access tokens) are signed with the private key. Anyone wanting to verify a token fetches the JWKS and checks the signature.
GET https://api.heimdall.productcraft.co/<your-app-slug>/v1/.well-known/jwks.json
GET https://api.heimdall.productcraft.co/<your-app-slug>/v1/.well-known/openid-configurationjose on Node, PyJWT on Python,github.com/lestrrat-go/jwx on Go — every mainstream JWT library knows how to consume a JWKS endpoint. Cache the response for an hour (the endpoint serves aCache-Control: public, max-age=3600) and refresh on kid miss to handle key rotation.
Keys rotate automatically every 90 days. Your service doesn't do anything special — when a new key gets published, your JWKS cache misses on the new kid, you refresh, you keep going. The old key stays valid for a 7-day overlap window so in-flight tokens don't break mid-rotation.
5
What's NOT in Heimdall
Things customers ask about that live elsewhere:
- Workspace identity — your team's accounts, members, roles inside the workspace. That's the Platform API.
- Transactional email — the actual SMTP delivery for a verification or reset email. Heimdall can fire-and-forget an email through Envoi for you, or you can take the code from the response body and route it through your own SMTP.
- Social features — posts, follows, feeds, moderation. That's Agora, a different service.