Complete field-by-field reference for the BridgeConfig YAML/JSON model. All field names match the YAML/JSON struct tags in ports/blueprint.go.
Duration format. Every duration field takes a string with a unit (
"30s","5m","1h30m"). A bare number (30) is rejected by the strict decoder – it would be read as nanoseconds, which is never intended.
classDiagram
class BridgeConfig {
bridge: BridgeSettings
config_watch: ConfigWatchDef
stores: StoresConfig
sessions: SessionDef[]
receivers: ReceiverDef[]
senders: SenderDef[]
bindings: BindingDef[]
routes: RouteDef[]
http: HTTPConfig
}
class BridgeSettings {
id: string
instance_id: string
deployment_mode: string
shutdown_timeout: string
drain_timeout: string
per_record_drain_timeout: string
max_drain_timeout: string
log_level: string
cluster: ClusterConfig
}
class RouteDef {
id: string
receiver_id: string
delivery_mode: string
dispatch_mode: string
policy: PolicyDef
bindings: string[]
processors: string[]
resolver: ResolverDef
session: RouteSessionDef
}
class ResolverDef {
type: string
default_binding: string
header_key: string
header_map: map
rules: RuleDef[]
}
class RuleDef {
binding_id: string
match: ConditionDef[]
}
class ConditionDef {
field: string
operator: string
value: any
}
class PolicyDef {
max_in_flight: int
ack_after: string
max_replay_attempts: int
replay_budget: string
backoff: BackoffDef
on_expired: string
on_permanent_failure: string
on_filtered: string
}
BridgeConfig --> BridgeSettings
BridgeConfig --> RouteDef
RouteDef --> PolicyDef
RouteDef --> ResolverDef
RouteDef --> RouteSessionDef
ResolverDef --> RuleDef
RuleDef --> ConditionDef
PolicyDef --> BackoffDef
RouteSessionDef --> DrainStrategyDef
bridge – Bridge Settings| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id |
string | yes | – | Unique bridge identifier |
instance_id |
string | no | auto-generated | Instance identifier (useful in clustered mode) |
deployment_mode |
string | no | standalone |
standalone or clustered |
shutdown_timeout |
duration | no | 30s |
The process shutdown budget on SIGTERM. In the shipped gobridge-aws image it is the ONE budget the config-watcher join, rollout-drive stop, HTTP shutdown, runtime drain, store close and telemetry flush all run inside, so drain_timeout must stay below it (the 30s/30s defaults leave no headroom; 45–60 s is the production shape). cmd/gobridge bounds its supervisor wait and, separately, its HTTP stop by it. See Health Checks and Graceful Shutdown. Keep the orchestrator’s stop grace above this value. |
drain_timeout |
duration | no | 30s |
How long the supervisor lets a runtime drain when it STOPS one (shutdown, or a reconfiguration swap). It is the ceiling on Runtime.Stop, not a per-batch outbox budget. |
per_record_drain_timeout |
duration | no | 3s |
Per-record budget in the outbox drain batch ceiling min(batchCount * per_record_drain_timeout, max_drain_timeout). The ceiling may only RAISE a batch budget already floored at one full send, so it can never cut a send short. |
max_drain_timeout |
duration | no | 10s |
Upper bound of that batch ceiling. Must be >= per_record_drain_timeout. |
log_level |
string | no | info |
Log level: debug, info, warn (alias warning), error. Case-insensitive. Validated as a closed enum – an unknown value is rejected at load instead of being silently ignored. Applied when the reload that carries it is installed, never before: a candidate that fails validation or fails to build leaves process verbosity unchanged, so the running system always matches a config an operator can read back |
bridge.cluster – Cluster Config| Field | Type | Required | Default | Description |
|---|---|---|---|---|
endpoints |
map[capability]url | no | auto-discovered | Static override for THIS instance’s advertised capability endpoints, keyed by capability (http) with a full URL value (http://host:port). NOT a peer/instance map. |
rollout |
string | no | refuse |
Live-config-change strategy for a clustered deployment. refuse (the default, also the meaning of an unset value) is ADR 0012: every live config delta is rejected and changes roll by whole-cohort replacement. independent lets every member apply a live-safe delta on its own, exactly as a standalone bridge does – no barrier, no vote, no shared store, no roster; the cost is a brief window where one member runs the new config and another still runs the old one. coordinated opts into the rollout barrier; it additionally requires members and a rollout store wired by the composition root. All three still refuse a delta that cannot be applied live on any node (a durable session identity, a store target), with the same reason a standalone bridge gives. Any other value is rejected at load – an unrecognised strategy is never silently downgraded to refuse. |
members |
list of string | yes when rollout: coordinated; unused otherwise |
– | The cohort ROSTER: every member’s stable member_id, listed identically on every member, without duplicates. It is the membership epoch the barrier freezes at propose time and counts acknowledgements against, so an id that no running process announces stalls every rollout. This is a peer list, unlike endpoints. Each id must match the member_id its process announces (in the AWS profile, the bootstrap member_id). |
confirm_window |
duration | no | unset (base protocol) | Opts a coordinated rollout into the confirm window: a positive duration makes every commit PROVISIONAL. Each member swaps and must then reach convergence; the coordinator confirms once the whole cohort converged, and if confirmation never lands every member reverts to the last confirmed generation. Unset or 0s is the base protocol, where a commit is final. A failed trial costs two disruptions (apply, then revert), which is why it is opt-in. Only valid with rollout: coordinated – independent has no cohort-wide commit to confirm and nothing that could revert one. |
bridge:
id: production-bridge
instance_id: instance-01
deployment_mode: clustered
shutdown_timeout: 45s
# Prefer the scaled drain formula for production workloads. The batch
# deadline is computed as min(batchCount * per_record_drain_timeout,
# max_drain_timeout); drain_timeout is ignored when either new field
# is set and is retained only for backward compatibility.
per_record_drain_timeout: 3s
max_drain_timeout: 30s
log_level: info
cluster:
# THIS instance's advertised capability endpoints, keyed by capability with
# a full URL value. The HTTP forwarder POSTs remote exclusive requests to
# endpoints["http"], so a static override MUST carry an "http" key. This is
# NOT a peer/instance map — leave it unset to auto-discover instead.
endpoints:
http: "http://10.0.1.10:8080"
# Live-config-change strategy. Default (unset, or "refuse") is ADR 0012:
# a clustered node refuses every live config delta and changes are rolled by
# whole-cohort replacement. "coordinated" opts into the barrier protocol in
# docs/cluster/spec/cluster-config-rollout-protocol.md and additionally requires
# `members` below and a rollout store wired by the composition root.
#
# The shipped AWS profile wires that store, but ONLY in its static member-slot
# shape (GoBridgeDynamoDBHA with MemberSlots): every cohort member runs as its
# own single-task ECS service with a restart-stable member_id. The autoscaled
# worker shape rejects "coordinated" at synth time, because interchangeable
# tasks cannot carry a stable member_id. See docs/cluster/README.md.
rollout: refuse
# The cohort ROSTER — the membership epoch the rollout barrier freezes and
# counts acknowledgements against. Required and non-empty when rollout is
# "coordinated"; identical on every member; no duplicates. This is a peer
# list, unlike `endpoints` above. It is fixed for the life of the cohort in
# EVERY rollout mode, including "independent": add or remove a member by
# redeploying the cohort, not by reloading it. Reordering it, or repeating an
# id, names the same cohort and is not a change.
members: [instance-01, instance-02]
# Optional. A positive confirm window makes every commit provisional: each
# member applies, and unless the whole cohort converges before the window
# expires, every member reverts to the last confirmed generation.
# confirm_window: 90s
config_watch – File Watch SettingsControls how file-based config changes are detected. Only relevant when using file watchers.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
mode |
string | no | notify |
notify (fsnotify) or poll (SHA-256 comparison) |
poll_interval |
duration | no | 30s |
In poll mode, the file re-read + content-hash comparison interval. In notify mode it doubles as the resync cadence – a slow hash-reconciliation poll behind fsnotify that catches a missed or coalesced filesystem event within one interval (one knob for “how stale a missed change can be”). |
debounce |
duration | no | 100ms |
Debounce period for notify mode |
config_watch:
mode: poll
poll_interval: 30s
stores – Backing Store ConfigurationConfigures the backing stores for lease coordination, outbox persistence, dead-letter queue, and exact managed MQTT subscription history.
| Role | Purpose | Required When |
|---|---|---|
lease |
Distributed lease ownership | Exclusive sessions in clustered mode |
outbox |
Durable message outbox | delivery_mode: shared_outbox |
dlq |
Dead-letter queue | on_expired: dlq, on_permanent_failure: dlq, or on_filtered: dlq |
managed_subscriptions |
Exact durable MQTT topic-filter history | Persistent/exclusive MQTT session with desired subscriptions |
| Field | Type | Required | Description |
|---|---|---|---|
type |
string | yes | Store backend: memory, sqlite, dynamodb |
options |
map | no | Backend-specific options |
Memory: no options required. Memory does not implement managed_subscriptions, because process-local history cannot survive restart.
SQLite (type: sqlite):
| Option | Type | Applies to | Default | Description |
|---|---|---|---|---|
path |
string | all roles | – (required) | Database file path (:memory: remains available to non-durable test roles). For managed_subscriptions, this must be a plain absolute, already-clean filesystem path: :memory:, relative paths, file: URIs, queries, and fragments are rejected. The final parent must be owner-controlled 0700 (missing adapter-owned parents are created as 0700); the database, WAL, SHM, and journal are descriptor-validated non-symlink regular files at 0600. Insecure existing paths are rejected, never silently chmodded. |
stale_claim_duration |
duration | outbox | runtime-derived | How long a same-owner stranded claim waits before another claim attempt may take it. Failover reclaim via a higher fencing version is always immediate and independent of this. |
retention |
duration | outbox | 1h |
Window completed/expired outbox rows are kept before piggybacked compaction deletes them. Negative disables compaction (rows kept forever). Keep comfortably above any upstream redelivery window, since deleting a terminal row releases its duplicate-detection identity. |
Windows limitation:
sqlitemanaged-subscription history currently fails construction explicitly on Windows. The adapter requires descriptor-relative no-follow creation and validation for the database and WAL/SHM/journal sidecars; equivalent secure Windows handle semantics are not implemented. Use DynamoDB for this role on Windows. Other SQLite store roles are unaffected.
DynamoDB (type: dynamodb):
| Option | Type | Applies to | Default | Description |
|---|---|---|---|---|
table_name |
string | all roles | gobridge-leases / gobridge-outbox / gobridge-dlq / gobridge-managed-subscriptions |
Overrides the role-specific DynamoDB table name. Runtime preflight and the AWS CDK grant path resolve the same default when omitted. |
stale_claim_duration |
duration | outbox | runtime-derived | Same semantics as the SQLite outbox key above. |
compaction_grace |
duration | outbox | store default | Window completed/expired outbox items are kept before the DynamoDB item TTL deletes them. Keep above any upstream redelivery window. |
retention |
duration | DLQ | none (kept until deleted) | TTL on dead-letter entries (ttl = failed_at + retention). Use a days-scale value so investigators have time to inspect dead-lettered messages. |
max_scan_pages |
int | DLQ | 100 |
Bounds the number of DynamoDB pages an index-less List/Purge reads before stopping, preventing an unbounded full-table scan. Negative disables the bound. |
The AWS region and endpoint are NOT store options: they come from the standard AWS SDK configuration chain (environment, shared config, IAM role). Supplying
options.regionoroptions.endpointis rejected by the strict decoder.
Managed-subscription data models and startup contract. SQLite keeps a baseline
identity row plus exact (storage_identity, filter) rows. DynamoDB uses a
storage_identity string HASH key, a baseline BOOL, and an optional filters
String Set; grant GetItem, UpdateItem, and DescribeTable (the standard
read/write-data grant is a permitted superset). The identity is an opaque,
secret-safe durable-session fingerprint. A missing baseline or any store outage
fails startup before the MQTT broker connection is opened; seed an explicit
empty baseline for a genuinely new durable session — ManagedSubscriptionBaselines
on the AWS profile, -seed-managed-subscriptions <session-id> on the reference
binary, or Builder.SeedManagedSubscriptionBaselines in a custom composition
root (see durable sessions).
DLQ read ordering is oldest-first (failed_at ASC) across all backends, so
operators triage the earliest failures first.
Fence retention floor (30 days). Independently of retention /
compaction_grace, the outbox keeps each partition’s fence row (its dedup /
ordering high-water-mark) for at least max(retention_or_compaction_grace, 30d)
past the last write that touches the partition. Setting a shorter retention
does not shorten this floor: it bounds how aggressively terminal record rows
are compacted, not the fence. The floor stops ephemeral/rotating session
partitions from accreting one immortal fence row each, while 30 days of
abandonment is deemed safe because such a partition has no competing owner left
to fence (sqliteoutbox/outbox.go, dynamodboutbox/acl_store.go).
DynamoDB outbox indexes. The outbox table requires three sparse indexes —
ExpiryIndex (hash has_expiry, range expires_at, KEYS_ONLY),
RecordIDIndex (hash record_id, KEYS_ONLY) and ClaimIndex (hash PK,
range claim_sort, Projection: ALL). Store.CreateTable provisions all
three; the factory preflight rejects a table that is missing one or has
ClaimIndex under-projected. See
DynamoDB outbox table schema.
Set stale_claim_duration above your worst-case drain-batch timeout. A
stale-claim reclaim is crash recovery — it hands a record to a second sender on
the assumption the first is dead — so a value below the time a HEALTHY owner can
hold a claim duplicates deliveries that are still in flight, silently. Validation
enforces two bounds on an explicit value:
| Value | Result |
|---|---|
at or below the largest route send_timeout |
rejected — the first owner’s send has not even timed out yet |
at or below send_timeout + the drain-batch ceiling (max_drain_timeout, else drain_timeout) |
warning — a record claimed at the head of a batch can wait most of that budget before its own send starts |
| above both | accepted |
Leaving the key unset is the safe default: the bridge derives
step_down_grace + max(2 x step_down_grace, 15s) and no bound is applied.
stores:
lease:
type: dynamodb
options:
table_name: gobridge-leases
outbox:
type: dynamodb
options:
table_name: gobridge-outbox
stale_claim_duration: 30s
dlq:
type: memory
options:
# In-memory DLQ entries are lost on restart, erasing the terminal
# evidence of dropped messages; the loss must be acknowledged.
acknowledge_volatile: true
managed_subscriptions:
type: dynamodb
options:
table_name: gobridge-managed-subscriptions
sessions – Transport SessionsSessions represent stateful transport connections (an MQTT connection, an AMQP connection). Only a transport whose factory reports the stateful_session capability creates one; the stateless transports (sqs, servicebus, http) return no session, and the builder rejects a receiver or sender that names a session_id on one of them.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id |
string | yes | – | Unique session identifier |
transport |
string | yes | – | A stateful transport kind: mqtt (alias mqtt.paho), amqp091 (alias amqp.amqp091) or amqp10 (alias amqp.amqp10). A stateless kind cannot be named here. |
session_mode |
string | no | ephemeral |
ephemeral, persistent, exclusive |
options |
map | no | – | Transport-specific options (see Transport Reference) |
Session modes:
ephemeral – Default. Clean session, no state between connections.persistent – Session state preserved across reconnections (MQTT: clean_start=false).exclusive – Lease-based single holder. Only one instance owns this session at a time. Requires a lease store.sessions:
- id: mqtt-conn
transport: mqtt
session_mode: exclusive
options:
session:
broker_urls:
- tcp://mqtt.example.com:1883
client_id: bridge-primary
receivers – Message Ingress| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id |
string | yes | – | Unique receiver identifier |
transport |
string | cond. | – | Transport name (required if no session_id) |
session_id |
string | cond. | – | Reference to a session (required if no transport) |
topics |
array | no | – | Subscription definitions |
options |
map | no | – | Transport-specific receiver options |
receivers[].topics[] – Subscriptions| Field | Type | Required | Default | Description |
|---|---|---|---|---|
topic |
string | yes | – | Topic or subject pattern |
qos |
int | no | 0 | Quality of service (MQTT: 0, 1, 2) |
options |
map | no | – | Per-subscription options |
receivers:
- id: sensor-in
session_id: mqtt-conn
topics:
- topic: "sensors/temperature/#"
qos: 1
- topic: "sensors/humidity/#"
qos: 1
Note: Either transport or session_id must be set. When session_id is set, the transport is inferred from the session.
senders – Message Egress| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id |
string | yes | – | Unique sender identifier |
transport |
string | cond. | – | Transport name (required if no session_id) |
session_id |
string | cond. | – | Reference to a session |
options |
map | no | – | Transport-specific sender options |
senders:
- id: sqs-out
transport: sqs
options:
queue_url: https://sqs.us-west-1.amazonaws.com/123456/events
batch_size: 10
bindings – Route DestinationsBindings connect routes to senders with a specific address.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id |
string | yes | – | Unique binding identifier |
sender_id |
string | yes | – | Reference to a sender |
session_id |
string | no | – | The session this binding runs on and – under shared_outbox – the outbox partition its records live in and are drained from; naming a session here also gives it its lease-held manager. An ingress session that only a receiver names needs neither this nor a route session block: the receiver’s own binding to it connects it and reconciles its subscriptions, with no lease and no partition (the shape a direct_hold route holds its source in). A session declared exclusive must still be named here or in a route session block, because exclusive means lease-held. |
address |
string | yes | – | Destination address – supports {header} templates |
options |
map | no | – | Per-binding options – parsed but not consumed at runtime (DestinationBinding.Config is never read); leave bindings bare and configure the transport on the sender. |
The address field supports {placeholder} tokens that are resolved at runtime from envelope headers:
bindings:
- id: to-factory-topic
sender_id: mqtt-out
address: "factory/{factory_id}/orders/{device_id}"
A message with headers factory_id: "A" and device_id: "42" resolves to topic factory/A/orders/42. The MQTT sender uses this as the publish topic. Missing placeholders cause an error. See Dynamic Destination Routing for full details.
bindings:
- id: to-events-queue
sender_id: sqs-out
address: events
- id: to-archive-topic
sender_id: mqtt-out
address: archive/events
The remainder of the configuration model – routes, the http API block, the
ID reference graph, delivery hooks, programmatic builder/lifecycle notes, and
the validation-rules summary – is documented in
Routes, Runtime & Validation Reference. That
reference also covers the per-route lease-timing knobs under routes[].session,
including renew_call_timeout, acquire_poll_interval, failover_slo,
startup_allowance, and broker_health_step_down. A declared SLO is validated
from failure detection through ServiceLevelFull against BOTH failure modes;
measured warm and cold evidence is still required. The arithmetic, and both
shipped lease profiles evaluated at their defaults, are in
Failover Budget.