Skip to content

10. Linked Services & Device Transactions (Broker)

The broker that routes device-initiated transactions to linked services — the authorize contract, caller authentication, reliability, and health monitoring.

10.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 (§14.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. This keeps the IdP boundary intact — the platform never learns payment or access-control mechanics, only "call product X, relay its verdict." Webhooks are never on this decision path (a device at a gate cannot block on an async webhook); the broker is the synchronous channel.

10.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 §5.4.

Managed via /v1/linked-services CRUD (Console, Tenant Admin — §14.1). The product authenticates inbound authorize calls by verifying the identity_assertion JWT against the platform JWKS — the platform stores no per-product secret (§10.5). Within the Link Holdings tenant there are linked services for wallet, access, and each Access sub-product; external tenants register their own.

Diagram 8.4 — 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 (PRD §6.12);<br/>it must implement the authorize endpoint (PRD §10.4)<br/>and a health endpoint (PRD §10.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).

10.3 Broker Sequence

Full diagram: identity-platform-diagrams.md §4.3.

  1. Device → POST /v1/device/transactions (mTLS) with {scan, context, idempotency_key}.
  2. Platform derives device_id/tenant_id from the SAN URI (§9.2.1) and looks up the device's bound_product + bound_action (§9.3).
  3. Platform runs 1:N identify (PalmVerifier port, §8.3). No match → returns not_recognized in-band; no product call. Small-model branch (§8.13): under the small model the device SDK has already matched and brings the matched user_id in the request; the platform skips this server-side identify and proceeds to step 4 with the device-supplied user_id. Steps 4–6, the mTLS edge, and the identity_assertion are unchanged.
  4. Match → platform calls the linked service's authorize endpoint with a signed identity_assertion.
  5. Product returns {decision, display_message, reference_id, ttl}.
  6. Platform relays the decision in-band, audit-logs the transaction (§11.3), and emits device.transaction.completed (§16) to async subscribers (delivered via §12).

Diagram 4.3 — Device-Initiated Palm Transaction (Synchronous Broker)

sequenceDiagram
    autonumber
    participant User
    participant Device as POS / Gate (Identity-managed)
    participant Identity as Identity Platform
    participant Palm as X-Telcom BioWave Pass Palm Server
    participant Product as Wallet / Access Backend (linked service)
    participant Audit as Audit Log

    Note over Identity, Audit: Device is bound to one {product, action}<br/>(bound_product, bound_action — PRD §9.3)

    User->>Device: Place palm on scanner
    Device->>Device: Capture palm image (RGB + IR)
    Device->>Identity: POST /v1/device/transactions<br/>(mTLS — device cert)<br/>{scan, context: {amount, currency},<br/>idempotency_key}

    Identity->>Identity: Parse SAN URI → device_id, tenant_id<br/>Look up bound_product, bound_action
    Identity->>Palm: POST /KZ/query (1:N)<br/>tenant-prefixed user_id namespace

    alt Match — code: 0
        Palm-->>Identity: {code: 0, data: {results: [{user_id, scores: [4]}],<br/>thresholds: [4]}}
        Identity->>Identity: Apply tenant match_policy;<br/>sign identity_assertion JWT<br/>(sub: user_id, aud: product_key,<br/>context_hash, jti, exp ≤ 60s)
        Identity->>Product: POST {base_url}{authorize_path}<br/>Authorization: Bearer <identity_assertion><br/>{user_id, action, context, idempotency_key}

        alt Product responds within timeout_ms
            Product->>Product: Verify JWT vs Identity JWKS;<br/>business decision (balance / access rights)
            Product-->>Identity: {decision: "allow" | "deny",<br/>display_message, reference_id, ttl}
            Identity->>Audit: Log: device_transaction<br/>(user_id, device_id, product, decision, latency_ms)
            opt Webhook subscribed
                Identity->>Identity: Send webhook:<br/>device.transaction.completed
            end
            Identity-->>Device: {decision, display_message,<br/>product_reference} (in-band)
            Device-->>User: Open gate / "Approved" (or decline)
        else Timeout / circuit open
            Note over Identity, Product: fail_mode per linked service<br/>(default: closed)
            Identity->>Audit: Log: device_transaction<br/>(decision: fail_closed,<br/>reason: authorize_timeout)
            Identity-->>Device: {decision: "deny"} (fail-closed)
            Device-->>User: "Try again"
        end

    else No match — platform-decided (scores < thresholds)
        Palm-->>Identity: {code: 0}<br/>(no candidate ≥ thresholds)
        Identity->>Audit: Log: device_transaction (not_recognized)
        Identity-->>Device: {decision: "not_recognized"}
        Device-->>User: "Palm not recognized"

    else Low confidence — platform-decided
        Palm-->>Identity: {code: 0}<br/>(scores < thresholds)
        Identity->>Audit: Log: device_transaction (not_recognized, low_confidence)
        Identity-->>Device: {decision: "not_recognized"}
        Device-->>User: "Palm not recognized — try again"

    else Query type forbidden — code: 30008
        Palm-->>Identity: {code: 30008, msg: "query type forbidden"}
        Identity->>Audit: Log: device_transaction (forbidden, query_type_restricted)
        Identity-->>Device: {decision: "deny", reason: "palm_type_not_allowed"}
        Device-->>User: "Try other hand"

    else Vendor error — code: 50000
        Palm-->>Identity: {code: 50000, msg: "Milvus error"}
        Identity->>Audit: Log: palm_vendor_unauthorized (severity: critical)
        Identity-->>Device: 503 Service Unavailable
    end

Diagram 4.4 — Device-Initiated Enrollment (POS → Consent → Enroll)

sequenceDiagram
    autonumber
    participant User
    participant Device as POS / Kiosk (Identity-managed)
    participant Identity as Identity Platform
    participant Palm as X-Telcom BioWave Pass Palm Server
    participant SMS as SMS Provider
    participant Audit as Audit Log

    User->>Device: Tap "Enroll" / "Sign up"
    Device->>Identity: POST /v1/device/signup (mTLS)<br/>{mobile}
    Identity->>Identity: Find or create user by mobile<br/>(tenant-scoped)
    Identity->>SMS: Send OTP
    SMS-->>User: OTP code
    User->>Device: Enter OTP
    Device->>Identity: POST /v1/device/signup/verify (mTLS)<br/>{challenge_id, code}
    Identity-->>Device: {user_id, is_new_user,<br/>palm_enrolled: false,<br/>consent_status, kyc_status}

    opt Product requires KYC and kyc_status != verified
        Device-->>User: Direct to complete KYC first<br/>(app / Nafath)
    end

    opt consent_required and consent_status = none
        Device->>User: Show consent screen
        User->>Device: Accept
        Device->>Identity: POST /v1/device/consent (mTLS)<br/>{user_id, consent_type, version}
        Identity->>Audit: Log: consent.granted
    end

    User->>Device: Place palm
    Device->>Device: Capture palm image (RGB + IR)
    Device->>Identity: POST /v1/device/enroll (mTLS)<br/>{user_id, scan}

    alt consent satisfied
        Identity->>Palm: POST /KZ/add<br/>tenant-prefixed user_id
        Palm-->>Identity: {code: 0, palm_id}
        Identity->>Audit: Log: enrollment.complete
        opt Webhook subscribed
            Identity->>Identity: Send webhook: enrollment.complete
        end
        Identity-->>Device: {palm_enrolled: true}
        Device-->>User: "Enrolled — palm now works across Link"
    else consent missing — 403
        Identity-->>Device: 403 {error: consent_required}
        Device-->>User: "Consent required"
    end

10.4 The authorize Contract

Each product implements one narrow endpoint (base_url + authorize_path from its registry entry, §5.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.

10.5 Caller Authentication — identity_assertion JWT

The platform is the signing authority (the IdP), so the assertion is the caller credential — no shared secret is stored by the platform. The product verifies the JWT against the platform's JWKS (/.well-known/jwks.json, §6.12).

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 (§9.2). A service mesh providing L4 mTLS is additive, not required.

10.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 (§8.6).
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, §11.3) with latencies, for SAMA traceability. The decision is returned to the device synchronously; the device.transaction.completed webhook (§16) 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 §10.7.

10.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 (§8.12.1), and complementary to the per-transaction circuit breaker (§10.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 (§16) — 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 9.4 — 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.