Pulp Engine Document Rendering
Get started
Release v0.53.0

Release v0.53.0

Date: 2026-04-04

Production-grade defaults, editor architecture decomposition, and canvas render performance.


Milestone A: Production-Grade Default

HARDEN_PRODUCTION tri-state config

HARDEN_PRODUCTION is now auto-derived from NODE_ENV=production when unset. Strict .refine() validation rejects invalid values like "banana". The resolved config uses Object.freeze() with HARDEN_PRODUCTION_EXPLICIT to distinguish auto-derived from explicit.

Named-user bootstrap

New scripts/generate-editor-users.mjs CLI generates EDITOR_USERS_JSON with crypto-strong keys. Shared-key identity advisory fires only when editor-login-capable (API_KEY_EDITOR, API_KEY_ADMIN, or legacy API_KEY) and no EDITOR_USERS_JSON.

Cluster-aware rate limiting

RATE_LIMIT_STORE=redis with REDIS_URL enables Redis-backed rate limiting. Dynamic ioredis import, startup fail-fast ping, RATE_LIMIT_FAIL_OPEN for degraded-mode semantics, graceful shutdown. rateLimitRedis added to GET /health/ready with state-transition logging.

Documentation sweep

8 docs files updated: deployment-guide, api-guide, evaluator-guide, release-checklist, runbook, README, .env examples, validate-deploy.sh.


Milestone B: Editor Decomposition

Store decomposition

  • ui.store.ts: selectionAnchorId, inlineEditingNodeId, richTextPanelMode
  • assets.store.ts: assets, assetsLoaded, setAssets
  • clipboard.store.ts: clipboard, copyNode, cutNode, pasteNode
  • editor.store.ts reduced from ~35 to ~23 fields/actions

Component decomposition

  • EditorShell split into EditorHeader (~280 lines) + DialogCluster (~80 lines)
  • EditorShell reduced from 631 to ~290 lines, 17 to 8 store subscriptions
  • PropertiesTab extracted with shared PropertiesTabId type

Preview selector facade

preview.selectors.ts with usePreviewDataJson, useUpdatePreviewDataJson, usePreviewDisplayMode, useSetPreviewDisplayMode. All 8 consumers migrated.


Milestone C: Performance & Resilience

Canvas render performance

  • useIsNodeSelected(id) granular selector: per-node boolean subscription prevents N re-renders on selection change
  • React.memo on NodeWrapper
  • Per-node find-replace selectors in NodeWrapper
  • OutlinePanel selection moved into SortableOutlineRow component
  • HeadingNodeView, TextNodeView, TableNodeView migrated to shared selector

API client resilience

  • fetchWithTimeout: 30s default, composable AbortSignal, already-aborted fast path, listener cleanup in finally
  • withRetry: exponential backoff (3 attempts, 1s base) for idempotent GETs
  • All 6 fetch() calls in api.ts replaced: 30s default, 60s renders, 120s uploads
  • fetchPreviewStatus result-based retry (3 attempts on service_unreachable or 5xx)
  • TimeoutError classified as transport failure in PublishGateDialog, PreviewPanel, DockedPreview

Save failure retry

Persistent retryable status with retrySave callback that bypasses publish gate. Tab-scoped via retryTabIdRef (clears on tab switch). Edit-invalidated (clears when template changes after failure).

API test coverage expansion

  • Asset upload: 7 cases (happy path, missing file, 413 size limit, SVG rejection, MIME mismatch, 401, 403)
  • Rate-limit enforcement: 4 cases (auth endpoint limit, render route limit, headers, retry-after)
  • CORS preflight: 3 cases (allowed origin, disallowed origin, wildcard mode)

Upgrade notes

Breaking: HARDEN_PRODUCTION default change

HARDEN_PRODUCTION now defaults to true when NODE_ENV=production. Existing production deployments that relied on hardening being off by default must add HARDEN_PRODUCTION=false to their environment. All compose files and .env examples have been updated with explicit HARDEN_PRODUCTION=false for evaluation use.

Compose file changes

compose.yaml, compose.postgres.yaml, and compose.container.yaml now include HARDEN_PRODUCTION: "false". Existing custom compose overrides are unaffected.


Validation

CI-verified

  • ci (lint, typecheck, editor build, check-version)
  • test-file-mode (API file-mode tests)
  • test-sqlserver (SQL Server storage tests)
  • test-e2e (Playwright editor workflows)
  • test-e2e-auth (Playwright auth flows)
  • docker-build-smoke (Docker image build + health check)

Locally verified

  • pnpm --filter @pulp-engine/editor typecheck — clean
  • pnpm --filter @pulp-engine/editor test — 794 passed
  • pnpm --filter @pulp-engine/editor build — succeeds
  • pnpm --filter @pulp-engine/api typecheck — clean
  • pnpm --filter @pulp-engine/api test:file — file-mode tests passing
  • pnpm --filter @pulp-engine/api test -- src/__tests__/asset-upload.test.ts — 7 passed
  • pnpm --filter @pulp-engine/api test -- src/__tests__/rate-limit-enforcement.test.ts — 4 passed
  • pnpm --filter @pulp-engine/api test -- src/__tests__/security-hardening.test.ts — passing
  • pnpm --filter @pulp-engine/api test -- src/__tests__/config-validation.test.ts — passing
  • pnpm lint — clean
  • node scripts/check-version.mjs — passed (on tagged commit)

Not verified

  • PostgreSQL storage integration (covered by CI test-file-mode job with Prisma, not exercised locally)
  • SQL Server storage (covered by CI test-sqlserver job, not exercised locally)
  • S3 asset binary store (no CI or local coverage; requires real S3 bucket)
  • Redis rate-limit store end-to-end (config validation tested, runtime requires real Redis)
  • Container and socket render modes (require Docker runtime)
  • Docker image production startup (covered by CI docker-build-smoke, not exercised locally)