Skip to content

3. Data Model

The entities Link Access owns, all keyed to Identity's shared user.

3.1 What Access owns

Identity is the system of record for the person — the user (user_id, tenant_id, mobile, palm_enrolled) and the tenant. Access stores none of that. It owns its business data — memberships, zones, grants, balances, attendance — and keys every row to Identity's user_id and tenant_id. There is no user table in Access, and palm templates never leave the Verification Server.

USER + TENANT is Identity's — shown dashed, with no arrows. It has none because it would connect to every box; instead, every Access row simply carries user_id + tenant_id as a reference. Access copies none of the person's data.

flowchart LR
  subgraph IDP["Identity Platform (external)"]
    USER["USER + TENANT<br/>Access references these by id"]
  end
  subgraph People
    MEMBERSHIP
    RELATIONSHIP
  end
  subgraph Where
    ZONE
    ZONEDEVICE
  end
  subgraph Entitlement
    PRODUCT
    GRANT
  end
  subgraph Money
    TOPUP
    TRANSACTION
    POSTING
    ACCOUNT
  end
  subgraph Records
    ATTENDANCE
  end
  ZONE -->|nests| ZONE
  ZONE --> ZONEDEVICE
  PRODUCT -->|issues| GRANT
  GRANT -->|scopes| ZONE
  ATTENDANCE -->|at| ZONE
  TOPUP -->|creates on paid| TRANSACTION
  TRANSACTION -->|has 2+| POSTING
  POSTING -->|hits| ACCOUNT
  classDef ext fill:#ede9fe,stroke:#7c3aed,stroke-dasharray: 5 5
  class USER ext

How to read this map. Identity owns the person; Access owns everything else, each row tagged with that person's id.

  • People — who you are to a venue: your role (Membership) and links to others (Relationship — e.g. parent↔child).
  • Where — the places: a Zone is a named spot (a gate, a classroom); ZoneDevice maps each physical scanner to its zone.
  • Entitlement — your right to enter: buying a Product (a sellable pass) gives you a Grant, which says which Zone you can go into, and when.
  • Money — your balance: you load funds with a card (a TopUp) and spend them by palm. Each time money moves — card → your balance, or your balance → the venue — the system writes down both ends, so the totals always add up and nothing can go missing. The tables that do this (Transaction, Posting, Account) are below.
  • Records — what happened: an Attendance row logs a scan at a zone.

Two chains carry the weight: Product → Grant → Zone (what lets you in) and TopUp → Transaction → Posting → Account (how money moves). Everything else hangs off your Identity user_id.

For the Parks MVP, RELATIONSHIP and ATTENDANCE are unused (see tags) — the rest is the first build. One-to-many detail lives in the field tables, via the foreign keys.

3.2 People

Access assigns roles and links members; the person still lives in Identity.

Membership — a user's role within a tenant.

Field Type Notes
membership_id id
tenant_id id Identity tenant
user_id id Identity user
role enum admin, operator, cashier, attendant, member, student, parent, employee, visitor
status enum active, suspended

Relationship — links two users. [POST-MVP for Parks]

Field Type Notes
relationship_id id
tenant_id id
from_user_id id e.g. the guardian
to_user_id id e.g. the child
type enum guardian, group

3.3 Where

Zone — a named policy target; devices are assigned to it, grants point at it.

Field Type Notes
zone_id id
tenant_id id
name string "Park Entry", "Class 5A"
parent_zone_id id? nesting; null = top level
effects array what allow does: open, attendance, notify
schedule object? when active (hours / term); null = always
status enum active, inactive

Pay is zone-agnostic — a till (bound_action = pay) goes straight to the ledger, so effects only cover access and attendance.

ZoneDevice — Access's map from an Identity device to a zone.

Field Type Notes
tenant_id id
device_id id the Identity device (device_id from the authorize call)
zone_id id

3.4 Entitlement

Grant — the right to enter a zone for a window. A pass is a purchased grant.

Field Type Notes
grant_id id
tenant_id id
user_id id
zone_id id covers this zone and, if nested, its children
valid_from / valid_until timestamp day vs season pass = different windows
source enum purchase, sis, hr, admin
product_id id? set when source = purchase
status enum active, revoked, expired

Product — a sellable pass type (the store's SKU).

Field Type Notes
product_id id
tenant_id id
name string "Day Pass", "Season Pass"
price / currency money
grants_zone_id id zone the pass grants
grants_validity duration e.g. 1 day, 90 days

Buying a product issues a Grant.

3.5 Money (double-entry)

The ledger is double-entry and append-only: every movement is a Transaction made of Posting lines that sum to zero. A balance is the cached sum of an account's postings.

Account — a bucket money sits in.

Field Type Notes
account_id id
tenant_id id
type enum user_balance, gateway_clearing, revenue
user_id id? set for user_balance; null for system accounts
currency string SAR
cached_balance money derived (sum of postings), cached for reads
status enum active, frozen

Transaction — a business event.

Field Type Notes
transaction_id id
tenant_id id
type enum topup, spend, refund
amount / currency money
status enum pending, completed, failed, reversed
idempotency_key string dedupes device retries + webhook replays
external_ref string? Moyasar payment id, or device transaction id

Posting — one debit/credit line of a transaction.

Field Type Notes
posting_id id
transaction_id id
account_id id
direction enum debit, credit
amount money

Value flows — each is one transaction, two postings:

Event Value moves
Top-up gateway_clearing → user_balance
Spend user_balance → revenue
Refund user_balance → gateway_clearing

TopUp — the gateway payment behind a top-up.

Field Type Notes
topup_id id
tenant_id / user_id id
amount / currency money
gateway enum moyasar (later stripe)
gateway_payment_id string
status enum pending, paid, failed
transaction_id id? the ledger transaction, posted on paid

A top-up starts pending; only Moyasar's confirmed webhook posts the ledger credit, idempotent on gateway_payment_id.

3.6 Records

Attendance — a scan recorded at an attendance zone. [POST-MVP for Parks]

Field Type Notes
attendance_id id
tenant_id / user_id id
zone_id id
at timestamp
kind enum check_in, check_out