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:
disabled— No encryption. Standard JSON bodies are used.selective— Encrypts a configurable list of routes. Built-in defaults cover all authentication routes (/api/v1/auth/*).metadata— Encrypts DocType fields flagged withjson_schema_extra={"encrypted": True}or{"pii": True}. Desk forms use DocType schema metadata to decide whether to encrypt a submission.strict— Encrypts all mutating (POST,PUT,PATCH) requests globally.
Key architectural choices:
- Single JWKS endpoint:
GET /api/v1/security/jwksserves all public keys, distinguished byuse(encvssig) andkid. - Lazy public key caching: The frontend fetches
/api/v1/security/configon boot and caches the encryption key from JWKS, avoiding per-request handshakes. - Priority 15 middleware:
PayloadDecryptionMiddlewareis registered at priority 15, between rate limiting (10) and CSRF/idempotency (20-30), so encrypted authentication payloads are decrypted beforeAuthMiddleware(50). - Pydantic
json_schema_extrafor DocTypes: Field-level flags are declared with standard PydanticField(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:
disabledmode is the default; existing clients and tests continue to work unchanged.
Negative
- OpenAPI constraint: Unknown
json_schema_extrakeys 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