Pulp EngineDocument Rendering
Get started

Plugin Trust Model

Audience: operators choosing whether to enable plugins, and plugin authors building against @pulp-engine/plugin-api.

Plugins are trusted server code

Pulp Engine plugins are loaded into the API process at startup via PULP_ENGINE_PLUGINS. They are not sandboxed. A plugin can:

  • Register storage backends, renderers, auth providers, routes, hooks, and events.
  • Read and write any data the API process can access (templates, assets, audit events, tenant records).
  • Make outbound network calls.
  • Read environment variables, the filesystem, and process memory.

Treat any enabled plugin as first-party server code — equivalent in trust to code you’d commit to this repository.

Operator posture

  • Source plugins from verified vendors. Pin to a specific package version (or a verified commit + checksum for file-path plugins). Avoid floating tags such as latest.
  • Audit before enabling. Read what the plugin registers and what permissions it implies. Pay attention to: storage backends (whole-database access), routes (new attack surface), auth providers (identity logic), and render hooks (run inside every render).
  • Default to disabled in production. Unless you authored or fully audited the plugin, leave PULP_ENGINE_PLUGINS unset. Production-ready features that started as plugins (e.g. @pulp-engine/barcode-renderer) graduate into the core build — there’s rarely a reason to ship a community plugin into a production deployment.
  • Multi-tenant deployments reject tenant-blind plugin storage. When MULTI_TENANT_ENABLED=true, registerStorageBackend() refuses to register plugin storage backends because the plugin-api 0.2.x storage contracts are tenant-blind and could cross tenant boundaries silently. Either disable multi-tenant mode or use a built-in storage backend (postgres, file, sqlserver). Tenant-aware plugin storage is planned for plugin-api 1.0 (Phase C.0b).
  • Reporting issues. Bugs in official packages under packages/ should be reported per SECURITY.md. Bugs in community or third-party plugins are the plugin maintainer’s responsibility — Pulp Engine cannot vet or backstop their security posture.

Why this is the right boundary

Plugins exist so operators can extend the core deliberately — adding a domain-specific renderer, an internal auth provider, a custom storage adapter — without forking the codebase. That extensibility is incompatible with strong sandboxing: the same hooks that let a barcode plugin register a renderer let a malicious plugin steal asset bytes or mint editor tokens.

The trust boundary is therefore drawn at PULP_ENGINE_PLUGINS: anything in that env var is in the trusted set; anything else is not. Operators control the env var.

If you want to evaluate a plugin without granting it production trust, run it in a non-production environment with synthetic data first.

Plugin author obligations

If you are publishing a plugin:

  • Document the runtime permissions it requires (network egress, filesystem paths, storage scope).
  • Declare @pulp-engine/plugin-api: ^0.2.0 in peerDependencies for the current contract line — Pulp Engine’s plugin-api contracts may change between minor versions during pre-1.0.
  • Surface failures via the structured logger; do not swallow errors silently.
  • Honour tenantId in any data-access surface (read and write) when plugin-api 1.0 lands tenant-aware contracts.

See also:

Public package distribution

The whole plugin-api package, including its loader and registry, is MIT licensed by owner decision on 11 October 2026. Its 0.2.x version line is independent of the server version checked by manifest.pulpEngineVersion. See plugin authoring for installation, testing and release compatibility. Publication does not change the trusted server-code boundary above.

Expression helpers and functions

plugin-api 0.3.0 adds registerHelper(name, fn, meta) and registerExpressionFunction(name, fn, meta). Names use ASCII letters followed by up to 63 letters, digits or underscores. Built-in Handlebars and JEXL names are reserved; duplicate plugin ownership is rejected with both plugin names in the error.

Helpers and functions execute synchronously in the API process during every render. They are trusted operator code, can block the event loop, and are tenant-blind. Never retain input data across calls or use mutable module state to cache tenant-specific results. Thenables and callable results are rejected; Handlebars SafeString results are unsupported. Helpers use ordinary sink escaping. Calls exceeding 50 ms log a rate-limited warning, which does not interrupt execution.

Non-expression registrations are staged until the activation callback succeeds; expression registrations and name claims are removed if activation throws. Expressions are removed when the server closes. Route registration errors raised later by Fastify still fail server startup. The pinned JEXL 2.3 runtime has no public removal method, so removal deletes entries from its tested grammar maps and clears compiled expressions.

The editor discovers plugin metadata and uses authenticated server evaluation. A browser implementation is not registered in this release. See examples/plugin-helpers for IBAN masking and fiscal-quarter examples.

SQL data source authors are trusted report authors. Any editor can read whatever the shared connection user can read; least-privilege read-only database credentials are mandatory. Statement guards and rollback do not limit readable tables, functions or external privileges. Providers receive optional tenant/actor/preview/signal context and getConnection(name), restricted to their declared connectionKinds. Plugins remain trusted server code. Never return credentials or raw driver diagnostics.

Delivery and e-sign providers (plugin-api 0.5.0)

registerDeliveryProvider and registerESignProvider are additive server hooks. They run before the always-available delivery registry is built. Built-in delivery names and the first-party DocuSign signing name cannot be replaced; duplicate provider registrations fail startup. Target custom delivery providers with {type:"plugin",provider:"acme-drop",connectionId,config}. JSON Schema configSchema validates the plugin target’s config; validateTarget, identity and connection test hooks are optional. identity contributes to dead-letter grouping. Tests can inspect both provider lists through plugin-testing.

Core resolves connection IDs under the current tenant and supplies only that connection’s decrypted {type,config,secrets} along with tenantId, bytes, MIME type and any document URL. Plugins receive no storage port through these hooks. This is a convenience boundary, not a process sandbox: plugins remain trusted code with host filesystem/network privileges. Multi-tenant activation warns that plugin caches and external resources must be isolated by tenant. A signing plugin can optionally verify raw callbacks and recover an uncertain creation through findEnvelope; recovery must only look up an existing provider envelope and must never send another one.