Pulp Engine v0.13.0 — Scoped API Credential Model
Summary
Replaces the single shared API_KEY with three named credential scopes. Production integrations can now hold a render-only key; template management uses a separate admin key; preview routes can be restricted to a dedicated credential. One key can no longer unlock everything.
What changed
New env vars
| Variable | Scope | Route access |
|---|---|---|
API_KEY_ADMIN | admin | Full access — templates, assets, render, preview |
API_KEY_RENDER | render | POST /render, POST /render/html only |
API_KEY_PREVIEW | preview | POST /render/preview/* only |
All configured keys must be distinct strings. See the authorization matrix below.
Authorization matrix
| Route group | Paths | Required scope |
|---|---|---|
| Public | GET /health, GET /assets/:filename | (no auth) |
| Render | POST /render, POST /render/html | render or admin |
| Preview | POST /render/preview/html, POST /render/preview/pdf | preview or admin |
| Template management | All /templates/* routes | admin |
| Asset management | POST /assets/upload, GET /assets, DELETE /assets/:id | admin |
| All other authenticated routes | Any route not matched above | admin |
Error semantics
| Response | Meaning |
|---|---|
401 Unauthorized | Header missing, or key not recognised |
403 Forbidden | Key recognised but scope insufficient for this route |
Previously, an unrecognised key returned 403. It now returns 401 — the server does not confirm whether a key exists in the credential map.
Startup validation
The server now rejects at startup (via buildServer() rejecting) if:
API_KEYis set alongside any new scoped key (ambiguous config)- Any two new scoped keys share the same value (duplicate)
NODE_ENV=productionand no credentials of any kind are set
Migration from API_KEY
The legacy API_KEY is still accepted and treated as admin scope. A deprecation warning is logged at startup. It cannot coexist with the new keys.
Recommended migration:
# Before
API_KEY=my-old-secret
# After — minimal (single admin key)
API_KEY_ADMIN=my-new-admin-secret
# After — split (separate render integrations)
API_KEY_ADMIN=my-new-admin-secret
API_KEY_RENDER=my-new-render-secret
Steps:
- Generate new secrets.
- Set the new env vars and remove
API_KEY. - Update callers to use the appropriate key.
- Restart the API.
Breaking changes
None, if you migrate API_KEY to API_KEY_ADMIN.
- Setting
API_KEYalone still works (deprecated, warned at startup). - Setting
API_KEYalongside any new key fails at startup — this is the only new hard error. - The
403response for unrecognised keys is now401(minor semantics change).
Files changed
| File | Change |
|---|---|
apps/api/src/config.ts | Added API_KEY_ADMIN, API_KEY_RENDER, API_KEY_PREVIEW env fields |
apps/api/src/plugins/auth.plugin.ts | Full rewrite — credential map, requiredScopes() helper, startup validation |
apps/api/src/__tests__/auth-scopes.test.ts | New — comprehensive scope tests |
apps/api/src/__tests__/render-preview.test.ts | Updated env stubs from API_KEY to API_KEY_ADMIN |
.env.example | Deprecated API_KEY, added new vars with documentation |
README.md | Updated production security section |
docs/api-guide.md | Updated authentication section with scoped model, matrix, migration, editor note |
docs/deployment-guide.md | Updated env vars table, added § 8 Migration |
docs/runbook.md | Updated checklist and smoke test curl commands |
docs/release-v0.13.0.md | This file |
Notes
adminscope covers all routes including render and preview — an admin key can be used anywhere.- The visual editor (
VITE_API_KEY) still requires an admin-scoped key (current limitation — a narrower editor credential is planned). - Static asset serving (
GET /assets/:filename) remains public; the PDF renderer fetches image URLs without sending an API key.