Authentication
How the two surfaces authenticate, and what the live deployment enforces today.
This page describes the auth model. For what this particular deployment
enforces right now, read /metadata/authentication
— it reports live process configuration rather than intent.
The short version
| Surface | Credential | Enforced |
|---|---|---|
/api/public/* |
None | Never — open by design |
/api/v1/* (runtime routes) |
JWT, or X-API-Key |
Only when REQUIRE_AUTH=true |
/api/v1/* (management routes) |
JWT | Only when REQUIRE_AUTH=true |
Portal pages, /openapi.yaml, /metadata*, /health, /version |
None | Never — public documentation |
/api/public is open on purpose
The public surface has no credential and never will. Two things depend on that:
- Community runtimes uplink without an account. Requiring a key would make the Control Plane a precondition for using the CLI, which it must never be.
- The demo UI (
demo.engineeringruntime.com) is a static site with no server of its own to hold a secret.
Treat everything on /api/public as world-readable and world-writable. It
carries demo-grade data for a single default organisation. Do not put anything
there you would not publish.
/api/v1 and the REQUIRE_AUTH switch
/api/v1 is the enterprise surface, and its enforcement is a deployment
decision:
REQUIRE_AUTH=false— routes resolve to the default organisation and answer without credentials. This is how the live demo runs today so the enterprise UI can be shown without handing out logins. A suppliedX-API-Keyis still validated and still scopes the request to that key's organisation; it is accepted, not demanded.REQUIRE_AUTH=true— every/api/v1route requires a valid JWT, and runtime contract routes additionally acceptX-API-Key. Anything else gets401.
The current public deployment runs with
REQUIRE_AUTH=false. That is a demo posture, not a recommendation: a deployment holding real audit data should setREQUIRE_AUTH=true, changeJWT_SECRET, and issue keys per runtime. The switch is checked at request time by the middleware ininternal/api/middleware.go— there is no second code path to keep in sync.
Try without a login
To feel the API and see real output, use /api/public (this portal’s
Getting started and Examples) or the demo UI at demo.engineeringruntime.com.
No password and no API key. Enterprise login and keys are not required for that
path.
JWT — people and UIs
JWT is for people and the enterprise UI (app.engineeringruntime.com).
Credentials are issued on request — they are not published on this portal.
After you have access, log in once, then send the token:
TOKEN=$(curl -s -X POST https://api.engineeringruntime.com/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"YOUR_EMAIL","password":"YOUR_PASSWORD"}' \
| python3 -c 'import sys,json;print(json.load(sys.stdin)["token"])')
curl -s https://api.engineeringruntime.com/api/v1/auth/me \
-H "Authorization: Bearer $TOKEN"
Tokens carry the user id, email, role, and organisation id, and expire after
JWT_EXPIRY_HOURS (24 by default). GET /api/v1/auth/me accepts an optional
bearer token and answers 401 when none is present, which makes it a cheap way
to test whether a token is still good.
Roles are admin, operator, and viewer. They are carried in the token
today; per-role route restrictions are not yet enforced.
API keys — runtimes
Machine callers use a key instead of a login (X-API-Key: erk_…). Keys are
created in the enterprise Control Plane after access is issued — this portal
does not publish a sample key.
With an issued JWT, create a key through the management API (shown once):
curl -s -X POST https://api.engineeringruntime.com/api/v1/api-keys \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"name":"ci-runner","scopes":"runtime"}'
{
"id": "…",
"name": "ci-runner",
"prefix": "erk_a1b2",
"api_key": "erk_…",
"warning": "store this api_key securely — it will not be shown again"
}
Send it on runtime contract routes:
curl -s 'https://api.engineeringruntime.com/api/v1/policies?active=true' \
-H 'X-API-Key: erk_…' \
-H 'X-Runtime-Client: engineering-runtime'
Only the hash is stored, so a lost key is replaced rather than recovered.
DELETE /api/v1/api-keys/{id} revokes one immediately; the next request using
it fails. Each accepted key updates its last_used_at, which is how you find
keys nobody is using any more.
To try the same runtime calls without a key, use /api/public instead.
Headers at a glance
| Header | Sent by | Effect |
|---|---|---|
Authorization: Bearer <jwt> |
UIs, people | Authenticates and selects the organisation from the token |
X-API-Key: erk_… |
Runtimes | Authenticates and selects the key's organisation |
X-Runtime-Client: engineering-runtime |
Runtimes | Identifies the caller; also selects the active policy on GET /policies |
Content-Type: application/json |
Anyone POSTing | Required for every request with a body |
Multi-tenancy
Every stored record carries an organization_id, and queries filter on it.
Which organisation a request resolves to depends on how it authenticated: from
the JWT, from the API key, or — when auth is not required — the default seeded
organisation. Hardening org isolation into a mandatory middleware on every
query is planned work, not a claim about today.
CORS
Browser callers are allowed from the configured CORS_ORIGINS (the demo and
app hostnames plus local development ports) and from any *.pages.dev preview
host. Server-to-server callers are unaffected by CORS.