9. Linked Services & Device Transactions (Broker)¶
The broker that routes device-initiated transactions to linked services — the authorize contract, caller authentication, reliability, and health monitoring.
9.1 Overview¶
A linked service is a product backend (e.g. Wallet, Access) the platform calls synchronously during a device-initiated transaction. When a user scans at a pos or gate device, the device makes one mTLS call to POST /v1/device/transactions (§12.1). The platform identifies the palm (1:N), then synchronously calls the device's bound linked service and relays its allow/deny decision back to the device on that same response.
The product owns the business decision (charge the wallet, open the gate); the platform owns identification + routing — it calls the product and relays the verdict, never learning the product's business logic. Webhooks are never on this decision path; the broker is the synchronous channel.
9.2 Linked Service Registry¶
Each tenant registers one entry per product it routes to. The linked_services schema (product_key, base_url, authorize_path, timeout_ms, fail_mode, status) is defined in §13.4.
Managed via /v1/linked-services CRUD (Console, Tenant Admin — §12.1). The product authenticates inbound authorize calls by verifying the identity_assertion JWT against the platform JWKS — the platform stores no per-product secret (§9.5). Within the Link Holdings tenant there are linked services for wallet, access, and each Access sub-product; external tenants register their own.
Diagram — Register a linked service
sequenceDiagram
autonumber
participant Admin as Tenant Admin
participant Console as Web Console
participant Identity as Identity Platform
participant DB as PostgreSQL
participant Audit as Audit Log
Note over Identity, Audit: All audit logging is conditional<br/>on tenant audit_enabled setting
Admin->>Console: Open "Linked Services" → "Add service"
Console-->>Admin: Show form:<br/>product_key, base_url, authorize_path,<br/>health_path, timeout_ms, fail_mode
Admin->>Console: Fill form, click Save
Console->>Identity: POST /v1/linked-services<br/>Authorization: Console session<br/>{product_key: "wallet", base_url,<br/>authorize_path, health_path,<br/>timeout_ms: 800, fail_mode: "closed"}
Identity->>Identity: Validate admin role (Tenant Admin),<br/>extract tenant_id, check product_key<br/>unique per tenant + URLs well-formed
Identity->>DB: Store linked_services record<br/>(id, tenant_id, product_key, base_url,<br/>authorize_path, health_path, timeout_ms,<br/>fail_mode, status: active,<br/>health_status: unknown)
Identity->>Audit: Log: linked_service_created<br/>(tenant_id, product_key, actor: admin)
Identity-->>Console: {id: "lsvc_abc", product_key: "wallet",<br/>status: "active"}
Console-->>Admin: "Wallet backend linked"
Note over Console, Identity: No secret exchanged. The product verifies the<br/>identity_assertion JWT via the platform JWKS (§9.5).<br/>It implements the authorize endpoint (§9.4)<br/>and a health endpoint (§9.7).
Note over Identity: Devices bound to product_key "wallet"<br/>(bound_product, PRD §9.3) now route here<br/>via the broker (§4.3).
9.3 Broker Sequence¶
The full sequence is in §7.4.2: one synchronous call in which the platform identifies the user (1:N, or accepts the device-reported user_id under the small model, §7.10), authorizes the bound product with a signed identity_assertion, relays the decision in-band, and audit-logs device.transaction.completed. The authorize contract is in §9.4.
9.4 The authorize Contract¶
Each product implements one narrow endpoint (base_url + authorize_path from its registry entry, §13.4):
POST {base_url}{authorize_path}
Authorization: Bearer <identity_assertion JWT>
{ "user_id": "...", "tenant_id": "...", "device_id": "...",
"action": "pay", "context": { "amount": 50.00, "currency": "SAR" },
"idempotency_key": "..." }
{ "decision": "allow", "display_message": "Approved · SAR 50.00",
"reference_id": "wal_txn_889", "ttl_seconds": 30 }
decision ∈ allow / deny. The product MUST be idempotent on idempotency_key — a device retry must not double-charge.
9.5 Caller Authentication — identity_assertion JWT¶
The product verifies the assertion JWT against the platform's JWKS (/.well-known/jwks.json, §5.12); no shared secret is stored by the platform.
| Claim | Meaning |
|---|---|
iss |
The platform |
aud |
The linked service's product_key |
sub |
user_id (the identified user) |
device_id, action |
Which device, which action |
context_hash |
SHA-256 of the request context — binds the assertion to this exact request |
idempotency_key |
Ties the assertion to the device call |
iat, exp, jti |
Issued-at, expiry (≤60s), unique id for replay defense |
The product rejects expired assertions and replayed jti values. mTLS is not used for this hop — it is a first-party internal call and the asymmetric assertion already authenticates the caller; mTLS is reserved for the device→platform edge (§8.2). A service mesh providing L4 mTLS is additive, not required.
9.6 Reliability — Timeout, Circuit Breaker, Fail Mode¶
The authorize call sits on the device's critical path, so it is tightly bounded:
| Control | Behavior |
|---|---|
timeout_ms |
Per-linked-service deadline on the authorize call. The product must answer fast — 1:N identify alone is ~850ms (§7.7). |
| Circuit breaker | Repeated timeouts/5xx open the circuit; calls short-circuit to the fail mode until it half-opens. |
fail_mode |
On timeout / open circuit: closed (default — return deny; a gate that cannot confirm must not open) or open (return allow; only for low-value, high-throughput lanes). Per linked service. |
Every transaction — match, no-match, product decision, fail-mode fallback — is audit-logged (device_transaction, §10.3) with latencies, for SAMA traceability. The decision is returned to the device synchronously; the device.transaction.completed webhook (§15) carries the same event to async consumers but is never the device's decision channel. This circuit breaker is the reactive live-path control; proactive detection of a down service is §9.7.
9.7 Health Monitoring¶
The platform proactively probes each active linked service so an outage is caught before it hits a transaction — parallel to the palm-vendor monitor (§7.14.1), and complementary to the per-transaction circuit breaker (§9.6, which remains the live-path control).
- Probe:
GET {base_url}{health_path}on a periodic cadence (platform default 60s), expecting HTTP 2xx (optionally{status, version}). - Unhealthy: N consecutive failures (default 3) flip the service to
unhealthyand emitlinked_service_unhealthy(§15) — audit event + tenant webhook + a console banner for the Tenant Admin. A later successful probe flips it back tohealthy(shown in the status read; no separate event). - Status: surfaced in
GET /v1/linked-services(health_status,last_health_check_at) and the console.
Diagram — Linked-service health check
sequenceDiagram
autonumber
participant Cron as Health Monitor
participant Identity as Identity Platform
participant Product as Linked Service (Wallet / Access Backend)
participant Audit as Audit Log
participant Notify as Tenant Webhook / Console
Cron->>Identity: Trigger health check<br/>(per active linked service, ~60s)
Identity->>Product: GET {base_url}{health_path}<br/>(no body)
alt Healthy — HTTP 2xx
Product-->>Identity: 200 OK {status: "ok", version}
Identity->>Identity: health_status = healthy,<br/>last_health_check_at = now
else Unhealthy — timeout / 5xx
Product-->>Identity: timeout / 5xx
Identity->>Identity: consecutive_failures++<br/>after N (default 3) →<br/>health_status = unhealthy
Identity->>Audit: Log: linked_service_unhealthy<br/>(tenant_id, product_key, reason,<br/>consecutive_failures, severity)
Identity->>Notify: webhook linked_service.unhealthy<br/>+ console banner (Tenant Admin)
end
Note over Identity, Product: Recovery: next successful probe → healthy.<br/>The circuit breaker (PRD §10.6) still guards<br/>the live transaction independently.