Skip to main content

Inbound Delivery

Inbound is the flow where an external system calls into your application: the HTTP gateway receives the vendor's request, verifies it against the system's inbound auth, runs the inbound adapter script — which translates the wire request into the contract's standard input, dispatches it to your business handler, and shapes the reply the vendor expects.

The HTTP Gateway

When vef.IntegrationModule is enabled, the framework registers:

POST /integration/inbound/:systemCode/:contractCode
  • The endpoint is a real route (registered after the API engine, before the SPA fallback); non-integration traffic pays nothing for it.
  • It deliberately bypasses the /api dispatch model: external replies need raw control of status and body, never the standard result envelope.
  • Deliveries are rate-limited per (system, client IP) in a sliding window (vef.integration.inbound.rate_limit, default 120 per minute, enforced per node).

The gateway translates each HTTP request into the protocol-neutral integration.InboundRequest envelope:

FieldContents
SystemCode / ContractCoderesolved from the URL path parameters
Protocol"http"
Method / Path / QueryHTTP-native request data
Headersheader names lowercased, multi-values joined with ", "
Bodythe raw request payload
ClientAddrnetwork peer address, for IP-based verification

Inbound Authentication

Verification is fail-closed: a system without inboundAuth refuses inbound delivery entirely — the none scheme opens it up deliberately. Every verification failure returns the uniform ErrInboundAuthFailed (HTTP 401); missing configuration, missing credentials, and wrong credentials are indistinguishable to the caller, and an unknown or disabled system denies exactly the same way so system codes cannot be enumerated.

Built-in schemes mirror the outbound vocabulary — the same name verifies the wire format its outbound counterpart sends; ip is inbound-only:

SchemeParamsBehavior
noneaccepts every caller; pair with network-level controls
ipwhitelist (comma-separated IP/CIDR entries)verifies the caller's network address; an empty whitelist is treated as a config fault, not open access
http_basicusername, password (sensitive)verifies RFC 7617 Basic credentials in constant time
bearertoken (sensitive)verifies a static Authorization: Bearer token in constant time
headerone entry per credential header (all sensitive)request must present every configured pair (AND); blank configured values never authenticate
queryone entry per credential parameter (all sensitive)request must present every configured pair (AND)
signaturesecret (hex, sensitive)verifies the framework HMAC convention: x-timestamp / x-nonce / x-signature signed over the system code, method, and path — replay-protected through the shared nonce store
scriptfree-form params (all sensitive) + verification bodyruns the custom verification body in a zero-IO runtime; it sees request and decrypted params and grants access by returning a truthy value

Applications register custom schemes with vef.ProvideIntegrationInboundAuthScheme:

type InboundAuthScheme interface {
Name() string
Verify(ctx context.Context, req *InboundRequest, auth *InboundAuthConfig) error
SensitiveParams() []string
}

Business Handlers

The business side of an inbound contract is an integration.InboundHandler — one handler per contract code, registered at boot:

vef.ProvideIntegrationInboundHandler(func(db orm.DB) integration.InboundHandler {
return integration.NewInboundHandler("patient.sync",
func(ctx context.Context, input PatientSyncInput) (PatientSyncOutput, error) {
// input already validated against the contract's input schema
return doSync(ctx, db, input)
})
})
  • integration.NewInboundHandler[I, O](contract, handle) adapts a typed function; the schema-validated input is decoded into I through a JSON round-trip.
  • Handlers must be idempotent: external systems deliver at-least-once.
  • Blank or duplicate contract registrations fail at boot.
  • A delivery for a contract with no registered handler fails with ErrInboundHandlerMissing (HTTP 501) — a deployment fault, not a caller error.

Adapter Script Environment

The inbound adapter script (direction inbound) sees the engine baseline plus:

BindingContents
requestread-only wire request: { protocol, method, path, headers, query, body, clientAddr } — header names lowercased, body as string
systemread-only system view: { code, name, params }
dispatch(input)validates input against the contract's input schema, runs the business handler, validates its output against the output schema, and returns it; failures surface as catchable exceptions
codescode map translation (external code values are the inbound script's core job) — see Code Maps

A typical script:

// 1. Translate the vendor's wire request into the standard input.
const body = JSON.parse(request.body);
const input = {
patientId: body.patient_no,
gender: codes.toCanonical('gender', body.sex),
};

// 2. Dispatch to the business handler (schema-enforced both ways).
const output = dispatch(input);

// 3. Shape the reply the vendor expects.
({ code: '0', msg: 'ok', data: { syncId: output.syncId } })

Batch payloads may call dispatch multiple times; every dispatch is recorded, and the last dispatch failure stays sticky for classification even when the script catches it to shape a partial-success reply.

Shaping the Reply

The script's final value becomes the HTTP response:

  • null / undefined200 with an empty body.
  • Any ordinary value → 200 with the value as JSON.
  • An object of the form { $response: { status, headers, body } } takes raw control: status defaults to 200 (out-of-range values fall back), headers are set verbatim, a string body is sent as text/plain unless the script set its own content type, any other body is JSON. The $-prefixed marker cannot collide with a plausible business payload.
({ $response: {
status: 200,
headers: { 'content-type': 'application/xml' },
body: '<Response><Code>0</Code></Response>',
}})

Uncaught pipeline errors are remapped to statuses an external caller's retry logic can act on (business errors on the /api surface ride HTTP 200, but external callers do not read the envelope):

ConditionStatus
verification failed, unknown/disabled system401
rate limit exceeded429
contract/adapter missing or disabled (behind a verified caller)404 (uniform, so callers cannot probe which piece exists)
input rejected by the contract schema400
no registered business handler501
script fault, render fault, anything else500

Observability

  • Verified deliveries are recorded to the invocation log per vef.integration.log with direction inbound; rejected deliveries stay out of the log deliberately (unauthenticated traffic must not grow the durable evidence trail). Deliveries rejected by verification fold into statistics under an empty contract; rejections for an unknown or disabled system are only logged server-side and never counted.
  • The dry_run_inbound operation is the inbound test console: it executes an inbound script against a synthetic external request with the business handler stubbed to return a supplied sample output. Verification is bypassed — the console tests translation, not credentials; nothing runs against business code and nothing is recorded. See RPC Resources.

Next Step

Code Maps covers per-system value translation used by both flows.