Domain Model
The tenant-scoped entity model — core, catalog and customers, money, presence and content, ads, sites, platform — and the fixed rules every table obeys.
03 — Domain model
Every tenant-scoped entity carries the tenant id. Identifiers are ULIDs. Timestamps are stored in UTC and displayed in Asia/Riyadh.
0. Implementation status on supastarter
Tenant= the Better AuthOrganization. The Prisma modelOrganizationis extended withcrNumber(unique),vatNumber,sector,phone,city, andcountry(defaultSA). The conceptualtenant_idbelow isorganizationIdin code.Locationexists now inpackages/database/prisma/schema.prisma(id, organizationId, name, addressLine, city, lat, lng, phone, whatsapp, hours, isPrimary, createdAt, updatedAt) and is mirrored in the Drizzle Postgres schema.UserandMembershipare the Better AuthUser(withphoneNumber) andMembermodels. Roles are Better Auth organization roles evaluated through Permix.Subscription/ plans come frompackages/paymentsand are attached to the organization.- Everything else in this document is the target model; tables are added to
packages/databaseas the modules that need them land. Prisma models use camelCase; the snake_case names below are the conceptual names.
1. Core
Tenant ──< Location (business → branches)
Tenant ──< Membership >── User (roles: owner / manager / staff / accountant)
Tenant ──1 BrandKit
Tenant ──1 Subscription ──< Entitlement
Tenant ──< Installation >── AddOn (add-ons installed from the Marketplace)
Tenant ──1 SectorProfile (sector_id + overrides)| Entity | Key fields |
|---|---|
tenants (Organization) | id, legal_name, trade_name, cr_number, vat_number, sector_id, status, wathq_snapshot (jsonb), created_at |
locations | id, tenant_id, name, address (jsonb), lat, lng, phone, whatsapp, hours (jsonb), gbp_location_id? |
users | id, phone, name, locale, last_login_at |
memberships | tenant_id, user_id, role, location_ids[] |
brand_kits | tenant_id, logo_asset_id, palette (jsonb, OKLCH), fonts (jsonb), tone, tagline |
subscriptions | tenant_id, plan_id, status, period_start, period_end, psp_customer_ref |
entitlements | tenant_id, key, value, source (plan / addon / trial / manual) |
2. Catalog and customers
Tenant ──< Category ──< Item ──< Variant
Item ──< ItemAvailability >── Location
Tenant ──< Customer ──< Consent
Customer ──< Interaction (visit, payment, review, message)| Entity | Fields |
|---|---|
items | id, tenant_id, category_id, kind (product / service), name_ar, name_en, description, price_incl_vat, vat_rate, duration_min?, images[], attributes (jsonb, by sector) |
customers | id, tenant_id, phone_hash, phone_enc, name, tags[], first_seen_at, ltv |
consents | customer_id, channel (sms / whatsapp / email / ads_audience), granted, source, at |
3. Money
Tenant ──< Order ──< OrderLine
Order ──< Payment (method: mada / card / applepay / stcpay / tabby / tamara)
Payment ──< Refund
Order ──1 Invoice (ZATCA simplified, QR TLV, xml_hash, status)
Tenant ──< PaymentLink (amount or open, expires, qr_asset)
Tenant ──1 PspAccount (sub-merchant refs, enabled methods, bnpl_status per provider)payments.psp_refis unique; webhooks are idempotent onpsp_event_id.- There is no platform "balance" table. Settlement happens at the PSP in the merchant's name.
4. Presence and content
Location ──1 Listing (gbp) ──< Review ──1? ReviewReply
Tenant ──< SocialAccount (ig / x / snap / tiktok / gbp) ──< Post ──< PostPublication
Tenant ──< Asset ; Tenant ──< Template (design)5. Ads
Tenant ──< AdAccount (google / snap / tiktok / meta, oauth_token_enc)
AdAccount ──< Campaign (template_id, objective, budget, status) ──< Creative
Campaign ──< SpendSnapshot (daily)
Tenant ──< Audience (hashed customers with consent)6. Sites
Tenant ──1..n Site (theme_id, sector_template, settings) ──< Page (blocks jsonb)
Site ──< Domain (hostname, kind: subdomain / custom, cf_hostname_id, status)7. Platform
Plan ──< PlanPrice ; AddOn ──< AddOnPrice
Outbox (event_id, type, payload, tenant_id, published_at)
AuditLog (actor, action, target, tenant_id, at, ip)
AiCall (tenant_id, tool, tokens_in, tokens_out, cost_sar, at)
Job (queue, key, status, attempts)8. Fixed rules
- No hard deletes for money or audit entities;
deleted_atonly. - Phone numbers are stored encrypted plus a hash for lookup; the full number is shown only to authorised roles.
- Every price is stored VAT-inclusive in halalas (integer) together with
vat_rate. - Any entity a module creates must be defined in
packages/database, never inside the module (prevents fragmentation).
Architecture
Modular monolith on supastarter-nextjs — the real monorepo map, core technical decisions, multi-tenancy, custom domains, the outbox event bus, the module contract, security, and environments.
Business Model
Revenue channels, plans priced in SAR, the revenue model at 1,000 paying merchants, variable costs, and the revenue impact of each module — all figures are modelling assumptions with stated confidence.