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_enrolledis a separate lifecycle. A user record is created by mobile number (device signup mode — §7.4 — or a product app/dashboard).palm_enrolledflips totrueonly 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