Skip to content

5. Data Model

This is the canonical home for every persisted entity. Capability sections (§6–§12) describe behavior and reference these schemas rather than redefining them. Every entity carries tenant_id unless noted (platform-level singletons do not). Where the platform's prior specification never formalized an entity's fields, that is called out as an ⚠ Open Question rather than invented here.

5.1 Entity Catalog

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

5.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 (§5.5) KYC enabled
kyc_data Verified KYC data (§5.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 enrollment is a separate lifecycle. A user record is created by mobile number — either at a device in signup mode (§13) or in a product app/dashboard. palm_enrolled is independent of that: it flips to true only when a palm is captured at a physical device (pos/kiosk) — never in an app. Every surface (wallet app, access dashboard) reads palm_enrolled to prompt an un-enrolled user to "enroll at a POS/kiosk." Within the Link Holdings tenant, one record + palm serves all products (§4.6).

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)

5.3 Authentication & Clients

oauth_clients (integrator backend credentials; lifecycle in §6.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 §12). Each endpoint owns its own signing secret, separate from OAuth client secrets (§6.5). Fields (see §12.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 (§16)
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; rotated on each refresh (§6.10); 30-day retention (§11.4); stored in the Redis token store (§20). ⚠ Open Question: the persisted field schema (token hash, user_id, issued_at, rotated_from, revocation flag) is not formalized in this PRD.

5.4 Devices & Transactions

devices (Identity-managed hardware; lifecycle in §9.1–§9.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 (§9.1)
bound_product product_key this device routes to — pos/gate/kiosk only; null for personal_scanner (§9.3)
bound_action Action the bound product authorizes (e.g. pay, entry) — non-scanner only (§9.3)
status pending_pairing | paired | revoked (§9.2.1)
pairing_code_hash Hashed pairing code, 5-min TTL, single-use (§9.2.1)
cert_fingerprint Current client-cert fingerprint (revocation checks against the denylist)
device_info {model, firmware, serial, hardware_id} captured at pairing (§9.2)
created_at Registration timestamp

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

Field Description
challenge_id Unique challenge identifier
tenant_id Owning tenant
user_id Subject user (the challenge carries the claimed identity)
device_id Target device that polls for it
type verify or enroll
status pending / completed / expired
context Passthrough metadata echoed to webhooks (e.g. document_id)
created_at Creation timestamp

device_transactions (append-only log of every device-initiated transaction — §10). Retained 10 years (SAMA — §11.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 — §10):

Field Description
id Surrogate id used by the management API path (/v1/linked-services/{id}, §14.1)
tenant_id Owning tenant
product_key Stable id for the product (e.g. wallet, access). Devices bind to it via bound_product (§9.3); it is the aud of the identity_assertion (§10).
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 (§10.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 (§10.7).

5.5 Compliance

consent_records (consent grants/withdrawals — §11.1; see also the consent events, §16). Retained 5 years (§11.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 §16 (Event Reference). Retained 10 years (SAMA — §11.4):

Field Description
event_id Unique event identifier
event_type Event name (catalog: §16)
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 (§16)

kyc_data (hooks). The users.kyc_status ∈ {none, pending, verified, failed, expired} and users.kyc_data fields carry KYC state so palm-enrollment gating (§13.2) and the Web Console can read it. The detailed verified_data schema, KYC sessions, and results are deferred — KYC integration is [POST-MVP] (§7).

review_case — opened (instead of rejecting) when a tenant's palm_duplicate_action is flag and pre-enrollment duplicate detection finds a possible duplicate (§8.10); 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.

5.6 Platform Infrastructure

platform_secrets — singleton-by-kind rows storing the backend's server TLS cert and the platform CA (§9.2.1, §9.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 (§9.2.2)

Database engine encryption-at-rest (RDS, Cloud SQL, on-prem Postgres TDE) protects the data layer; no app-level encryption, no env vars, no external secret manager. The app loads both rows into memory at startup — the platform CA private key never touches disk.

5.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.

┌─────────────────────────┐     ┌─────────────────────────┐
│  Link Identity Backend  │     │  Verification Server    │
│  (Link-owned)           │     │  (via PalmVerifier Port) │
│                         │     │                         │
│  - user_id              │────►│  - palm_template        │
│  - identities           │     │  - user_id reference    │
│  - password_hash        │     │                         │
│  - refresh_tokens       │     │  Vendor adapters:       │
│  - profile              │     │  - X-Telcom BioWave Pass│
│  - kyc_data             │     │    (sole vendor; via    │
│  - consent records      │     │     PalmVerifier port)  │
│  - client registry      │     │                         │
│  - device registry      │     │                         │
│  - platform_secrets     │     │                         │
│  - audit logs           │     │                         │
└─────────────────────────┘     └─────────────────────────┘

Palm namespace. Palm templates are tenant-scoped — the user_id sent to the vendor is namespace-prefixed with <tenant_slug>__ (§4.7). Within a tenant the namespace is shared across products, so Link Holdings' Wallet and Access resolve the same enrollment.

Palm model (deployment-wide). The active palm model (small/large — defined in §8.13) and the §8.14 verification-server endpoint are deployment-level settings — not per-tenant, not per-user — stored once at the platform level alongside the §8.12.2 global thresholds. Migration (§8.14) transitions the whole deployment. Under the small model devices' client SDKs reach the verification server directly, so the PalmVerifier-port integration is the large-model path.

Diagram 1.3 — 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