Skip to content

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 }

decisionallow / 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 unhealthy and emit linked_service_unhealthy (§15) — audit event + tenant webhook + a console banner for the Tenant Admin. A later successful probe flips it back to healthy (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.