Engineering Runtime API

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:

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:

The current public deployment runs with REQUIRE_AUTH=false. That is a demo posture, not a recommendation: a deployment holding real audit data should set REQUIRE_AUTH=true, change JWT_SECRET, and issue keys per runtime. The switch is checked at request time by the middleware in internal/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.