Skip to main content

ADR-0018: Application-Layer Payload Encryption Modes

  • Status: Implemented
  • Date: 2026-08-17
  • Deciders: @revant.one
  • Supersedes: N/A
  • Superseded by: N/A

Context

Framework M handles sensitive data such as passwords, email addresses, phone numbers, and PII. By default, HTTPS protects data in transit from passive eavesdropping, but it does not prevent reverse proxies, WAFs, APM loggers, ingress controllers, or compromised infrastructure from inspecting cleartext request bodies. To mitigate this, Framework M needs application-layer payload encryption: the client encrypts the request body with the server's public key, and the server decrypts it inside the application before handling the request.

A one-size-fits-all encryption policy is impractical. Local development and CI need cleartext JSON for ease of debugging, while fintech or zero-trust deployments need aggressive encryption. Therefore, the framework must support multiple configurable modes.

Decision

We will implement a four-tier payload encryption model controlled by a single mode setting:

  1. disabled — No encryption. Standard JSON bodies are used.
  2. selective — Encrypts a configurable list of routes. Built-in defaults cover all authentication routes (/api/v1/auth/*).
  3. metadata — Encrypts DocType fields flagged with json_schema_extra={"encrypted": True} or {"pii": True}. Desk forms use DocType schema metadata to decide whether to encrypt a submission.
  4. strict — Encrypts all mutating (POST, PUT, PATCH) requests globally.

Key architectural choices:

  • Single JWKS endpoint: GET /api/v1/security/jwks serves all public keys, distinguished by use (enc vs sig) and kid.
  • Lazy public key caching: The frontend fetches /api/v1/security/config on boot and caches the encryption key from JWKS, avoiding per-request handshakes.
  • Priority 15 middleware: PayloadDecryptionMiddleware is registered at priority 15, between rate limiting (10) and CSRF/idempotency (20-30), so encrypted authentication payloads are decrypted before AuthMiddleware (50).
  • Pydantic json_schema_extra for DocTypes: Field-level flags are declared with standard Pydantic Field(json_schema_extra={...}) and exposed via the metadata router (GET /meta/{doctype}).

Consequences

Positive

  • Defense in depth: Sensitive payloads remain opaque to infrastructure between TLS termination and the application.
  • Configurable compliance: Developers can choose the mode that matches their audit requirements without rewriting code.
  • Zero frontend boilerplate: Auth provider and Desk form controller automatically encrypt when the active mode requires it.
  • Backward compatible: disabled mode is the default; existing clients and tests continue to work unchanged.

Negative

  • OpenAPI constraint: Unknown json_schema_extra keys break Litestar's OpenAPI generator. Field flags therefore cannot be used on Litestar request/response DTOs; they are limited to DocType models consumed through the metadata router. Auth routes rely on selective route configuration instead.
  • Key rotation complexity: Rotating the encryption key requires both backend private-key updates and frontend cache invalidation.
  • Slightly larger payloads: JWE envelopes add overhead compared to raw JSON.

Implementation Notes

  • Backend decryption: libs/framework-m-standard/src/framework_m_standard/adapters/web/middleware/payload_decryption.py
  • Crypto adapter: libs/framework-m-standard/src/framework_m_standard/adapters/crypto/jwe_crypto.py
  • Security endpoints: libs/framework-m-standard/src/framework_m_standard/adapters/web/security_routes.py
  • Frontend crypto: libs/framework-m-desk/src/crypto.ts
  • Desk form wiring: libs/framework-m-desk/src/data.ts, libs/framework-m-desk/src/hooks/useFormController.ts