Skip to content

8. Palm Verification

Palm enrollment and verification — the PalmVerifier port, score model, duplicate detection, palm-type locking, and the small/large model split.

8.1 Overview

Identity Platform uses the same ports and adapters (hexagonal) architecture for palm biometric verification as it does for KYC. The core domain defines a standard PalmVerifier interface, and vendor-specific adapters implement it. This allows the platform to swap or run multiple palm verification vendors without changing business logic.

8.2 Architecture

┌─────────────────────────────────────────────────────┐
│                 Identity Platform                    │
│                                                      │
│   ┌─────────────────────────────────────────────┐   │
│   │              Core Domain                     │   │
│   │                                              │   │
│   │   User, PalmTemplate, Enrollment, Challenge  │   │
│   │                                              │   │
│   └─────────────────────────────────────────────┘   │
│                        │                             │
│                PalmVerifier Port                     │
│                   (interface)                        │
│                        │                             │
│                        ▼                             │
│              ┌───────────────────────┐               │
│              │  X-Telcom BioWave Pass │               │
│              │   Adapter (sole MVP)   │               │
│              └───────────────────────┘               │
│                                                      │
└─────────────────────────────────────────────────────┘

8.3 PalmVerifier Port (Interface)

The PalmVerifier Port defines the abstract interface that all palm verification vendor adapters must implement.

Method Parameters Returns Description
enroll user_id, palm_template EnrollmentResult Store a palm template for a user
verify user_id, palm_scan VerificationResult 1:1 match against stored template
identify palm_scan, tenant_id IdentificationResult 1:N search across all templates
get_status user_id EnrollmentStatus Check if user has an enrolled template
delete_template user_id boolean Remove a stored palm template

verify (1:1) vs identify (1:N) — the difference is whether the caller already names the user:

  • verify (1:1) checks a claimed identity: the caller already knows the user_id (e.g. an e-signature challenge carries it), and the platform compares against that one template. This is the public, challenge-based path for personal scanners (§9.2, §13).
  • identify (1:N) answers "who is this?" from a bare palm with no claim. Device-initiated transactions are claim-less, so they always use identify — and it runs internally inside the broker (§10), not as a public endpoint.

8.4 Score Model

X-Telcom BioWave Pass returns a 4-element scores array alongside a 4-element thresholds array per query — IR and RGB features evaluated against the large and small models. Each index corresponds to a model variant:

Index Variant
0 Large-model IR
1 Large-model RGB
2 Small-model IR
3 Small-model RGB

Match Policy (configured per tenant via palm_match_policy):

Policy Behavior
all_thresholds All 4 scores must be ≥ corresponding thresholds (default — most strict)
majority At least 3 of 4 scores ≥ corresponding thresholds (relaxed)
any At least 1 score ≥ corresponding threshold (least strict — discouraged)

Under the large model, the platform applies the policy after receiving the vendor response — the matching decision (matched: true/false) is platform-controlled, not vendor-controlled. Under the small model the SDK/verification server decides and the platform records the device-reported result (§8.13).

8.5 Verification Result

{
  "user_id": "user_123",
  "matched": true,
  "scores": [1.0, 1.0, 1.0, 0.9495],
  "thresholds": [0.7018, 0.7211, 0.7072, 0.7253],
  "match_policy": "all_thresholds",
  "latency_ms": 180,
  "vendor": "biowave",
  "timestamp": "2026-02-25T10:00:00Z"
}

8.6 Identification Result

{
  "matched": true,
  "user_id": "user_123",
  "palm_id": 100001,
  "scores": [1.0, 1.0, 1.0, 0.9495],
  "thresholds": [0.7018, 0.7211, 0.7072, 0.7253],
  "match_policy": "all_thresholds",
  "candidates": 1,
  "search_pool_size": 124500,
  "latency_ms": 850,
  "vendor": "biowave",
  "timestamp": "2026-02-25T10:00:00Z"
}

8.7 Supported Palm Vendors

Vendor Technology Strengths Integration
X-Telcom BioWave Pass Palm vein (IR + RGB; large/small models) Sole MVP vendor; large-scale 1:N comparison; self-hostable REST API (/KZ/*)

Additional vendors can be plugged in behind the PalmVerifier port (§8.3) with no business-logic change; BioWave Pass is the only vendor documented for MVP.

8.8 Vendor Configuration (per Tenant)

{
  "tenant_id": "wallet",
  "palm_config": {
    "vendor": "biowave",
    "vendor_config": {
      "base_url": "http://palm.internal.link.sa:8080",
      "request_id_header": "request_id",
      "timeout_ms": 2000
    },
    "match_policy": "all_thresholds",
    "duplicate_check_enabled": true,
    "duplicate_action": "reject"
  }
}

8.9 Palm Verification Flow

Device-initiated (pos/gate — 1:N identify, large model):

1. User scans palm at the device
2. Device → Identity: POST /v1/device/transactions (mTLS) with {scan, context}
3. Identity → PalmVerifier Port → Vendor Adapter → Vendor API (1:N identify)
4. Vendor returns 4-element scores + thresholds per candidate
5. Identity applies the tenant's match_policy (§8.4) to decide the match
6. Match → Identity calls the bound product's authorize endpoint, relays the verdict (§10)
7. Identity returns {decision, display_message, product_reference} to the device in-band

Personal scanners (1:1 verify / e-signature) use the challenge-poll flow instead — see §9.2.

Under the small model this server-side identify step is replaced by the device SDK's direct match + report — see §8.13.

8.10 Pre-Enrollment Duplicate Detection

Detects identity fraud and accidental re-enrollment before committing a new palm template. Opt-in per tenant via palm_duplicate_check_enabled.

Flow: 1. Before committing the enrollment, the platform runs a 1:N POST /KZ/query with the new palm features. 2. BioWave returns the best match (user_id, id, 4-element scores/thresholds). A true duplicate also surfaces vendor code 30007 (feature already registered) on /KZ/add. 3. Platform applies the tenant's palm_match_policy: a conflict exists when a match passes thresholds AND has a different user_id than the enrolling user. 4. If a conflict is found, the configured palm_duplicate_action determines what happens:

Action Behavior
reject Enrollment fails with HTTP 409. User cannot enroll until ops reviews. Audit event: palm_duplicate_detected with severity: high and matched_user_ids.
flag Enrollment proceeds but a review_case record is created for ops to investigate.

When to enable: High-value tenants (banks, regulated finance) and any tenant where a single user enrolling under multiple identities is a fraud risk.

Performance impact: Adds one extra round-trip to the vendor per enrollment (~200–800ms). Not enabled by default.

Diagram 4.1 — Pre-Enrollment Duplicate Detection

sequenceDiagram
    autonumber
    participant Kiosk as POS / Kiosk
    participant Wallet as Wallet Backend
    participant Identity as Identity Platform
    participant Palm as X-Telcom BioWave Pass Palm Server
    participant DB as PostgreSQL
    participant Audit as Audit Log

    Note over Kiosk: User has placed palm,<br/>features extracted

    Kiosk->>Wallet: POST /enroll/palm<br/>{user_id, features, images, type}
    Wallet->>Identity: POST /v1/enroll<br/>{user_id, palm_template, type: "left"}

    Identity->>DB: Get tenant config
    DB-->>Identity: {palm_duplicate_check_enabled: true,<br/>duplicate_action: "reject" | "flag"}

    rect rgb(255, 245, 230)
        Note over Identity, Palm: Step 1: Similarity search<br/>(top-5 candidates)
        Identity->>Palm: POST /KZ/query<br/>Headers: request_id: <uuid><br/>Content-Type: multipart/form-data<br/>Body:<br/>- features_rgb.bin (optional)<br/>- features_ir.bin (optional)<br/>- image_rgb.png (required)<br/>- image_ir.png (required)<br/>- metadata: {type: "left",<br/>is_encrypted: false}<br/>(returns up to 5 candidates,<br/>deduped by user_id)

        alt Candidates above threshold returned
            Palm-->>Identity: {code: 0,<br/>data: {results: [up to 5 candidates,<br/>each with user_id, id, scores: [4]],<br/>thresholds: [4]}}

            Identity->>Identity: Filter results where<br/>all 4 scores ≥ thresholds<br/>AND user_id != current user_id

            alt Match found — possible fraud/duplicate
                Identity->>Audit: Log: palm_duplicate_detected<br/>(enrolling_user_id, matched_user_ids,<br/>scores, severity: high)

                alt Tenant config: reject
                    Identity-->>Wallet: 409 Conflict<br/>{error: "duplicate_palm",<br/>message: "Biometrics already registered"}
                    Wallet-->>Kiosk: "Cannot enroll —<br/>contact support"
                else Tenant config: flag for review
                    Identity->>DB: Create review_case<br/>(enrolling_user_id, matched_user_ids,<br/>status: pending)
                    Note over Identity: Continue to /KZ/add<br/>but flag for ops review
                end
            else No conflicting match
                Note over Identity: Safe to enroll —<br/>proceed to the /KZ/add enrollment step (Section 4.4)
            end

        else No candidate match (platform-decided)
            Palm-->>Identity: {code: 0}<br/>(no candidate ≥ thresholds → platform no-match)
            Note over Identity: Clean palm, proceed to enroll
        end
    end

    rect rgb(230, 255, 230)
        Note over Identity, Palm: Step 2: Proceed with enrollment
        Note over Identity: See Section 5.3 for the full /KZ/add<br/>request/response; error codes 30007/30005<br/>in PRD §15
        Identity->>Palm: POST /KZ/add (see Section 4.4)
        Palm-->>Identity: {code: 0, data: {user_id, id, query_type}}
        Identity->>DB: Mark palm_enrolled = true,<br/>store palm_id
        Identity->>Audit: Log: palm_enrolled
        Identity-->>Wallet: {status: "enrolled"}
    end

8.11 Palm Type Restrictions

Per-user restriction on which palm types can verify that user. Useful for high-security tenants and account-suspension scenarios.

Endpoint: PUT /v1/users/{user_id}/palm-restriction (Console session, Tenant Admin or Operator)

Body:

{ "query_type": "left" }

Allowed values for query_type:

Value Behavior
left Only left-palm verifications allowed
right Only right-palm verifications allowed
all Both palms allowed (default after enrollment)
disable All palm verifications blocked without deleting the template (used for soft-suspension)

When a verification or identification call is made and the user's restriction does not allow the submitted palm type, the platform returns 403 Forbidden with error palm_type_restricted (mapped from vendor error code 30008 — see §15).

Audit event: palm_restriction_set (records user_id, query_type, and set_by).

Common patterns: - After enrollment, automatically lock to the enrolled palm type (e.g., left) - On account suspension, set to disable - On reinstatement, restore to original

For the sequence diagram, see Section 4.2 of identity-platform-diagrams.md.

Diagram 4.2 — Lock User to Specific Palm Type

sequenceDiagram
    autonumber
    participant Admin as Tenant Admin
    participant Console as Web Console
    participant Identity as Identity Platform
    participant Palm as X-Telcom BioWave Pass Palm Server
    participant DB as PostgreSQL
    participant Audit as Audit Log

    Note over Admin, Audit: Use cases:<br/>- Lock to specific palm after enrollment<br/>- Suspend palm verification on account hold<br/>- Re-enable on reinstatement

    Admin->>Console: Set palm restriction for user<br/>(e.g., "left only" or "disable")
    Console->>Identity: PUT /v1/users/{user_id}/palm-restriction<br/>Authorization: Bearer <token><br/>{query_type: "left"}

    Identity->>DB: Validate user belongs to tenant
    DB-->>Identity: User found

    Identity->>Palm: POST /KZ/set_query_type<br/>Headers: request_id: <uuid><br/>Content-Type: application/json<br/>Body: {user_id, query_type: "left"}

    alt Success — code: 0
        Palm-->>Identity: {code: 0, msg: "Success"}
        Identity->>DB: Update user.palm_query_type = "left"
        Identity->>Audit: Log: palm_restriction_set<br/>(user_id, query_type: "left",<br/>set_by: admin_id)

        opt Webhook subscribed
            Identity->>Identity: Send webhook: user.palm_restriction_changed<br/>{user_id, query_type}
        end

        Identity-->>Console: {user_id, query_type: "left",<br/>updated_at}
        Console-->>Admin: "Restriction applied"

    else User not found in X-Telcom BioWave Pass — code: 30006
        Palm-->>Identity: {code: 30006,<br/>msg: "DB ID not found"}
        Identity->>Audit: Log: palm_restriction_failed<br/>(user_id, reason: not_enrolled)
        Identity-->>Console: 404 Not Found<br/>"User not enrolled in palm"
    end

    Note over Admin, Audit: Subsequent /v1/device/transactions or /v1/verify<br/>calls with mismatched palm_type<br/>will fail with code 30008<br/>(see Section 4.3 — query type forbidden)

8.12 Operational

8.12.1 Health Monitoring

The platform monitors vendor reachability (POST /KZ/connect) and version (GET /pv/version) on a periodic cadence (default: every 60s). Failed checks trigger a palm_vendor_unhealthy audit event and page the on-call rotation. Version changes trigger palm_vendor_version_change for traceability.

For the sequence diagram, see Section 9.1 of identity-platform-diagrams.md.

Diagram 9.1 — X-Telcom BioWave Pass Health Check

sequenceDiagram
    autonumber
    participant Cron as Health Monitor
    participant Identity as Identity Platform
    participant Palm as X-Telcom BioWave Pass Palm Server
    participant Audit as Audit Log
    participant Alert as Ops Alerting

    Cron->>Identity: Trigger health check<br/>(startup / interval / post-deploy)

    par Connection test
        Identity->>Palm: POST /KZ/connect<br/>Headers: request_id: <uuid><br/>Content-Type: application/json<br/>Body: (none)
        alt Reachable
            Palm-->>Identity: {code: 0, msg: "Success"}
        else Unreachable
            Palm-->>Identity: timeout / network error
        end
    and Version probe
        Identity->>Palm: GET /pv/version<br/>Headers: request_id: <uuid><br/>(no body, response is text/plain)
        alt Reachable
            Palm-->>Identity: "0.0.13" (text/plain)
        else Unreachable
            Palm-->>Identity: timeout / 5xx
        end
    end

    alt All checks passed
        Identity->>Identity: Update health status: healthy
        opt Version changed since last check
            Identity->>Audit: Log: palm_vendor_version_change<br/>(old: "0.0.12", new: "0.0.13")
        end
    else Any check failed
        Identity->>Identity: Update health status: unhealthy
        Identity->>Audit: Log: palm_vendor_unhealthy<br/>(reason, severity: high)
        Identity->>Alert: Page on-call —<br/>X-Telcom BioWave Pass unreachable
    end

8.12.2 Threshold Configuration

Platform Admins can tune the vendor's 4 global thresholds (vendor POST /KZ/set_thresholds) via GET/PUT /v1/admin/palm/thresholds. Used for: - Tightening after a fraud incident - Per-environment calibration (staging vs. production) - Trade-off tuning (false-accept vs. false-reject rate)

Thresholds are global at the vendor side (not per-tenant). Tenant-level relaxation is done through palm_match_policy instead.

Audit event: palm_thresholds_updated (records old and new threshold values).

For the sequence diagram, see Section 9.2 of identity-platform-diagrams.md.

Diagram 9.2 — X-Telcom BioWave Pass Threshold Configuration

sequenceDiagram
    autonumber
    participant Admin as Platform Admin
    participant Console as Web Console
    participant Identity as Identity Platform
    participant Palm as X-Telcom BioWave Pass Palm Server
    participant DB as PostgreSQL
    participant Audit as Audit Log

    Note over Admin: Use cases:<br/>- Tighten thresholds after fraud event<br/>- Loosen for lower-security environment<br/>- Tune per-model variant accuracy

    Admin->>Console: Open palm threshold settings
    Console->>Identity: GET /v1/admin/palm/thresholds<br/>Authorization: Bearer <admin-token>
    Identity->>DB: Get current thresholds (cached)
    DB-->>Identity: thresholds: [4 floats]
    Identity-->>Console: {thresholds: [4],<br/>last_updated_at, updated_by}
    Console-->>Admin: Display current thresholds<br/>(annotated with model variant per index)

    Admin->>Console: Update thresholds<br/>(e.g., bump index 0 from 0.70 → 0.75)
    Console->>Identity: PUT /v1/admin/palm/thresholds<br/>Authorization: Bearer <admin-token><br/>{thresholds: [4 floats]}

    Identity->>Identity: Validate: all values 0.0–1.0,<br/>array length == 4

    Identity->>Palm: POST /KZ/set_thresholds<br/>Headers: request_id: <uuid><br/>Content-Type: application/json<br/>Body: {thresholds: [4 floats]}

    alt Success
        Palm-->>Identity: {code: 0, msg: "Success"}
        Identity->>DB: Persist thresholds<br/>(audit trail)
        Identity->>Audit: Log: palm_thresholds_updated<br/>(updated_by: admin_id,<br/>old_thresholds, new_thresholds)
        Identity-->>Console: {status: "updated",<br/>thresholds, updated_at}
        Console-->>Admin: "Thresholds updated"
    else Validation/permission error
        Palm-->>Identity: {code: 10001, msg: "..."}
        Identity-->>Console: 400 Bad Request
        Console-->>Admin: Error message
    end

8.13 Palm Model (Small vs Large)

The palm match runs in one of two models, chosen once per deployment by a Platform Admin — deployment-wide, not per-tenant or per-user (like the global thresholds in §8.12.2). The two models differ in one thing: who runs the match.

  • Large model — the platform matches. The device sends the scan to the platform, which calls the verification server over /KZ/*, applies palm_match_policy (§8.4), and owns the decision. This is the model the rest of §8 describes.
  • Small model — the device matches. The device's client SDK calls the verification server directly; the platform sits outside the match path. The device reports the result — matched user_id + a pass/fail boolean — to the platform over its mTLS channel (§9.2). The platform records it and, for pos/gate, then authorizes the bound product with that user_id (§10).

Why it's a different topology, not just another vendor. Because the platform matches only in the large model, the PalmVerifier port (§8.3) abstracts the large model only — the small model is not another adapter behind that port. Under the small model the platform has no verification-server connection at all; one is added only to migrate (§8.14).

What each model supports:

Feature Large model Small model
palm_match_policy / 4-element score model (§8.4) platform decides N/A — SDK/server decides; platform records boolean
Pre-enrollment duplicate detection (§8.10) available N/A
Global threshold config (§8.12.2) applies N/A (SDK/server-side)
Palm type restrictions (§8.11) applies N/A in matching path
PalmVerifier-port identify/verify (§8.3) yes no — device SDK direct
Platform role matcher + router recorder + router (device-trusted)

Same endpoints, different payload. The active model decides what every device-facing palm endpoint carries: under the large model the device sends scans (the platform matches); under the small model it sends results (the platform records). This covers /v1/device/transactions, /v1/device/enroll, and the challenge endpoints /v1/verify + /v1/enroll + /v1/challenges/{id}/complete (transaction, enrollment, and verify flows). The platform implements both paths; migration (§8.14) flips which one is live.

Trust trade-off. Small-model results are device-trusted — the platform makes no independent match decision (mTLS authenticates the reporting device, not the match itself). That fits pilot / early-stage scale; the device-trust posture and capacity are the two reasons to move to the large model for production or fraud-sensitive scale.

This section is the canonical definition of the palm model (referenced by §5.7, §10.3, §19.1). Sequence diagram: identity-platform-diagrams.md §4.5.

Diagram 4.5 — Small-Model Device Transaction (Client SDK Direct)

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

    User->>Device: Place palm on scanner
    Device->>Device: Capture palm image (RGB + IR)

    rect rgb(255, 245, 230)
        Note over Device, Palm: Device-trusted — match decided SDK/server-side,<br/>NOT by the platform
        Device->>Palm: SDK 1:N match (direct — no platform on path)
        Palm-->>Device: {user_id, pass/fail, scores}
    end

    alt Match (SDK pass)
        Device->>Identity: POST /v1/device/transactions<br/>(mTLS — device cert)<br/>{user_id, matched: true,<br/>context, idempotency_key}
        Identity->>Identity: Parse SAN URI → device_id, tenant_id;<br/>record result (no /KZ/* call);<br/>skip server-side identify;<br/>sign identity_assertion JWT
        Identity->>Product: POST {base_url}{authorize_path}<br/>Authorization: Bearer <identity_assertion><br/>{user_id, action, context, idempotency_key}
        Product-->>Identity: {decision, display_message,<br/>reference_id, ttl}
        Identity->>Audit: Log: device_transaction<br/>(source: device_reported, decision)
        Identity-->>Device: {decision, display_message,<br/>product_reference} (in-band)
        Device-->>User: Open gate / "Approved" (or decline)
    else No match (SDK fail)
        Device->>Identity: POST /v1/device/transactions<br/>{matched: false, context, idempotency_key}
        Identity->>Audit: Log: device_transaction (not_recognized)
        Identity-->>Device: {decision: "not_recognized"}
        Device-->>User: "Palm not recognized"
    end

Diagram 4.6 — Small-Model Device Enrollment (Client SDK Direct)

sequenceDiagram
    autonumber
    participant User
    participant Device as POS / Kiosk (client SDK)
    participant Identity as Identity Platform
    participant Palm as X-Telcom BioWave Pass (verification 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

    Note over Device: Consent is gated HERE, device-side, BEFORE enrolling —<br/>the platform is off the enroll path under the small model.

    User->>Device: Place palm
    Device->>Device: Capture palm image (RGB + IR)

    rect rgb(255, 245, 230)
        Note over Device, Palm: Device-trusted — enrollment done SDK/server-side,<br/>NOT by the platform
        Device->>Palm: SDK enroll (direct — no platform on path)
        Palm-->>Device: {palm_id, success}
    end

    Device->>Identity: POST /v1/device/enroll (mTLS)<br/>{user_id, enrolled: true}
    Identity->>Identity: Record palm_enrolled (no /KZ/add);<br/>trust device-reported result
    Identity->>Audit: Log: enrollment.complete<br/>(source: device_reported)
    opt Webhook subscribed
        Identity->>Identity: Send webhook: enrollment.complete
    end
    Identity-->>Device: {palm_enrolled: true}
    Device-->>User: "Enrolled — palm now works across Link"

8.14 Small→Large Migration

A Platform Admin migrates a deployment from the small model to the large model. The migration is one-way and reprocesses the already-stored RGB+IR enrollments into large-model representations — no re-enrollment and no user action.

Prerequisite — verification-server endpoint. Under the small model the platform has no connection to the verification server (the device SDK does). Before migrating — and for large-model operation afterward — a Platform Admin configures the deployment's verification-server endpoint (connection URL) so the platform can reach it (GET/PUT /v1/admin/palm/verification-server, §14.2; stored deployment-level, §5.7).

  1. The Admin triggers migration in the Console (POST /v1/admin/palm/migrate, §14.2). Emits palm_model_migration_started.
  2. The platform connects to the configured verification-server endpoint and runs the vendor-supplied migration script against it; the server reprocesses the stored RGB+IR enrollments into large-model representations.
  3. The platform monitors progress (GET /v1/admin/palm/model surfaces migration_status).
  4. On success the platform switches the deployment's palm model small→large. This switch is the platform-side cutover: request handling atomically moves from the small-model record path to the large-model /KZ/* broker path for the whole deployment (in-flight small-model operations complete under the old model). Palm flows then follow the large-model server-API path (§8.9, §10). Emits palm_model_migration_completed and palm_model_changed; failure emits palm_model_migration_failed and leaves the model unchanged.

The migration script is vendor-supplied — the platform runs it against the configured verification-server endpoint; it does not author the reprocessing logic.

For the sequence diagram, see Section 9.3 of identity-platform-diagrams.md.

Diagram 9.3 — Small→Large Model Migration

sequenceDiagram
    autonumber
    participant Admin as Platform Admin
    participant Console as Web Console
    participant Identity as Identity Platform
    participant Palm as X-Telcom BioWave Pass (verification server)
    participant DB as PostgreSQL
    participant Audit as Audit Log

    Note over Admin, Identity: Prerequisite — verification-server endpoint configured<br/>(PUT /v1/admin/palm/verification-server — §13.2)

    Admin->>Console: "Migrate to large model" (confirm)
    Console->>Identity: POST /v1/admin/palm/migrate
    Identity->>Audit: Log: palm_model_migration_started

    rect rgb(255, 245, 230)
        Note over Identity, Palm: Platform runs the vendor-supplied migration script<br/>against the configured verification-server endpoint
        Identity->>Palm: Connect + run migration script<br/>(reprocess stored RGB+IR → large model)
        loop Until complete
            Identity->>Palm: Poll migration progress
            Palm-->>Identity: {status}
        end
    end

    alt Migration succeeded
        rect rgb(230, 255, 230)
            Identity->>DB: Switch deployment palm model small→large
            Identity->>Audit: Log: palm_model_migration_completed + palm_model_changed
            Identity-->>Console: {model: "large", migration_status: "completed"}
            Console-->>Admin: "Migration complete — flows now use the large-model server API (§4.3)"
        end
    else Migration failed
        Identity->>Audit: Log: palm_model_migration_failed (model unchanged)
        Identity-->>Console: 500 {error: migration_failed}
        Console-->>Admin: Error — deployment stays on the small model
    end