Skip to content

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_id column 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)

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

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

  3. 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_id and one party_id, but may have multiple customer relationships and multiple access grants.

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

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

  6. 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_grants table expresses access; accounts.account_party_relationships expresses 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.parties and 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.