Skip to content

13. Data Model

Every entity carries tenant_id unless noted (platform-level singletons do not). Fields the prior spec never formalized are flagged ⚠ Open Question rather than invented.

13.1 Entity Catalog

Entity Purpose Key / Scope Defined in
tenant A customer/integrator boundary tenant_id (slug) §13.2
users End-user account (managed identity) user_id + tenant_id; mobile is the natural key §13.2
profile End-user profile data (sub-object of users) §13.2
console_users Platform/tenant operators (separate domain — §3.2) id + role (+ tenant_id) §13.2
oauth_clients Integrator backend credentials client_id + tenant_id §13.3
refresh_tokens End-user refresh tokens per user §13.3
webhook_endpoints Outbound webhook subscriptions + signing secret id + tenant_id (or platform scope) §13.3
devices Identity-managed hardware (scanner/pos/gate/kiosk) device_id + tenant_id §13.4
challenges Integrator-initiated verify/enroll jobs (personal scanner) challenge_id + tenant_id §13.4
device_transactions Append-only log of device-initiated transactions per tenant/device §13.4
linked_services Per-tenant product-backend registry for the broker id; unique per tenant_id + product_key §13.4
consent_records Consent grants/withdrawals per user §13.5
audit_log All business events per tenant §13.5
kyc_data KYC state/result on a user (hooks; [POST-MVP] detail — §6) sub-object of users §13.5
review_case Ops fraud-review case opened when duplicate detection is set to flag (§7.9) per tenant §13.5
platform_secrets Server TLS cert + platform CA (platform singleton) by kind §13.6

13.2 Tenancy & Identity

tenant (provisioning lifecycle in §4.3–4.4):

Field Description
tenant_id Unique identifier (slug format)
name Display name
status provisioning, active, suspended, deactivating, deleted
region Deployment region (e.g., KSA, UAE)
config Tenant configuration (the §4.5 settings)
created_at Creation timestamp
created_by Platform Admin who provisioned

users (the unified end-user table). All fields are available to all tenants; each is populated based on which capabilities the tenant has enabled.

Field Description Populated When
user_id Unique user identifier Always — platform-generated or provided via POST /v1/users
tenant_id Tenant this user belongs to Always
mobile Mobile number Optional
mobile_verified Whether mobile is verified Auth enabled
email Email address Optional
email_verified Whether email is verified Auth enabled
password_hash Hashed password Password auth enabled
google_id Google OAuth ID Google auth enabled
apple_id Apple OAuth ID Apple auth enabled
profile User profile data (JSON — see below) Auth enabled
kyc_status KYC verification status (§13.5) KYC enabled
kyc_data Verified KYC data (§13.5) KYC enabled
status active / suspended / deleted Always
palm_enrolled Whether palm template exists Always
created_at Account creation timestamp Always
enrolled_at Palm enrollment timestamp Palm enrolled

Mobile is the account key; palm_enrolled is a separate lifecycle. A user record is created by mobile number (device signup mode§7.4 — or a product app/dashboard). palm_enrolled flips to true only when a palm is captured at a physical device (pos/kiosk), never in an app.

profile (JSON sub-object of users):

{
  "name": "string",
  "name_ar": "string",
  "date_of_birth": "date",
  "nationality": "string",
  "address": "string",
  "national_id": "string",
  "avatar_url": "string",
  "custom_fields": {}
}

console_users (separate identity domain — §3.2 / §3.6; never overlaps users):

Field Description
id Unique console-user identifier
email Login email
password_hash bcrypt hash
role platform_admin, tenant_admin, or tenant_operator (§3.3)
tenant_id Owning tenant (null for platform_admin)
must_change_password Forces a password reset on next login (set for the env-seeded first admin — §3.6)
mfa_enabled MFA required for Platform/Tenant Admins (§3.6)

13.3 Authentication & Clients

oauth_clients (integrator backend credentials; lifecycle in §5.4):

Field Type Description
client_id string Unique identifier (UUID, platform-generated)
tenant_id string Owning tenant
name string Human-readable label (e.g., "Wallet Backend", "Analytics Pipeline")
grant_types string[] Allowed grant types (default: ["client_credentials"])
status string active or revoked
secret_hash string bcrypt hash of the current secret
created_at timestamp Creation time
created_by string Console user who created the client
last_rotated_at timestamp Last time the secret was regenerated
revoked_at timestamp When the client was revoked (if applicable)

webhook_endpoints (outbound subscriptions; behavior in §11). Each endpoint owns its own signing secret (§5.5). Fields (see §11.6):

Field Description
id Endpoint identifier
tenant_id Owning tenant (omitted/platform-level when scope = platform)
url Destination URL (POST target)
events Subscribed event types (§15)
status active / inactive
scope tenant (default) or platform (Platform Admin only — receives platform.* events)
signing_secret_hash bcrypt hash of the HMAC-SHA256 signing secret (plaintext shown once)

refresh_tokens — issued to end users; stored in Redis; rotated on refresh (§5.10), 30-day retention (§10.4). ⚠ Open Question: persisted field schema not yet formalized.

13.4 Devices & Transactions

devices (Identity-managed hardware; lifecycle in §8.1§8.3):

Field Description
device_id Unique device identifier (cert Subject CN)
tenant_id Owning tenant (cert SAN URI)
device_class personal_scanner | pos | gate | kiosk (§8.1)
bound_product product_key this device routes to — pos/gate/kiosk only; null for personal_scanner (§8.3)
bound_action Action the bound product authorizes (e.g. pay, entry) — non-scanner only (§8.3)
status pending_pairing | paired | revoked (§8.2.1)
pairing_code_hash Hashed pairing code, 5-min TTL, single-use (§8.2.1)
cert_fingerprint Current client-cert fingerprint (revocation checks against the denylist)
device_info {model, firmware, serial, hardware_id} captured at pairing (§8.2)
created_at Registration timestamp

challenges (integrator-initiated verify/enroll jobs for personal scanners; behavior in §8.2; see also the challenge_created event, §15):

Field Description
challenge_id Unique challenge identifier
tenant_id Owning tenant
user_id Subject user, internal — resolved from the integrator's external_user_id at creation (the challenge carries the claimed identity)
device_id Target device that polls for it, internal — resolved from the integrator's hardware_id
type verify or enroll
status pending / completed / expired
metadata Passthrough echoed to the integrator webhook (e.g. document_id, document_hash)
created_at Creation timestamp

device_transactions (append-only log of every device-initiated transaction — §9). Retained 10 years (SAMA — §10.4):

Field Description
tenant_id Owning tenant
device_id Device that initiated
user_id Identified user (nullable — null on not_recognized)
product_key Bound product the broker called
action Bound action (e.g. pay, entry)
context Request context, redacted (e.g. amount/currency)
identify_result Match outcome / scores summary
decision allow / deny / not_recognized / fail-mode fallback
product_reference Product's reference id from authorize
idempotency_key De-dupes device retries
latencies Per-stage latency (identify, authorize)
created_at Timestamp

linked_services (per-tenant product-backend registry for the broker — §9):

Field Description
id Surrogate id used by the management API path (/v1/linked-services/{id}, §12.1)
tenant_id Owning tenant
product_key Stable id for the product (e.g. wallet, access). Devices bind to it via bound_product (§8.3); it is the aud of the identity_assertion (§9).
base_url Product backend base URL
authorize_path Path of the product's authorize endpoint (appended to base_url)
health_path Path of the product's health endpoint (appended to base_url), probed periodically (§9.7)
timeout_ms Deadline for the authorize call (default 800)
fail_mode closed (default — deny on timeout/open circuit) or open (allow)
status active / disabled

health_status (healthy / unhealthy / unknown) and last_health_check_at are platform-tracked (not admin-set) and surfaced via the API / console (§9.7).

13.5 Compliance

consent_records (consent grants/withdrawals — §10.1; see also the consent events, §15). Retained 5 years (§10.4):

Field Description
consent_id Unique consent record id
tenant_id Owning tenant
user_id Subject user
consent_type The consent purpose/type
version Consent text version granted
purposes Purposes covered
status granted / withdrawn
granted_at Grant timestamp
withdrawn_at Withdrawal timestamp (if withdrawn)

audit_log — common fields below; the full event-type catalog is in §15 (Event Reference). Retained 10 years (SAMA — §10.4):

Field Description
event_id Unique event identifier
event_type Event name (catalog: §15)
timestamp ISO 8601 timestamp
tenant_id Tenant that triggered the event
actor Who performed the action: {type: "user"|"client"|"system", id: "..."}
ip_address Source IP address
user_agent Client user agent string
result success or failure
metadata Event-specific fields (§15)

kyc_data (hooks). users.kyc_status ∈ {none, pending, verified, failed, expired} plus users.kyc_data, read by enrollment gating (§7.4.3) and the Web Console. Detailed verified_data schema deferred — KYC is [POST-MVP] (§6).

review_case — opened (instead of rejecting) when a tenant's palm_duplicate_action is flag and pre-enrollment duplicate detection finds a possible duplicate (§7.9); enrollment proceeds, but the case is queued for ops/fraud investigation. It captures the same signals as the palm_duplicate_detected event (enrolling_user_id, matched_user_ids, scores) plus a review status. ⚠ Open Question: the full persisted field schema is not yet specified.

13.6 Platform Infrastructure

platform_secrets — singleton-by-kind rows storing the backend's server TLS cert and the platform CA (§8.2.1, §8.2.2):

Field Description
kind server_tls or platform_ca (one row each)
cert_pem PEM-encoded certificate
private_key_pem PEM-encoded private key (CA private key never touches disk after load)
uploaded_by Console user who uploaded/generated it
uploaded_at Timestamp
fingerprint Cert fingerprint
last_alert_threshold_days Last expiry-alert threshold fired, for idempotent alerting (§8.2.2)

Secrets rely on database encryption-at-rest (RDS / Cloud SQL / on-prem TDE) — no app-level encryption or external secret manager. The app loads both rows into memory at startup; the platform CA private key never touches disk.

13.7 Data Separation & Namespacing

The Link Identity backend owns identity, auth, device, and compliance data. Palm templates live only in the Verification Server, reached through the PalmVerifier port — they are never stored in or exposed by the Identity backend.

flowchart LR
    subgraph Backend["Link Identity Backend (Link-owned)"]
        B["user_id<br/>identities<br/>password_hash<br/>refresh_tokens<br/>profile<br/>kyc_data<br/>consent records<br/>client registry<br/>device registry<br/>platform_secrets<br/>audit logs"]
    end
    subgraph VS["Verification Server (via PalmVerifier port)"]
        V["palm_template<br/>user_id reference<br/>Vendor adapter: X-Telcom BioWave Pass"]
    end
    B -->|user_id reference| V

Palm namespace. Tenant-scoped — the user_id sent to the vendor is prefixed with <tenant_slug>__ (§4.7); shared across products within a tenant.

Palm model (deployment-wide). The active palm model (small/large, §7.10) and verification-server endpoint are deployment-level — not per-tenant or per-user — stored alongside the global thresholds (§7.14.2). Migration (§7.11) transitions the whole deployment.

Diagram — Data architecture

flowchart LR
    subgraph Link Identity Backend
        subgraph PostgreSQL
            USERS[(users<br/>user_id, tenant_id,<br/>mobile, status,<br/>palm_enrolled, profile)]
            IDENT[(identities<br/>mobile, email,<br/>google_id, apple_id)]
            KYC[(kyc_records<br/>status, provider,<br/>verified_data)]
            CONSENT[(consent_records<br/>type, version,<br/>granted_at, evidence)]
            CLIENTS[(clients<br/>client_id,<br/>secret_hash)]
            LINKED[(linked_services<br/>product_key, base_url,<br/>authorize_path, fail_mode)]
            DEVICES[(devices<br/>device_id, tenant_id,<br/>device_class, bound_product,<br/>cert_fingerprint, status)]
            DEVTXN[(device_transactions<br/>device_id, user_id?,<br/>decision, idempotency_key,<br/>retention: 10 years)]
            AUDIT[(audit_logs<br/>event, timestamp,<br/>actor, details,<br/>retention: 10 years)]
        end
        subgraph Redis
            TOKENS[(refresh_tokens)]
            OTP[(otp_codes)]
            CHALLENGES[(challenges)]
            SESSIONS[(rate_limits)]
        end
    end

    subgraph Palm Verification Server
        subgraph X-Telcom BioWave Pass
            PALM[(palm_templates<br/>user_id reference,<br/>features_rgb,<br/>features_ir)]
        end
    end

    USERS --> IDENT
    USERS --> KYC
    USERS --> CONSENT
    USERS -.->|user_id reference| PALM