Transport Adapter Guide
This guide covers the runtime side of adding a new API transport to Reventless (e.g., OpenAPI/REST, gRPC). It is the counterpart to api-protocol-integration.md, which covers the deploy-time schema generation layer.
A transport adapter is the in-memory (or Lambda handler) code that receives an incoming request, bridges it to the framework's internal types, calls the relevant interceptors, and dispatches the command or query.
Architecture: Two Layers
| Layer | Guide | Purpose |
|---|---|---|
| Schema generation (deploy-time) | api-protocol-integration.md | Generates protocol-specific schema (SDL, OpenAPI spec, MCP manifest) from entry types |
| Resolver / handler (runtime) | this guide | Receives requests, extracts identity, calls interceptors, dispatches commands and queries |
These layers are independent. A new transport needs both, but they can be developed separately.
Identity Propagation
Every command and query carries an Identity.t value. The transport adapter is responsible for extracting it from the incoming request and passing it into the framework.
Type
// Reventless.Identity.t — parsed from JSON
type t = {
userId: string,
// ... additional fields from your identity provider
}
let anonymous: t // fallback when no identity is present
let schema: S.t<t> // sury schema for parsing
Convention
Transport adapters use a dedicated header (or equivalent transport-level mechanism) to carry the identity. The in-memory GraphQL transport reads X-Identity — a JSON-encoded Identity.t value set by the caller (e.g., an auth middleware layer).
Extraction pattern
// Example: HTTP transport
let extractIdentity = (request: myTransportRequest): Reventless.Identity.t => {
try {
switch request->getHeader("x-identity") {
| Some(json) => json->JSON.parseOrThrow->S.parseOrThrow(Reventless.Identity.schema)
| None => Reventless.Identity.anonymous
}
} catch {
| _ => Reventless.Identity.anonymous
}
}
Always fall back to Reventless.Identity.anonymous on any parse error — never throw from identity extraction.
Command Interceptor
Hook location
// reventless-core/src/components/CommandGenerator/CommandGenerator_Callback.res
type interceptResult = Allow | Deny(string)
type commandComponentKind = Aggregate | StateChangeSlice
type commandInterceptor = (
~identity: Reventless.Identity.t,
~componentName: string, // aggregate / slice name
~componentKind: commandComponentKind,
~tag: string, // command variant name (e.g. "AddProduct")
~args: JSON.t, // raw command arguments (includes entity ID)
) => promise<interceptResult>
let commandInterceptorHook: ref<option<commandInterceptor>> = ref(None)
The hook is a module-level ref set once at application startup. None means passthrough (default).
When to call it
Call commandInterceptorHook after identity extraction and before calling generateCommand. The framework's CommandGenerator_Callback.makeGenerateCommand already does this — transport adapters do not call the hook directly. Instead, they construct a CommandGenerator.payload with the extracted identity and pass it to generateCommand, which calls the hook internally.
Payload construction
// CommandGenerator.payload — the struct passed to generateCommand
type payload = {
command: string, // variant constructor name (e.g. "AddProduct")
arguments: 'a, // command args object (entity ID + command fields)
meta: {
ip: array<string>,
user: string, // identity.userId
info: string, // human-readable source (e.g. "Mutation.Catalog_AddProduct")
},
identity: Reventless.Identity.t,
}
The identity flows through the payload into makeGenerateCommand, where the hook intercepts it before publishJsons is called.