Customer, party and user schema¶
Why this schema is load-bearing¶
This is not a nice-to-have data model refinement. Getting the party model wrong makes the following structurally impossible to fix after launch:
- Depositor Compensation Scheme (DCS) — the RBNZ Deposit Takers Act 2023 requires a Single Depositor View (SDV) producible within 24 hours, at the depositor level, including joint accounts, trustee holdings, and authorised individuals. A
customer_idcolumn on an account cannot express this. - AML/CFT — the Anti-Money Laundering and Countering Financing of Terrorism Act 2009 requires separate identification of the customer, beneficial owners, and persons acting on behalf. Beneficial ownership chains (>25% threshold) require a graph, not a flag.
- CRS/FATCA — Inland Revenue's CRS guidance distinguishes account holders from controlling persons and requires trace-through for nominees, agents, and trusts.
- Expense intelligence — the sole trader, property investor, and freelancer capabilities require a party model that correctly represents a natural person acting in multiple economic capacities without inventing separate legal entities.
Getting this wrong is expensive to fix. The schema is designed to be correct once and extended safely, not rebuilt when the first trust account or nominee arrangement appears.
The four-layer model¶
| Layer | What it represents | Where it lives | Key tables |
|---|---|---|---|
| 1. Party | A real-world legal subject — person or organisation | SD02 kyc database |
party.parties, party.party_identifiers, party.person_profiles, party.organisation_profiles, party.party_roles |
| 2. Banking relationship | The bank's served customer relationship | SD02 kyc database |
banking.customer_relationships, regulatory.party_regulatory_profiles |
| 3. Operating context | A non-legal economic unit (sole trader activity, rental property) | SD01 core database |
contexts.operating_contexts, contexts.operating_context_memberships, contexts.account_context_links, contexts.tax_profiles |
| 4. User / access | An authenticated digital identity and their permissions | SD08 app database |
access.user_identities, access.access_grants |
Table definitions are in the authoritative data model pages: - Layer 1 & 2: SD02 data model - Layer 3: SD01 data model - Layer 4: SD08 data model
Design principles¶
Six rules (lock these in)¶
-
A sole trader is not a separate legal party. A sole trader activity is an Operating_Context linked to the natural person's Party record. Never create a second Party for the same person in a different economic role.
-
A property cannot be an account holder. An asset is owned by a legal party (person, company, trust). The account is held by the legal owner. The property is an Operating_Context for reporting and tax attribution.
-
User_Identity is not Customer and not Party. A user is a digital login. A customer is a banking relationship. A party is a legal subject. One person has one
user_idand oneparty_id, but may have multiple customer relationships and multiple access grants. -
Roles are relationships, not flags. DIRECTOR, TREASURER, BENEFICIAL_OWNER, AUTHORISED_SIGNATORY are rows in
party.party_roles, not boolean columns on the party. Adding a flag column is always the wrong shortcut — it cannot express the second beneficial owner, the succession of directors, or the revocation of a mandate. -
Derive regulatory views from the model; do not duplicate. DCS, AML, and CRS views are queries over the canonical tables. There is no separate "DCS customer" table or "CRS reportable account" table. They drift the moment they are created.
-
Access and ownership are different. An account user can have access without being an account holder. The charity treasurer has operational access to the charity's accounts but is not the depositor. The
access.access_grantstable expresses access;accounts.account_party_relationshipsexpresses ownership.
Cross-domain FK convention¶
The party model spans three Neon databases (SD02 kyc, SD01 core, SD08 app). Cross-database Postgres FKs are not possible. The convention is:
- UUIDs are generated in SD02
party.partiesand treated as stable cross-domain identifiers - Application code enforces referential integrity at write time
- Each data model page documents which columns are cross-domain UUID references
Schema ownership map¶
| Schema | Database | Owner system | Purpose |
|---|---|---|---|
party.* |
kyc (SD02) | KYC Platform | Legal identity, identifiers, profiles, inter-party roles |
banking.* |
kyc (SD02) | KYC Platform | Customer relationships, KYC/AML classification |
kyc.* |
kyc (SD02) | KYC Platform | KYC checks, documents, sanctions, reviews (existing) |
regulatory.* |
kyc (SD02) | KYC Platform | DCS, CRS, FATCA classifications per party |
contexts.* |
core (SD01) | Core Banking | Operating contexts, account-context links, tax profiles |
assets.* |
core (SD01) | Core Banking | External assets (KiwiSaver, property, investments) |
accounts.* |
core (SD01) | Core Banking | Accounts, balances, account-party relationships |
access.* |
app (SD08) | App Platform | User identities, access grants, sessions |
Worked examples¶
A — Personal customer¶
Party: [NATURAL_PERSON] Ross
└─ banking.customer_relationships: PERSONAL
└─ party.party_identifiers: IRD_NO, PASSPORT
└─ party.person_profiles: DOB, address, tax residency NZ
└─ regulatory.party_regulatory_profiles: dcs_eligible=true, dcs_depositor_category=INDIVIDUAL
SD08:
└─ access.user_identities: user_id → person_party_id=Ross
SD01:
Account: Savings (NZD)
└─ accounts.account_party_relationships: ACCOUNT_HOLDER → Ross party
└─ contexts.account_context_links → PERSONAL operating context
B — Sole trader (same natural person)¶
Party: [NATURAL_PERSON] Ross ← SAME party as A
SD01:
contexts.operating_contexts: "Ross Consulting" [SOLE_TRADER_BUSINESS]
└─ primary_party_id → Ross party
└─ legal_party_id → NULL (no separate legal entity)
└─ contexts.tax_profiles: GST registered, 2-monthly, invoice basis
Account: Business Transaction (NZD)
└─ accounts.account_party_relationships: ACCOUNT_HOLDER → Ross party (not a different entity)
└─ contexts.account_context_links → "Ross Consulting" context
Ross has one party_id, one user_id, and two operating contexts. The DCS SDV sees one depositor with two accounts. AML sees one customer with PERSONAL and BUSINESS relationships.
C — Multiple rental properties¶
SD01:
assets.assets: "12 Smith St" [PROPERTY, RENTAL_RESIDENTIAL]
└─ assets.asset_party_relationships: LEGAL_OWNER → Ross party
└─ assets.property_contexts → contexts.operating_contexts "12 Smith St Investment"
└─ contexts.tax_profiles: ring_fencing_flag=true, NZ
assets.assets: "24 Oak Ave" [PROPERTY, RENTAL_RESIDENTIAL]
└─ assets.asset_party_relationships: LEGAL_OWNER → Ross party
└─ assets.property_contexts → contexts.operating_contexts "24 Oak Ave Investment"
└─ ring_fencing_group_id = [same group as 12 Smith St — portfolio offset]
D — Ltd company director¶
SD02:
Party: [ORGANISATION] Acme Holdings Ltd
└─ banking.customer_relationships: BUSINESS
└─ party.organisation_profiles: LIMITED_COMPANY
└─ party.party_identifiers: NZ_COMPANY_NO, IRD_NO
party.party_roles: Ross → Acme Holdings Ltd
└─ role_type: DIRECTOR
└─ role_type: BENEFICIAL_OWNER (ownership_pct: 100)
SD08:
access.access_grants: user_id=Ross → scope_type=PARTY, scope_id=Acme Holdings party
└─ access_role: OWNER
SD01:
Account: Acme Holdings business account
└─ accounts.account_party_relationships: ACCOUNT_HOLDER → Acme Holdings party
└─ accounts.account_party_relationships: AUTHORISED_SIGNATORY → Ross party
E — Charity treasurer¶
SD02:
Party: [ORGANISATION] Wellington Community Trust
└─ banking.customer_relationships: BUSINESS
└─ party.organisation_profiles: CHARITY
└─ party.party_identifiers: NZ_CHARITIES_NO
party.party_roles: Ross → Wellington Community Trust
└─ role_type: TREASURER
└─ source_of_authority: "Trustee board resolution 2025-03-01"
SD08:
access.access_grants: user_id=Ross → scope_type=PARTY, scope_id=WCT party
└─ access_role: TREASURER
└─ permissions_mask: {can_view_balances: true, can_initiate_payments: true, can_export_reports: true}
SD01:
Account: WCT Operating account
└─ accounts.account_party_relationships: ACCOUNT_HOLDER → Wellington Community Trust party
← Ross is NOT the depositor. Ross has operational access only.
The DCS SDV for Ross contains only his personal and sole trader deposits. The SDV for WCT lists WCT as the depositor, with Ross as an authorised individual. They are never combined.
Derived regulatory views¶
These are queries over the canonical schema, not separate tables. They will drift the moment a separate copy exists.
Single Depositor View (DCS / SDV)¶
Required under the Deposit Takers Act 2023. Produced by joining:
1. accounts.account_party_relationships WHERE dcs_relevant = true
2. Party details from SD02 party.parties and party.person_profiles
3. DCS eligibility and category from regulatory.party_regulatory_profiles
4. Joint/trustee/authorised-individual flags from relationship_type
5. Current balance from accounts.accounts
Output per RBNZ SDV standard: depositor identifier and name, all deposit accounts (sole, joint, trustee), aggregate eligible deposit per depositor, joint account co-depositors, authorised individuals per account.
AML/CFT customer graph¶
Produced for CDD and enhanced due diligence:
- Customer party record
- All party.party_roles where subject_party_id = customer (roles the customer holds)
- All party.party_roles where object_party_id = customer (persons acting on behalf of the customer)
- Beneficial owners: role_type = 'BENEFICIAL_OWNER' AND ownership_pct >= 25 AND end_date IS NULL
- Source of authority and evidence for each role
CRS/FATCA reportable account view¶
Produced for annual CRS reporting to Inland Revenue:
- Accounts where at least one account_party_relationships.crs_account_holder = true
- Account holder details (name, address, TINs, date of birth) from party.*
- Account balance at year end from accounts.accounts
- Entity classification from regulatory.party_regulatory_profiles
- Controlling persons where entity is a passive NFE (crs_controlling_person = true)
Single Customer View (SCV — app)¶
Service and analytics view for the customer app:
- All access.access_grants for a user_id
- All parties, accounts, operating contexts accessible via those grants
- All external assets (assets.*) linked to accessible parties
- Rendered as "your complete financial picture"
Migration from kyc.customers¶
The kyc.customers table remains in production as a backward-compatible anchor. The party.parties.party_id adopts the same UUID as kyc.customers.id — they are the same identifier.
Migration is additive and two-phased:
Phase 1 — additive: Create party.*, banking.*, regulatory.* as new tables alongside kyc.*. New modules write to both. Existing modules continue writing the current customer record. The party model is production-validated against live data.
Phase 2 — consolidation: Migrate FKs from kyc.customers(id) to party.parties(party_id) across SD01, SD04, SD05, and SD08. Deprecate redundant columns on kyc.customers (e.g. pep_flag moves to regulatory.party_regulatory_profiles). Remove kyc.customers in a final cleanup release.
Do not shortcut this. Adding sole_trader_flag boolean to kyc.customers is the wrong path — it will not support multi-entity, will not produce a correct SDV, and will need to be undone when the first trust or nominee account appears.