Pulp Engine Document Rendering
Get started
Release v0.23.0

Pulp Engine v0.23.0 — Release Notes

Private asset delivery and per-user named credentials

Two major capabilities land in this release:

  1. Private asset delivery (ASSET_ACCESS_MODE=private) — all asset binaries served through an authenticated API proxy; Puppeteer inlines assets server-side for PDF/HTML rendering. No public bucket required.
  2. Per-user named credentials (EDITOR_USERS_JSON) — each team member gets a personal login key, server-derived identity, and an individual role. createdBy attribution is now persistent on template versions and assets.

What changed

New ASSET_ACCESS_MODE configuration (default: public)

ASSET_ACCESS_MODE controls how uploaded asset binaries are delivered. The default public is the existing behaviour — no change for existing deployments.

ModeFilesystemS3
public (default)@fastify/static serves /assets/* without authPublic S3 URL stored in AssetRecord.url; bucket must be publicly readable
privateGET /assets/:filename requires admin or editor credentials; API streams from diskAPI proxies from S3 using its credentials; bucket does NOT need public-read; S3_PUBLIC_URL not required

Private mode render pipeline:

Instead of Puppeteer fetching /assets/… URLs at render time, the API inlines all referenced assets as base64 data URIs in the HTML before passing it to Puppeteer. No unauthenticated network calls from the renderer.

Private mode editor:

The visual editor fetches /assets/:filename with X-Editor-Token and displays images via a revocable blob URL. No <img src> pointing directly at /assets/ — auth is applied at fetch time.

S3 in private mode: add s3:GetObject to the API credentials in addition to PutObject / DeleteObject. No S3_PUBLIC_URL required.


New EDITOR_USERS_JSON named-user credential registry

Set EDITOR_USERS_JSON to a JSON array of user objects to enable per-user named credentials:

[
  { "id": "alice", "displayName": "Alice Smith", "key": "alice-unique-secret", "role": "editor" },
  { "id": "bob",   "displayName": "Bob Jones",   "key": "bob-unique-secret",   "role": "admin"  },
  { "id": "carol", "displayName": "Carol Wu",    "key": "carol-secret",        "role": "editor",
    "tokenIssuedAfter": "2026-03-25T12:00:00Z" }
]
FieldRequiredDescription
idyesURL-safe unique identifier — used as the actor value in tokens and audit records
displayNameyesHuman-readable name shown in the editor header
keyyesPersonal login credential — must be unique; must not duplicate any API_KEY_* value
roleyes"editor" or "admin" — maps directly to credential scope
tokenIssuedAfternoISO-8601 UTC datetime — tokens with iat before this are rejected for this user only (targeted session revocation without global key rotation)

When EDITOR_USERS_JSON is configured:

  • POST /auth/editor-token accepts only personal user keys — shared API keys (API_KEY_EDITOR, API_KEY_ADMIN) are rejected with HTTP 401.
  • The actor field in the request body is silently ignored; actor identity is always server-derived from the registry.
  • X-Api-Key machine access (CI/CD, render pipelines) is unaffected.
  • Startup fails immediately if id values are duplicated, key values are duplicated, or any user key collides with an active API_KEY_* value.

Absent = shared-key mode (existing behaviour unchanged).


Persistent createdBy attribution

Template version history and asset records now carry a createdBy: string | null field:

  • In named-user mode: createdBy is the user’s id — server-derived and cryptographically bound.
  • In shared-key mode: createdBy is the operator-supplied actor label (or null if none was provided).
  • Existing records created before v0.23.0 have createdBy: null.

Editor identity pill

The visual editor header now shows the authenticated user’s displayName (named-user mode) or actor label (shared-key mode) as an identity pill when a session is active.


API contract changes (additive)

These fields are new. No existing fields were renamed or removed.

EndpointNew fieldTypeNotes
GET /auth/statusidentityMode"shared-key" | "named-users"Always present
POST /auth/editor-tokendisplayNamestring | nullnull in shared-key mode
GET /templates/:key/versionscreatedBy (per version)string | nullnull for pre-v0.23.0 records
GET /assetscreatedBy (per asset)string | nullnull for pre-v0.23.0 records

New environment variables

VariableDefaultNotes
ASSET_ACCESS_MODEpublicpublic or private
EDITOR_USERS_JSON(absent)JSON array; see deployment guide § Named-User Mode

Database changes

Postgres: one new migration (add_created_by) adds a nullable created_by column to template_versions and assets. Run pnpm --filter @pulp-engine/api db:deploy as part of the upgrade.

SQL Server: one new migration (002_add_created_by.sql) adds the same nullable created_by columns. Run pnpm --filter @pulp-engine/api db:migrate:sqlserver before starting v0.23.0. The runner applies the migration automatically on both fresh installs and upgrades.

File mode: no migration needed.


Backward compatibility

  • No API routes removed or renamed.
  • ASSET_ACCESS_MODE=public is the default — no config change required for existing deployments.
  • EDITOR_USERS_JSON absent → shared-key mode — identical to previous behaviour.
  • createdBy is nullable — existing records report null.
  • Postgres migration is a nullable column addition — safe to apply on a live database with no downtime.

Upgrading

  1. Pull v0.23.0.
  2. Apply database schema changes:
    • Postgres: pnpm --filter @pulp-engine/api db:deploy
    • SQL Server: pnpm --filter @pulp-engine/api db:migrate:sqlserver
    • File mode: no action needed
  3. Restart the API.

Optional configuration changes:

  • Add ASSET_ACCESS_MODE=private for authenticated asset delivery.
  • Add EDITOR_USERS_JSON to enable per-user named credentials.

See docs/deployment-guide.md § Named-User Mode and § Asset Access Mode for full configuration reference.


Known limitations and caveats

  • Named-user cutover: switching from shared-key to named-user mode causes all existing editor sessions to become invalid (shared API keys are rejected). Set EDITOR_TOKEN_ISSUED_AFTER to the cutover timestamp to cleanly expire outstanding sessions. Plan for all editor users to re-login with their personal keys.
  • Asset inlining scope: only /assets/filename relative paths are inlined. Absolute URLs (e.g. public S3 URLs from public mode) are left unchanged.