inmake DOCS
Docs / Modules / The envelope

The envelope

What every call from the platform carries: headers, body, labels, verifying the signature, what the gateway does around the call.

Every platform → module call is POST {endpoint}{path} with the same headers and the same body shape, whatever the point and whatever the tier. The module verifies it before doing anything else.

3.1 Headers#

Header Value On
access-seed <ts>.<hex HMAC-SHA256("<ts>.<label>", module_key)> — generateSeed(); there is no access_seed twin (underscored headers are dropped by nginx) every call
x-drop-module the module id every call
x-drop-point web | admin | event | inbound | callback:{name} | schedule | install | verify every call
x-drop-store the store id (sid) every call
x-drop-env prod | demo every call
x-drop-request a uuid v4 — the dedupe key; a retried event carries the same value every call
x-drop-updated the doc's Firestore updateTime in ms — the receiver's out-of-order guard events only
x-drop-event, x-drop-locale, x-drop-collection, x-drop-form, x-drop-id, x-drop-schema, x-drop-version, x-drop-storage-path, x-drop-deleted the existing hook delivery context (docs/hooks-and-caching.md) events only
content-type application/json every call

3.2 Body#

{module, store, env, request, point, config,
 locale?, identity?: {customer}, member?: {uid, email, role, capabilities},
 entity?: {kind, id}, url_tokens?: {[path]: string}, input}
  • config — the install's non-secret config map, on every point, so a module never reads its own install doc per call. Secrets never ride the envelope; the module reads those itself (§5).
  • input — the point's payload (§4).
  • identity — the storefront customer, on web calls that carried an identity-token.
  • member — the console user behind an admin call.
  • entity — the order / product / customer an admin mount was placed on.
  • url_tokens — rides only install, verify and admin: the url_key of each inbound route so a module can register its own webhook URL.

3.3 Labels#

The seed is bound to a per-call label — verifying the signature verifies which call it is:

Point Label
install install
verify verify
web web/{path}
admin admin/{path}
callback:{name} callback/{name}
schedule schedule
inbound inbound/{path} (signed with inbound_key, not module_key)
event the labels hooks already use — {locale}/{collection}/{id} for data, orders/{id}, invoices/{id}, forms/{form}/{id}, files/{storage_path} — plus modules/{id}/{collection}/{docId} for module_records

3.4 Verifying#

verifyModuleEnvelope(headers, key) in core-lib utils/module_envelope.ts, re-exported by drop-core-sdk and drop-core-server like every core-lib util:

verifyModuleEnvelope(headers, moduleKey)
  → {ok: true, point, label, request}
  | {ok: false, reason: VerifySeedReason | 'missing_point' | 'missing_request'}

Replay tolerance is 5 minutes (maxAgeMs: 5 * 60_000) with 60 s of future skew. A connected module on Express is ten lines with expressModuleRouter(handlers, {moduleKey, sessionKey}) from drop-core-server (src/module/express_module.ts, express lazy-required), which verifies every envelope, dispatches on point, computes the install x-drop-ack itself, dedupes x-drop-request (a 1,000-entry LRU) and stamps x-drop-module-version on every response:

import express from 'express';
import {expressModuleRouter} from 'drop-core-server';

const app = express();
app.use(expressModuleRouter({
    install: {onKeys: keys => store.save(keys), onToken: t => store.save(t)},   // the two-step handshake (§4.1)
    verify:  async ({config}) => ({ok: !!config.account}),
    event:   async ({input}) => { await sync(input); },
    web:     async ({input}) => ({status: 200, headers: {'content-type': 'application/json'}, body: '{}'}),
}, {moduleKey: () => store.get('module_key'), sessionKey: () => store.get('session_key')}));
app.listen(8080);

(Illustrative — the handler object is typed ModuleHandlers = {install?, verify?, event?, schedule?, web?, admin?, callbacks?: Partial<Record<ModuleCallbackName, Handler>>} in the starter; the same object feeds the Extism entry for the hosted tier.) A module that is not on Express calls verifyModuleEnvelope itself and answers 403 on {ok: false}.

3.5 What the gateway does around the call#

  1. Loads the install (cached, invalidated by every control-plane write); enabled: false → the caller gets 404 {error: 'module_disabled'} and the module is not called.
  2. Loads custody (the install token + the three keys, in the platform's secret store — never the install doc).
  3. Signs with module_key and the point's label.
  4. Delivers: connected → fetch(endpoint + path) with AbortSignal.timeout(ms) after the SSRF check (https: only, a public hostname, port 443 or none, none of the platform's hosts; redirect: 'manual' — a 3xx is a failure, a module endpoint answers directly); hosted / platform → the runner (§8.8): the same envelope, the bundle by custody object + wasm_hash, the install token, the clamped limits and the declared egress; the hosted profile's calls_per_day is metered first (429 past the cap).
  5. Caps the response by content-length and a capped stream read: 64 KiB for callbacks / events / admin, 1 MiB for web (callback:shipment.label, callback:shipment.manifest and callback:invoice.pdf: 4 MiB — the bytes come back inline).
  6. Records a drop.stat row (module_call; storefront hits are module_web).
  7. Health: a failure (network error, timeout, 5xx, bad JSON, size cap) increments install.health.points[point].failures; success resets it. At 20 consecutive failures on a point install.health = {state: 'failing', point, since, last_error} and the point short-circuits to its fallback (§4) until Retry in the console (POST /api/module/enable {id, retry: true}) or a successful schedule call clears it.

x-drop-module-version. Every module response may carry it (the Express router does). A value ≠ the installed manifest.version sets ModuleInstall.drift = {version, seen_at} and the console says the endpoint runs {v}; upload the {v} package to update. The platform never re-reads a manifest from an endpoint — the package is the contract.