S
Product and technical specification

Settle

An Australian divorce and de facto property settlement modelling platform, with asset discovery for complex pools. This document describes the system as built in the current repository and designs, without building, the phase two document intelligence layer.

Working title'Settle' is a placeholder. The product name is undecided and every user-facing string reads from BRAND.name so a rename is a one-line change.
Version0.1 draft (engine 1.0.0, snapshot schema version 1, migration 0000)
AudienceTechnical founder, engineering, product and legal review
StackTypeScript, Next.js (App Router) on Vercel, Supabase Postgres and Auth, Drizzle ORM, Vitest, Playwright
Status legendBuilt exists in the repository today. Phase two is designed here, not built.
Prepared16 September 2026
Section 1

Executive summary

Separating couples describe property settlement as gruesome, lawyer-heavy and expensive. The two most painful tasks are listing every asset and debt, and completing financial disclosure with someone they may no longer trust. Lawyers spend expensive hours on both before any negotiation starts.

Settle is a web application that lets one person, or both people together, build the property pool, record what each side thinks each item is worth, and see immediately what different splits look like in practical terms: who keeps the house, who pays whom, whether a superannuation split is needed. It then shows an indicative court outcome as a range, never a single number, produced by a fixed, published, deterministic method with every adjustment explained. A prominent disclaimer sits on the output itself: this is not a legal service, and a family lawyer should finalise any agreement.

What is built today

  • Five bounded contexts in TypeScript following domain-driven design: disclosure, asset discovery, settlement modelling, parenting arrangements and identity, plus a shared kernel (Money in integer cents, Result, ids, dates).
  • Per-party claimed values on every pool item. An agreed value is derived only when both claims exist and match exactly. Disagreement is a normal state with its own UI, not an error.
  • Solo mode (one person enters estimates for the other) and joint mode (partner invited by email, their own figures overwrite the estimates).
  • A four-step settlement engine (assessSettlement, ENGINE_VERSION 1.0.0) and a split modeller that turns a percentage into allocations, a balancing payment and, where needed, a suggested super split.
  • Anonymous in-browser modelling with no account. Sign-up is required only to save or to invite a partner. Passwordless auth (magic link or six-digit code) via Supabase, Google optional, no Facebook.
  • Mocked asset discovery behind a provider port: ASIC, PPSR, state land titles, ATO super and identity verification, gated by a paid 'discovery' tier and, for registries, by verified identity.
  • A Postgres schema (Drizzle, migration 0000_calm_triathlon) with tables prefixed by context, including two phase two tables (ph2_documents, ph2_disclosure_flags) that exist as schema only.
  • Six deterministic demo scenarios built through the domain constructors.

What is designed but not built Phase two

AI parsing of bank statements, payslips, superannuation statements and tax returns into structured extractions, and a rule-based checker that raises disclosure gaps and inconsistencies for a human to resolve. The AI never sets a value and never decides a split; it only proposes, and a party accepts or dismisses.

The core bet. The free government tool covers simple, amicable cases. Settle's differentiator is the hard part of everyone else's case: finding the assets, dealing with disagreement about their value, and making complex pools (trusts, companies, SMSFs, investment property) legible before a lawyer is engaged.

Section 1.1

Positioning versus amica

amica is the government-backed online tool that helps separated couples reach agreement on money and parenting. It is well built, trusted, and suits couples who are largely amicable, know what they own, and have a simple pool. Settle does not compete for those couples; it should send them to amica.

Dimensionamica (as understood, verify before publishing)Settle
Target caseSimple, amicable, both parties willing to engageAny case, including one party working alone, disagreement about values, and complex pools
Asset listingManual entry of known assetsManual entry plus a discovery tier that searches registries for what disclosure missed
DisagreementRequires convergence to proceedPer-party claimed values; the model runs across both views and reports a dollar range that spans them
Solo useDesigned for two participantsSolo mode with estimates for the partner, upgradeable to joint without re-entry
OutputSuggested split and agreement documentPractical split scenarios first (who keeps what, balancing payment, super split), then an indicative court range with per-factor explanations
Complex structuresLimitedFirst-class categories for business interests, trust interests, SMSFs, defined benefit super, Division 7A style tax debts
DocumentsUser-entered figuresPhase two parsing of statements, payslips and tax returns, with gap and inconsistency flags
PriceFree or low costFree core tool, paid discovery tier (pricing placeholder, see Section 9)
Legal standingNot legal adviceNot legal advice; disclaimer attached to every output; lawyer finalises

Positioning statement

For people separating with anything more than a simple pool, Settle is the place to build the full picture of what you both own, see what a fair split looks like in dollars and decisions, and walk into a lawyer's office with the expensive work already done.

Explicit non-goals

  • Settle does not give legal advice, draft consent orders or binding financial agreements, or file anything with the court.
  • Settle does not compute child support. The parenting context exposes care and cost percentages so the direction of travel can be explained, and stops there.
  • Settle does not use AI to produce numbers. The engine is a published heuristic. Phase two AI extracts facts from documents and proposes; humans decide.
Section 2

Personas and demo scenarios

Three personas drive the design. Each demo scenario in src/demo/scenarios.ts is a complete MatterSnapshot built through the domain constructors with deterministic ids, so seed data can never drift from the invariants. Demo logins are passwordless; the local provider accepts code 123456.

The solo starter

Has left or is about to leave. Does not trust the other side to engage yet. Wants to know 'roughly where do I stand' before spending money on a lawyer. Uses anonymous mode, enters estimates for the partner.

The cooperative pair

Both willing to participate but disagree on a few values (the house, the shares). Want a shared, honest picture and to narrow the dispute to the items that matter. Use joint mode.

The under-disclosed spouse

Suspects the pool is bigger than presented: a company, a trust, a second super fund. Cannot afford forensic accountants up front. Pays for the discovery tier.

The six demo scenarios

Pool and range figures below were produced by running computeResults against the real scenarios on 16 September 2026. The 'range' column is the indicative percentage for party A.

SlugTitle and storyType, mode, tierYearsItemsNet pool (low to high)Range A
young-couple-no-kidsShort marriage, no children, modest pool. Jess Nguyen (nurse, $98k) and Tom Barker (electrician, $112k). One apartment, two cars, small super. Tom brought in $60k versus Jess's $25k. Three items disputed (both cars, contents).Marriage, solo, free4.89$436,300 to $449,30039% to 49%
family-primary-carerLong marriage, three children, one income. Sarah Whitfield (full-time parent, carer, $0) and David Whitfield (mining engineer, $215k). Family home retained by Sarah, large super on David's side. Disputes on the home, BHP shares and one account. Used for the worked example in Section 8.Marriage, joint, free16.811$1,644,000 to $1,743,50064% to 72.5%
unemployed-partnerDe facto, one party recently unemployed. Liam Okafor (retrenched, health concern flagged, $0) and Chloe Martin (pharmacist, $104k). Renting; pool is mostly super and debt including an ATO debt.De facto, solo, free8.710$219,000 to $226,50056% to 69%
high-income-investorsTwo high earners, shares and an investment property. Priya Raman (anaesthetist, $410k, $150k inheritance) and Ben Castellano (software architect, $260k, $180k initial contribution). Disputes over the Footscray property, RSUs, Tesla and art. Priya's name has canned discovery findings.Marriage, joint, discovery12.912$3,160,000 to $3,307,00044.3% to 51.3%
low-income-rentersLow incomes, renting, two young children. Kayla Brown (casual retail, $38k, primary homemaker) and Jordan Lee (delivery driver, $52k). Week-about care, so no primary carer. Nothing but super, a car loan and buy-now-pay-later balances. Every item agreed.De facto, joint, free7.48$65,400 (no dispute)49.5% to 56.5%
family-trust-companyComplex pool: family trust, company, SMSF. Elena Delfino (part-time bookkeeper, $45k, primary homemaker) and Marcus Delfino (company director, $320k, $90k initial contribution). Elena values the company at $1.4m, Marcus at $620k. The trust has only Elena's figure (awaiting other). Marcus's name returns rich mock findings across ASIC, PPSR, land titles and ATO super.Marriage, solo, discovery20.012$4,517,000 to $5,637,00056.2% to 69.7%

Demo accounts: demo.jess@example.com, demo.tom@example.com, demo.sarah@example.com, demo.david@example.com, demo.liam@example.com, demo.chloe@example.com, demo.priya@example.com, demo.ben@example.com, demo.kayla@example.com, demo.jordan@example.com, demo.elena@example.com, demo.marcus@example.com. The /demo page lists scenarios via GET /api/demo/scenarios and signs in as A or B via POST /api/demo/:slug/login.

Section 3

Functional scope

Scope is organised by bounded context. Everything marked built exists in src/ today; the UI pages listed in Section 4 render these capabilities.

AreaCapabilityStatus
DisclosureCreate a matter: relationship type (marriage or de facto), cohabitation start, separation date; separation cannot precede startBuilt
Two parties (roles A and B) with name, date of birth, occupation, employment status, gross annual income (cents), health or capacity flag, primary homemaker flag, initial contribution, inheritances or gifts, participating flagBuilt
Children with first name and date of birthBuilt
Pool items across 16 categories in three kinds (asset, liability, superannuation), ownership joint, A or B, optional 'retained by', optional discovery referenceBuilt
Per-party valuations with source (manual, estimate_for_partner, discovery, document); derived agreed value; accept other party's value; agreement state per item; disclosure summary with low and high pool, disputed items ranked by gap, completenessBuilt
Solo to joint is one-way (inviteToJoint); partner's own figures overwrite estimatesBuilt
ParentingCare arrangement per child as nights with A per fortnight (0 to 14) with optional pattern text; care percent, nights per year, Services Australia care level, cost percent tableBuilt
Parenting summary: dependent children (under 18), primary carer (majority care of majority of dependents, null when even), average care percent for ABuilt
Settlement modellingFour-step deterministic assessment with factors, bands, indicative range in percent and dollars, range notes, cautions and on-output disclaimerBuilt
Split modelling: allocation of each item, party positions, balancing payment, super split suggestion, practical notes; standard scenarios (equal, low, mid, high)Built
Perspective switch (view the pool as A or B)Built
Asset discoveryProvider port with five provider codes and metadata (tier and identity requirements); gating function; application service that runs a provider and records a request with findingsBuilt
Deterministic mock adapters keyed on subject name; accept a finding to create a pool item with discoveryRefBuilt (mock)
Real registry integrationsLater
IdentityPasswordless sign-in (magic link or six-digit code) through an AuthProvider port; local provider for development, Supabase provider for production; Google OAuth; no FacebookBuilt
Sessions as httpOnly cookie settle_session backed by id_sessionsBuilt
Invitations with 14-day expiry, opposite role offered, email must match on acceptance; matter membershipBuilt
Tier upgrade and identity verification (mock payment, mock verification)Built (mock)
DocumentsUpload, parse and extract structured data from bank statements, payslips, super statements and tax returnsPhase two
Gap and inconsistency detection with human reviewPhase two
Section 3.1

User journeys

Journey 1: anonymous solo modelling (free)

  1. Land on /, read what the tool does and the disclaimer, press 'Start'.
  2. /start: choose marriage or de facto, enter cohabitation start and separation date. An anonymous matter is created in the browser and stored under localStorage['settle.matter.v1'].
  3. /matter/you and /matter/partner: enter your own details and your best estimates for the other person. Estimates are stored under role B with source: 'estimate_for_partner'.
  4. /matter/children: add children and the nights-per-fortnight pattern per child, or skip.
  5. /matter/disclosure: add assets, debts and super. For each item enter your value and your estimate of the other side's value. Agreement chips update as you type.
  6. /matter/results: computeResults runs in the browser. Split scenarios are shown first, then the indicative range with the disclaimer attached, then the factor explanations.

Journey 2: save and invite (joint)

  1. From results or /matter/share, press 'Save' or 'Invite'. If not signed in, the app sends the person to /sign-in and returns them afterwards.
  2. Sign in by email code or magic link (or Google). POST /api/matters stores the snapshot; the creator becomes a member with snapshot.matter.ownerRole.
  3. POST /api/matters/:id/invite with the partner's email creates an invitation offering the opposite role and moves the matter to joint mode.
  4. The partner opens /invite/[token], signs in with the invited email, and POST /api/invitations/:token/accept adds them to id_matter_members.
  5. The partner reviews every item and enters their own figures, which replace the estimates. Items with matching values become agreed; the others are shown as disputed, ranked by gap.

Journey 3: discovery (paid)

  1. On /matter/discovery, providers are listed with lock chips explaining what unlocks them: the discovery tier for all registries, and verified identity for land titles and ATO super.
  2. Upgrade (mock payment) via POST /api/account/upgrade; verify identity (mock) via POST /api/account/verify-identity.
  3. Run a provider for a subject (POST /api/matters/:id/discovery). Findings appear with confidence (exact, probable, possible) and a suggested item.
  4. Accept a finding; the disclosure context creates a PoolItem with discoveryRef and the estimated value recorded for the requesting party with source discovery.
flowchart LR
  L["Landing /"] --> S["Start: relationship basics"]
  S --> Y["You"] --> P["Partner (estimates in solo)"] --> C["Children and care"] --> D["Disclosure: items and per-party values"]
  D --> R["Results: splits, then indicative range"]
  R -->|save or invite| SI["Sign in (passwordless)"] --> SH["Share: save + invite"]
  SH --> INV["Partner accepts /invite/token"] --> D
  R -->|deep discovery| DS["Discovery (tier + identity gated)"] -->|accept finding| D
  style R fill:#FBE9DA,stroke:#E97819
    
The end-to-end journey. Results are reachable without an account; saving, inviting and discovery require sign-in.
Section 4

Information architecture

The App Router page list below is the contract between product and engineering; it matches docs/ARCHITECTURE.md. Every /matter/* page reads and writes the same MatterSnapshot, whether it lives in the browser or in Postgres.

RoutePurposeRequires
/Landing: what it does, start anonymously, demo scenarios, disclaimerNothing
/startRelationship basics (type, dates); creates the anonymous matterNothing
/matter/youParty A details and incomeA matter in the browser or a membership
/matter/partnerParty B details and income (estimates in solo mode)As above
/matter/childrenChildren, ages, care arrangement per childAs above
/matter/disclosureAssets, debts, super; per-party values; agreement state per itemAs above
/matter/resultsSplits first, then indicative court range with on-output disclaimer, factorsAs above
/matter/discoveryAsset discovery (tier gated, identity gated); findings add to disclosureSigned in, saved matter
/matter/shareSave and invite partnerSigned in
/sign-inPasswordless (email code or magic link) plus GoogleNothing
/invite/[token]Accept an invitationSigned in as the invited email
/dashboardSaved mattersSigned in
/demoPick a demo scenario, sign in as A or BNothing

Navigation model

The matter pages form a linear stepper (you, partner, children, disclosure, results) with a persistent progress bar and free movement between completed steps. Results, discovery and share are always one tap away from the stepper once a matter exists. On mobile the stepper collapses to the current step name plus a 'Steps' drawer.

Persistence states

StateWhere the snapshot livesWho can see itModelling runs
AnonymouslocalStorage['settle.matter.v1']This browser onlyIn the browser via computeResults
Saved (solo)Postgres via MatterRepository, browser keeps a working copyThe owner (one membership row)Browser, or GET /api/matters/:id/results
JointPostgresBoth members, each with their roleEither side, with perspective chosen
Section 4.1

Wireframes: landing and start

Wireframes are CSS mockups, not final visual design. They fix layout priority, copy hierarchy and the behaviours in Section 4. Navy is structure, terracotta is the one primary action per screen.

settle.example /
How it worksDemoSign in
Work out a fair property settlement, without the fog.

List what you both own and owe, see what different splits look like in real terms, and get an indicative court range. No account needed to start.

Start for freeTry a demo
1. Build the pool
Assets, debts, super. Your value and theirs.
2. See the splits
Who keeps what, who pays whom.
3. Find what is missing
Registry discovery for complex pools.
Explore a demo
Short marriage, no kidsThree children, one incomeFamily trust and company+3 more
Not a legal service. Settle models outcomes from the information you enter. A family lawyer should review and finalise any agreement.
Working titlePrivacy, Terms, hello@example.com
Landing (/): value proposition, one primary action, demo entry points, disclaimer visible above the fold.
settle.example / start
Saved in this browser
About the relationship

Three questions. You can change them later.

Type of relationship
MarriageDe facto
When did you start living together?
YYYY-MM-DD
When did you separate?
YYYY-MM-DD

Separation cannot be before the relationship began.

Who is filling this in?
Just me for nowWe are doing this together
Continue
Nothing leaves your browser until you saveStep 1 of 6
Start (/start): creates the anonymous matter. 'Just me' is solo mode; 'together' still starts solo and prompts to invite from the share page, because joint requires two signed-in members.
Section 4.2

Wireframes: disclosure list

The disclosure list is the heart of the product. Every row shows both parties' claimed values and an agreement chip derived from agreementState. In solo mode the B column is labelled 'Your estimate for David' and stored with source: 'estimate_for_partner'. In joint mode each person edits only their own column; the other column is read-only with an 'Accept their value' action.

settle.example / matter / disclosure
YouPartnerChildrenDisclosureResultsDiscoveryShare
What you own and owe

Joint mode. Sarah and David both participating. 8 agreed, 3 disputed, 0 awaiting, 0 unvalued.

Sort by gapAdd item
Net pool $1,644,000 to $1,743,500Your view $1,738,000David's view $1,649,500Complete 100%
Assets
ItemOwnerSarah (you)DavidStatusKeeps
Family home, Ashgrove QLDJoint1,450,0001,380,000 gap $70,000DisputedSarahAccept theirs
Joint savingsJoint62,00062,000AgreedShared
David everyday accountDavid9,00014,500DisputedDavidAccept theirs
2021 Kia CarnivalJoint38,00038,000AgreedSarah
Employee share plan (BHP)David95,00071,000DisputedDavidAccept theirs
Holiday caravan via discoveryDavid18,000not yet valuedAwaiting David
ContentsJointno valueno valueUnvaluedShared
Debts
Westpac home loanJoint520,000520,000AgreedSarah (follows home)
Car loan (Hilux)David31,00031,000AgreedDavid
Superannuation
UniSuper (David)David486,000486,000AgreedMember
Debts are entered as positive amounts. Values are whole dollars.See results
Disclosure (/matter/disclosure). Chips map one-to-one to ItemAgreementState: agreed (green), disputed (terracotta, never red), awaiting_other (grey), unvalued (dashed). Two rows are illustrative additions to the demo data to show the awaiting and unvalued states.

Row behaviours

  • Editing your value calls recordValuation; the agreed value and chip are recomputed immediately and never edited directly.
  • 'Accept theirs' calls acceptOtherPartyValue, which records their figure as yours with the note 'Accepted other party value' and therefore produces agreement.
  • 'Keeps' sets retainedBy and drives the split modeller. Superannuation shows 'Member' and cannot be reassigned here.
  • Negative values are rejected with 'Enter debts as positive amounts under liabilities'. Remove item is a destructive action and uses red only in its confirmation.
Section 4.3

Wireframes: results

Order is fixed by the product brief and enforced in the design: practical split scenarios first, then the indicative range with the disclaimer attached to it, then the factor explanations. Numbers below are the real family-primary-carer output from Section 8.

settle.example / matter / results
DisclosureResultsDiscoveryShareViewing as Sarah
What different splits look like
Pool from your view $1,738,000. 3 items disputed.
Equal split50 / 50
Sarah keeps home and its mortgage, Carnival, her super. David keeps Hilux, shares, his super.
Sarah pays David $206,000. Larger than her available cash: usually means refinancing the home or selling an asset.
Midpoint of indicative range68.3 / 31.7
Same allocations. Target for Sarah $1,187,054.
David pays Sarah $112,054. More than his ready cash: refinance or sell an asset. No super split needed.
Lower end of range64 / 36
David pays Sarah $37,320 from cash or investments.
Upper end of range72.5 / 27.5
David pays Sarah $104,000 plus a super split of about $81,050.
Try another percentageChange who keeps what
Indicative court range
Sarah 64% to 72.5%
0%100%
$1,052,160 to $1,264,038
David 27.5% to 36%
$452,100 to $627,660

3 items have different values from each party, so the dollar range spans both views of the pool.

This is a modelling tool, not a legal service. The figures are an indicative guide produced by a fixed, published method from the information entered. Outcomes in the Federal Circuit and Family Court of Australia depend on the full circumstances and on evidence. A family lawyer should review and finalise any agreement before it is signed or filed, and independent legal advice is required for a binding financial agreement.
How we got there (engine 1.0.0)
1 Identify and value the poolNet pool $1,644,000 to $1,743,500 across 11 items
2 Assess contributions50% to Sarah (47.5% to 52.5%). Homemaker and parenting contributions offset David's income.0 pts
3 Consider future needsIncome disparity (Sarah, carer, no income) +10; care of 3 children incl. one under five +10; capped at 20+20 pts
4 Just and equitable check64% to 72.5% for Sarah
Save this matterInvite DavidExport summary for a lawyer
Results, desktop (/matter/results). The disclaimer lives inside the indicative range card; the page has no footer disclaimer to fall back on.
Section 4.3, continued

Results on a phone

On narrow screens scenarios become a horizontally swipeable card rail with the midpoint card first. The indicative range stacks the two parties. The disclaimer keeps its full text; it is never truncated or collapsed behind a 'read more'.

results
StepsS
What different splits look like

Swipe. Pool from your view $1,738,000.

Midpoint 68.3 / 31.7
Sarah keeps the home, its loan and the Carnival. David keeps the Hilux and shares. Super stays put.
David pays Sarah $112,054
Refinance or sell an asset.
Equal 50 / 50
Sarah pays $206,000
Change who keeps what
Scenario rail, midpoint first.
results (scrolled)
Indicative court range
Sarah 64% to 72.5%
$1,052,160 to $1,264,038
David 27.5% to 36%
$452,100 to $627,660

3 disputed items widen the dollar range.

Not a legal service. An indicative guide from a fixed, published method. Court outcomes depend on full circumstances and evidence. A family lawyer should review and finalise any agreement before it is signed or filed, and independent legal advice is required for a binding financial agreement.
How we got there
Step 3: future needs +20
Income disparity +10. Care of three children, one under five, +10. Capped at 20.
SaveInvite David
Range card with attached disclaimer, then factors as expandable cards.
Section 4.4

Wireframes: discovery, sign-in and share

settle.example / matter / discovery
DisclosureResultsDiscoveryShareDiscovery tier
Find what disclosure missed

Search public registries for a person. Findings are leads; nothing enters the pool until you accept it.

Subject: Marcus Delfino (B)
ASIC companies and directorshipsRun
Officeholder and shareholder records. Discovery tier.
PPSR security interestsRun
Finance over vehicles and personal property. Discovery tier.
State land titlesVerify identity to unlock
Registered proprietor searches. Discovery tier and verified identity.
ATO superannuation lookupVerify identity to unlock
Your own accounts, including lost super. Consent driven.
Findings: ASIC, completed just now
Director: Delfino Holdings Pty Ltd (ACN 612 448 903)Exact
Appointed 12 Mar 2015. Registered office Fortitude Valley QLD.
Suggested: Business interest, est. $850,000, owner MarcusAdd to disclosureDismiss
Director: Delfino Family Investments Pty Ltd as trusteeExact
Corporate trustee, commonly indicates a discretionary family trust.
Suggested: Trust interest, no estimateAdd to disclosure
Shareholder: Northbank Cafe Group Pty Ltd (15%)Probable
Suggested: Shares, est. $120,000Add to disclosure
Discovery (/matter/discovery). Lock chips come straight from PROVIDER_META and gateDiscovery. Findings show confidence and a one-click suggested item.
settle.example / sign-in
Sign in to save or invite

No passwords. We email you a six-digit code or a link.

you@example.com
Email me a codeSend a magic link
or
Continue with Google
Enter the code
123456
Codes expire after 10 minutes. Resend.

Your anonymous matter in this browser will be saved to your account.

Sign-in (/sign-in). Email code and magic link both call POST /api/auth/start; the code path then calls /api/auth/verify.
settle.example / matter / share
Save and share
Saved to your account 2 minutes agoSolo mode
Invite David to add his own figures

David gets role B. Your estimates for him are replaced by his figures as he enters them. Once invited, the matter is shared and cannot go back to solo.

david@example.com
Send invitationExpires in 14 days
Pending invitation
david@example.com, sent today, expires 30 Sep 2026
Copy linkRevoke
Members
S Sarah Whitfield, role A, you D David, role B, invited
Share (/matter/share). Revoke is destructive and is the only red on this screen.
Section 5

Domain model and bounded contexts

The code is organised as five bounded contexts plus a shared kernel under src/contexts. The rule enforced by convention and review: no context imports another context's infrastructure. Domain types may be imported across contexts where one context is upstream of another; those dependencies are drawn below.

flowchart TB
  SK["Shared kernel: Money (cents), Result, ids, dates"]
  DC["Disclosure: Matter, Party, Child, PoolItem, Valuation, agreedValue rule, DisclosureSummary"]
  PA["Parenting: CareArrangement, care percent, cost percent, primary carer"]
  SM["Settlement modelling: assessSettlement, Factor, Band, modelSplit, standardScenarios"]
  AD["Asset discovery: Provider port, PROVIDER_META, gateDiscovery, runDiscovery, Finding"]
  ID["Identity: User, Session, AuthProvider port, Invitation, membership, Tier"]
  RM["Read model: MatterSnapshot, MatterResults, computeResults (published language)"]
  SK --> DC
  SK --> PA
  SK --> SM
  SK --> AD
  SK --> ID
  DC -->|"upstream: Child type (conformist)"| PA
  DC -->|"upstream: Matter, Party, PoolItem, summariseDisclosure (conformist)"| SM
  PA -->|"upstream: ParentingSummary (conformist)"| SM
  DC -->|"upstream: PoolItemCategory (conformist)"| AD
  AD -->|"Finding is a lead; user accepts, disclosure creates PoolItem with discoveryRef (anti-corruption at the boundary)"| DC
  AD -->|"Tier type (conformist)"| ID
  ID -->|"membership gates access to a Matter"| DC
  DC --> RM
  PA --> RM
  SM --> RM
  RM -->|"same JSON shape in browser, API and DB"| UI["UI, API route handlers, MatterRepository"]
  style RM fill:#FBE9DA,stroke:#E97819
  style SK fill:#EAF0F6,stroke:#103654
    
Context map. Arrows point downstream. Disclosure is the core domain; settlement modelling and parenting conform to its types; discovery never writes into disclosure directly.
ContextResponsibilityTables
Shared kernelInteger-cent Money with splitByPercent that always sums exactly, formatAud, branded ids, PartyRole A or B, ISO dates, Result so domain code never throws for expected failuresNone
Disclosure (core)The Matter aggregate and everything disclosed. Owns the agreed value rule and the low and high pool calculationdc_matters, dc_parties, dc_children, dc_pool_items, dc_pool_item_valuations
ParentingCare pattern per child and derived percentages. Does not compute child supportpa_care_arrangements
Settlement modellingPure computation. No tables; results are recomputed on demand and versioned by ENGINE_VERSIONNone (sm_ reserved)
Asset discoveryRegistry search behind a port, gating, findings as leadsad_discovery_requests
IdentityUsers, sessions, challenges, tier, invitations, matter membership; authentication delegated to AuthProviderid_users, id_sessions, id_auth_challenges, id_matter_members, id_invitations
Documents Phase twoUploaded documents, extractions, disclosure flagsph2_documents, ph2_disclosure_flags
Section 5.1

Aggregates, invariants and agreement states

Key invariants (from ARCHITECTURE.md, enforced in code)

  • Money is integer cents everywhere in the domain. recordValuation rejects non-integers and negatives. Formatting happens at the edge with formatAud.
  • PoolItem.valuations holds one claimed value per party. agreedValue is derived by deriveAgreedValue and is null unless both claims exist and are exactly equal. It is never set by hand; the repository recomputes it on save.
  • In solo mode the user's estimates for the partner are stored under the partner's role with source: 'estimate_for_partner'. Joint mode overwrites them with the partner's own figures (source manual).
  • The settlement engine is pure and deterministic. Every adjustment is a Factor with an explanation. The output carries a disclaimer string the UI must render on the result itself.
  • Court outcome is always a range. Dollar ranges span both parties' views of the pool.
  • Moving from solo to joint is one-way (inviteToJoint rejects an already-joint matter).
  • Separation date cannot precede cohabitation start. Nights per fortnight are 0 to 14. Descriptions and names cannot be blank.

Effective value and the two pools

effectiveValue(item, perspective) returns the agreed value first, then the viewer's own claim, then the other party's claim. summariseDisclosure also computes a low pool (for each item the value least favourable to a high total: the smaller asset claim, the larger debt claim) and a high pool (the reverse). The engine's referencePool is the midpoint of the two net totals.

stateDiagram-v2
  [*] --> unvalued : createPoolItem
  unvalued --> awaiting_other : one party records a value
  awaiting_other --> agreed : other party records the same value
  awaiting_other --> disputed : other party records a different value
  awaiting_other --> awaiting_other : same party revises value
  disputed --> agreed : either party revises to match, or acceptOtherPartyValue
  agreed --> disputed : either party revises to a different value
  disputed --> disputed : revise, still different
  agreed --> agreed : both revise to the same new value
  note right of disputed
    Normal state. Shown with a terracotta chip,
    ranked by valuationGap on the disclosure page,
    widens the dollar range on results.
  end note
  note right of agreed
    agreedValue is set and equals both claims.
    Derived by deriveAgreedValue, never edited.
  end note
    
Agreement states of a pool item, as returned by agreementState. Removing a valuation is not supported today; a value can only be replaced.
Section 5.2

Ubiquitous language

Terms below are used identically in code, UI copy, this document and conversations with the client. Where the UI shows a friendlier label, it is noted.

Term (code)Meaning
Matter (Matter)The shared container for one separating couple: mode, status, relationship type, dates, owner role.
Party, role (Party, PartyRole)One of the two people, always A or B. Roles are stable across the whole system; the creator is usually A (ownerRole).
Mode (MatterMode)solo: one person enters everything including estimates for the other. joint: both are members and enter their own figures.
Status (MatterStatus)draft, disclosing, modelling, agreed, archived. Informational today; drives dashboard labels.
Pool item (PoolItem)One asset, liability or superannuation interest. UI: 'item'. Has a kind (derived from category), ownership and optional retained-by.
Claimed value (Valuation)What one party says an item is worth, with source, note and timestamp. UI: 'your value', 'their value', 'your estimate for X'.
Agreed value (agreedValue)Derived. Present only when both claims exist and match.
Agreement state (ItemAgreementState)agreed, disputed, awaiting_other, unvalued. UI chips.
Gap (valuationGap)Absolute difference between the two claims; zero unless both present.
Effective value (effectiveValue)The value used from one perspective: agreed, else own, else other's.
Low pool, high pool (DisclosureSummary.low/high)Net pools built from the least and most favourable claims per item. The dollar range on results spans them.
Reference pool (referencePool)Midpoint of low and high net totals; used to express contribution differences as a percentage of the pool.
Factor (Factor)One adjustment with code, group (contributions or future needs), label, legislative basis, plain-English explanation and points toward A as a band.
Band (Band)A low, mid and high number. Settlements are ranges, not points.
Indicative range (indicativeRange)Percent low and high for each party plus dollar low and high. UI: 'Indicative court range'. Never shown as one number.
Split scenario (SplitScenario)A percentage turned into allocations, positions, a balancing payment and possibly a super split suggestion. UI: 'What different splits look like'.
Balancing paymentCash or asset transfer so retained items land on the target split.
Super split suggestionAmount of superannuation that would need to move by splitting order when the payer cannot fund the balance outside super.
Care arrangement (CareArrangement)Nights per fortnight a child spends with A. Care percent and Services Australia care level are derived.
Primary carerThe party with majority care of the majority of dependent children; null when even.
Provider (DiscoveryProvider, ProviderCode)A registry behind a port: asic, ppsr, land_titles, ato_super, identity.
FindingA lead from a provider with confidence and an optional suggested item. Never enters the pool without a person accepting it.
Discovery requestOne run of one provider for one subject, with status and findings.
Tierfree or discovery. Attached to a user, not a matter.
Invitation, memberAn emailed offer of the opposite role; accepting creates a matter membership row.
Snapshot (MatterSnapshot)The whole matter as one JSON aggregate, version 1. The published language between browser, API and database.
Results (MatterResults)An assessment plus the standard scenarios, from one perspective.
Section 6

Architecture: system context

A C4 level-one view. The people, the one system, and the external systems it depends on. Registry adapters are mocked today and are drawn where the real services will sit.

flowchart LR
  subgraph people["People"]
    A["Party A: the person who starts the matter"]
    B["Party B: the former partner, invited later"]
    LW["Family lawyer: receives the summary, finalises the agreement"]
  end
  subgraph sys["Settle (working title)"]
    S["Web application: Next.js on Vercel, modelling in browser and server"]
  end
  subgraph ext["External systems"]
    SBA["Supabase Auth: magic link, email OTP, Google"]
    PG["Postgres: Supabase in production, local in development"]
    ASIC["ASIC registry (mocked)"]
    PPSR["PPSR (mocked)"]
    LT["State land title registries (mocked)"]
    ATO["ATO super via myGov consent (mocked)"]
    IDV["Identity verification vendor (mocked)"]
    LLM["Document extraction model (phase two)"]
  end
  A -->|"models anonymously, saves, invites"| S
  B -->|"accepts invitation, enters own values"| S
  S -->|"exported summary (planned)"| LW
  S -->|"passwordless sign-in"| SBA
  S -->|"Drizzle over pg"| PG
  S --> ASIC
  S --> PPSR
  S --> LT
  S --> ATO
  S --> IDV
  S -.->|"phase two"| LLM
  style S fill:#103654,stroke:#103654,color:#fff
  style LLM stroke-dasharray: 5 5
    
System context. Solid arrows exist today (mocked where noted). The dashed arrow is phase two.

Architectural principles

  • Same shape everywhere. The MatterSnapshot JSON is what the browser stores, what the API sends, and what the repository normalises into rows and back. Anonymous to saved needs no conversion.
  • Compute where the data is. The engine runs in the browser for anonymous users and in route handlers for saved matters. It is the same pure function.
  • Ports for everything external. AuthProvider and DiscoveryProvider are interfaces with local and mock implementations; production adapters are drop-ins.
  • Whole-aggregate persistence. MatterRepository.save replaces the aggregate in one transaction. Simple and correct at tens of items per matter.
Section 6.1

Components and deployment

flowchart TB
  subgraph browser["Browser"]
    direction LR
    UI["React 19 pages"]
    ENG["computeResults"]
    LS["localStorage settle.matter.v1"]
    UI --> ENG
    UI --> LS
  end
  subgraph vercel["Vercel: one Next.js deployment"]
    direction LR
    RH["Route handlers /api/*"]
    AUTH["AuthProvider port"]
    REPO["MatterRepository (Drizzle, pg Pool)"]
    DISC["runDiscovery + gateDiscovery"]
    RH --> AUTH
    RH --> REPO
    RH --> DISC
  end
  subgraph data["Supabase project or local Postgres"]
    direction LR
    DB[("Postgres: id_ dc_ pa_ ad_ ph2_")]
    SBA["Supabase Auth"]
    STO["Storage bucket (phase two)"]
  end
  subgraph adapters["Registry adapters"]
    direction LR
    MOCK["MockProvider x4 + mockVerifyIdentity"]
    REAL["Real ASIC, PPSR, titles, ATO, KYC (later)"]
  end
  subgraph ph2["Phase two worker"]
    direction LR
    Q["Queue"] --> W["Parsing worker"] --> CHK["Disclosure checker"]
  end
  UI -->|"JSON over HTTPS, settle_session cookie"| RH
  DISC --> MOCK
  DISC -.-> REAL
  MOCK ~~~ Q
  REAL ~~~ Q
  AUTH --> SBA
  REPO --> DB
  RH -.-> STO
  STO -.-> W
  CHK -.-> DB
  style ph2 stroke-dasharray: 5 5
  style REAL stroke-dasharray: 5 5
  style STO stroke-dasharray: 5 5
    
Deployment view. Dashed elements are not built. Everything in the Vercel box is a single Next.js deployment; there is no separate API service.
ConcernDecision
RuntimeNext.js 16 App Router, React 19, TypeScript 5, Node 20+ (crypto.randomUUID in browser and server). Tailwind 4 for styling.
HostingVercel. Route handlers run as serverless functions; a lazily created pg.Pool (max 10) is shared per process.
DatabasePostgres. Supabase in production, local settle_dev in development. Identical DDL from Drizzle migrations. Connection via DATABASE_URL; on Supabase use the pooled (transaction mode) connection string.
AuthisSupabaseEnabled() is true only when both public Supabase variables exist; otherwise LocalAuthProvider with fixed code 123456 and a local Google stand-in route.
SessionsApp-owned: id_sessions.token in an httpOnly settle_session cookie. Supabase Auth proves the email; Settle issues its own session so local and production behave the same.
Configsrc/lib/server/env.ts is the only reader of process.env on the server.
ObservabilityVercel logs and Supabase logs today. Structured logging of API errors { code, message }. No analytics vendor is included; see privacy.
Section 6.2

Sequence: anonymous, save, invite, accept

sequenceDiagram
  autonumber
  actor A as Party A (browser)
  participant LS as localStorage
  participant API as Next route handlers
  participant AUTH as AuthProvider
  participant DB as Postgres
  actor B as Party B (browser)
  A->>LS: create matter, parties, items (settle.matter.v1)
  A->>A: computeResults in browser
  A->>API: POST /api/auth/start { email, method }
  API->>AUTH: startEmailSignIn
  AUTH-->>A: email with code or magic link
  A->>API: POST /api/auth/verify { challengeId, code }
  API->>AUTH: verifyEmailCode
  API->>DB: upsert id_users, insert id_sessions
  API-->>A: { user } + Set-Cookie settle_session
  A->>API: POST /api/matters { snapshot }
  API->>DB: MatterRepository.save (one transaction), insert id_matter_members role = ownerRole
  API-->>A: { matterId, role }
  A->>API: POST /api/matters/:id/invite { email }
  API->>DB: insert id_invitations (roleOffered = other role, 14 days), dc_matters.mode = joint
  API-->>A: { invitation, acceptUrl }
  A-->>B: shares link /invite/[token] (email in production)
  B->>API: sign in with the invited email (same auth steps)
  B->>API: POST /api/invitations/:token/accept
  API->>DB: check pending, not expired, email matches, insert id_matter_members role B, invitation accepted
  API-->>B: { matterId, role: B }
  B->>API: GET /api/matters/:id
  API-->>B: { snapshot, role }
  B->>API: PUT /api/matters/:id { snapshot with B valuations, source manual }
  API->>DB: save, recompute agreed_value per item
    
The anonymous snapshot is posted unchanged. Membership, not ownership of rows, is what authorises access to a matter.

Step 17 uses acceptUrl for local demos. In production the invitation email carries the link and the token is never shown to the inviter.

Section 7

Data model

The schema is defined in src/db/schema.ts with Drizzle and generated into drizzle/0000_calm_triathlon.sql. Table prefixes make context ownership visible in the database: id_ identity, dc_ disclosure, pa_ parenting, ad_ asset discovery, sm_ reserved (no tables, pure computation), ph2_ phase two. Money columns are bigint cents. All timestamps are timestamptz.

erDiagram
  id_users {
    uuid id PK
    text email UK
    text display_name
    tier tier
    boolean identity_verified
    text auth_subject
  }
  id_sessions {
    text token PK
    uuid user_id FK
    timestamptz expires_at
  }
  id_auth_challenges {
    uuid id PK
    text email
    text method
    text code_hash
  }
  id_matter_members {
    uuid matter_id PK
    uuid user_id PK
    party_role role
  }
  id_invitations {
    uuid id PK
    uuid matter_id FK
    uuid invited_by_user_id FK
    text invited_email
    party_role role_offered
    text token UK
    invitation_status status
    timestamptz expires_at
    uuid accepted_by_user_id FK
  }
  dc_matters {
    uuid id PK
    matter_mode mode
    matter_status status
    relationship_type relationship_type
    date cohabitation_start
    date separation_date
    party_role owner_role
  }
  dc_parties {
    uuid id PK
    uuid matter_id FK
    party_role role
    text display_name
    date date_of_birth
    text occupation
    employment_status employment_status
    bigint annual_income
    boolean health_or_capacity_concern
    boolean primary_homemaker
    bigint initial_contribution
    bigint inheritances_or_gifts
    boolean participating
  }
  dc_children {
    uuid id PK
    uuid matter_id FK
    text first_name
    date date_of_birth
  }
  dc_pool_items {
    uuid id PK
    uuid matter_id FK
    pool_item_kind kind
    text category
    text description
    ownership ownership
    bigint agreed_value
    party_role retained_by
    text discovery_ref
  }
  dc_pool_item_valuations {
    uuid item_id PK
    party_role party_role PK
    bigint value
    valuation_source source
    text note
  }
  pa_care_arrangements {
    uuid child_id PK
    uuid matter_id FK
    smallint nights_with_a_per_fortnight
    text pattern
  }
  ad_discovery_requests {
    uuid id PK
    uuid matter_id FK
    discovery_provider provider
    jsonb subject
    party_role requested_by_role
    discovery_status status
    jsonb findings
    text error
  }
  ph2_documents {
    uuid id PK
    uuid matter_id FK
    party_role uploaded_by_role
    document_kind kind
    text storage_path
    document_status status
    jsonb extraction
    text parser_version
  }
  ph2_disclosure_flags {
    uuid id PK
    uuid matter_id FK
    disclosure_flag_kind kind
    smallint severity
    text message
    uuid related_item_id FK
    uuid source_document_id FK
    disclosure_flag_status status
  }
  id_users ||--o{ id_sessions : "has"
  id_users ||--o{ id_matter_members : "joins"
  dc_matters ||--o{ id_matter_members : "has members"
  dc_matters ||--o{ id_invitations : "invites to"
  id_users ||--o{ id_invitations : "sends"
  dc_matters ||--|{ dc_parties : "two parties"
  dc_matters ||--o{ dc_children : "has"
  dc_matters ||--o{ dc_pool_items : "pool"
  dc_pool_items ||--o{ dc_pool_item_valuations : "claimed per party"
  dc_children ||--o| pa_care_arrangements : "care"
  dc_matters ||--o{ ad_discovery_requests : "runs"
  dc_matters ||--o{ ph2_documents : "uploads"
  dc_matters ||--o{ ph2_disclosure_flags : "flags"
  dc_pool_items o|--o{ ph2_disclosure_flags : "relates to"
  ph2_documents o|--o{ ph2_disclosure_flags : "sourced from"
    
Full schema as generated by migration 0000. Zoom in to read it at natural size and scroll sideways; the per-table pages that follow list every column. Routine timestamp columns are omitted from the diagram. Every table with a matter_id cascades on matter delete, which is what makes 'delete my matter' complete.
Section 7.1

Schema: identity (id_)

Enum types created by the migration: tier (free, discovery), party_role (A, B), invitation_status (pending, accepted, expired, revoked).

id_users

ColumnTypeConstraintsWhy it exists
iduuidPK, default gen_random_uuid()App-owned user id, stable across auth providers
emailtextNOT NULL, unique index id_users_email_idxPasswordless identity; lowercased before storage
display_nametextnullableOptional friendly name
tiertierNOT NULL default 'free'Gates discovery providers
identity_verifiedbooleanNOT NULL default falseGates land titles and ATO super
auth_subjecttextnullableSupabase auth.users.id when on Supabase; null for local auth. Basis for RLS mapping
created_attimestamptzNOT NULL default now()Audit

id_sessions

ColumnTypeConstraintsWhy it exists
tokentextPKOpaque value stored in the settle_session httpOnly cookie
user_iduuidNOT NULL, FK id_users ON DELETE CASCADEDeleting a user ends their sessions
expires_attimestamptzNOT NULLServer-side expiry; checked on every request
created_attimestamptzNOT NULL default now()Audit

id_auth_challenges

ColumnTypeConstraintsWhy it exists
iduuidPK default gen_random_uuid()The challengeId returned by /api/auth/start in local mode
emailtextNOT NULLWho is signing in
methodtextNOT NULL'magic_link' or 'email_otp'
code_hashtextNOT NULLCodes are never stored in clear
created_attimestamptzNOT NULL default now()10-minute expiry window
consumed_attimestamptznullableSingle use

id_matter_members

ColumnTypeConstraintsWhy it exists
matter_iduuidPK (with user_id), FK dc_matters CASCADEWhich matter
user_iduuidPK (with matter_id), FK id_users CASCADEWhich user
roleparty_roleNOT NULL, unique index id_matter_members_role_idx (matter_id, role)A user occupies exactly one side; no two users can hold the same role on one matter
joined_attimestamptzNOT NULL default now()Audit

id_invitations

ColumnTypeConstraintsWhy it exists
iduuidPKIdentity
matter_iduuidNOT NULL, FK dc_matters CASCADETarget matter
invited_by_user_iduuidNOT NULL, FK id_usersInviter, for audit and revocation rights
invited_emailtextNOT NULLLowercased; must match the accepting user's email
role_offeredparty_roleNOT NULLAlways the opposite of the inviter's role
tokentextNOT NULL, unique index id_invitations_token_idxUnguessable UUID in the accept URL
statusinvitation_statusNOT NULL default 'pending'Lifecycle
created_at, expires_attimestamptzNOT NULL14-day TTL by default
accepted_by_user_iduuidnullable, FK id_usersWho accepted
Section 7.2

Schema: disclosure (dc_)

Enums: matter_mode, matter_status, relationship_type, employment_status, pool_item_kind, ownership, valuation_source. Category is stored as text (16 values in KIND_BY_CATEGORY) so new categories do not need a migration.

dc_matters

ColumnTypeConstraintsWhy it exists
iduuidPKMatter id; browser-generated UUIDs are accepted on first save
modematter_modeNOT NULL default 'solo'Solo or joint; one-way
statusmatter_statusNOT NULL default 'draft'Dashboard label
relationship_typerelationship_typeNOT NULLMarriage (s79) or de facto (s90SM)
cohabitation_start, separation_datedateNOT NULLRelationship length drives initial contribution erosion and the short-relationship note
owner_roleparty_roleNOT NULL default 'A'Role the creator occupies
created_at, updated_attimestamptzNOT NULL default now()Dashboard ordering

dc_parties (unique index dc_parties_matter_role_idx on matter_id, role)

ColumnTypeConstraintsWhy it exists
id, matter_id, roleuuid, uuid, party_rolePK; FK CASCADE; NOT NULLExactly one row per role per matter
display_nametextNOT NULLUsed in every explanation string
date_of_birthdatenullableAge gap factor (only when both present)
occupationtextnullableContext for the lawyer summary
employment_statusemployment_statusNOT NULL default 'employed'Zero income plus unemployed or carer triggers the strongest income factor
annual_incomebigintNOT NULL default 0Gross annual income in cents
health_or_capacity_concernbooleanNOT NULL default falseHealth factor flag; no free text is stored
primary_homemakerbooleanNOT NULL default falseHomemaker factor narrative
initial_contributionbigintNOT NULL default 0Assets brought in at cohabitation, cents
inheritances_or_giftsbigintNOT NULL default 0Received during the relationship, cents
participatingbooleanNOT NULL default falseHas this party signed in (joint mode)

dc_children (index dc_children_matter_idx)

id uuid PK; matter_id uuid FK CASCADE; first_name text NOT NULL; date_of_birth date NOT NULL. Age at assessment determines dependency (under 18) and the under-five uplift.

dc_pool_items (index dc_pool_items_matter_idx)

ColumnTypeConstraintsWhy it exists
id, matter_iduuidPK; FK CASCADEIdentity and ownership
kindpool_item_kindNOT NULLasset, liability or superannuation; derived from category in the domain
categorytextNOT NULLOne of 16 categories
descriptiontextNOT NULLHuman label
ownershipownershipNOT NULLjoint, A or B; drives allocation rules
agreed_valuebigintnullableDerived. Equal to both valuations when they match, otherwise null. Written by the repository from deriveAgreedValue, never by a user
retained_byparty_rolenullableWho wants to keep the item after settlement
discovery_reftextnullableProvenance: finding id from ad_discovery_requests.findings
created_attimestamptzNOT NULL default now()Ordering

dc_pool_item_valuations: the per-party valuation table

ColumnTypeConstraintsWhy it exists
item_iduuidPK (with party_role), FK dc_pool_items CASCADEOne row per party per item, at most two rows
party_roleparty_rolePK (with item_id)Whose claim this is
valuebigintNOT NULLClaimed value in cents; non-negative by domain rule
sourcevaluation_sourceNOT NULL default 'manual'manual, estimate_for_partner, discovery, document
notetextnullableFor example 'Accepted other party value'
recorded_attimestamptzNOT NULL default now()When the claim was made

Agreed value rule, in SQL terms. agreed_value = CASE WHEN count(*) = 2 AND min(value) = max(value) THEN min(value) END over the item's valuation rows. The repository computes this in TypeScript on save; a database trigger or generated view is a reasonable hardening step and is listed in the roadmap.

Section 7.3

Schema: parenting, discovery, phase two, security

pa_care_arrangements

ColumnTypeConstraintsWhy it exists
child_iduuidPK, FK dc_children CASCADEOne arrangement per child
matter_iduuidNOT NULL, FK dc_matters CASCADEDenormalised for membership checks and RLS
nights_with_a_per_fortnightsmallintNOT NULL default 70 to 14; B has the remainder
patterntextnullableFree text such as 'Week about, changeover Sunday 5pm'

ad_discovery_requests (index ad_discovery_requests_matter_idx)

ColumnTypeConstraintsWhy it exists
id, matter_iduuidPK; FK CASCADEIdentity
providerdiscovery_providerNOT NULLasic, ppsr, land_titles, ato_super, identity
subjectjsonbNOT NULLSubjectPerson: role, full name, optional DOB, other names, state
requested_by_roleparty_roleNOT NULLWho asked; accepted findings are valued for this party
statusdiscovery_statusNOT NULLqueued, running, complete, failed, blocked_tier, blocked_identity
findingsjsonbNOT NULL default '[]'Array of Finding including raw payload for audit
requested_at, completed_attimestamptzNOT NULL; nullableTiming
errortextnullableFailure detail

ph2_documents Phase two (table exists, only status 'uploaded' is reachable today)

ColumnTypeConstraintsWhy it exists
id, matter_iduuidPK; FK CASCADEIdentity
uploaded_by_roleparty_roleNOT NULLDocuments belong to a side
kinddocument_kindNOT NULLbank_statement, payslip, super_statement, tax_return, other
storage_pathtextNOT NULLObject path in the private Supabase Storage bucket
statusdocument_statusNOT NULL default 'uploaded'uploaded, queued, parsed, failed, rejected
extractionjsonbnullableStructured output per Section 14.1 schemas
parser_versiontextnullableReproducibility; re-parse when the parser changes
uploaded_at, parsed_attimestamptzNOT NULL; nullableTiming

ph2_disclosure_flags Phase two

ColumnTypeConstraintsWhy it exists
id, matter_iduuidPK; FK CASCADEIdentity
kinddisclosure_flag_kindNOT NULLgap, inconsistency, undisclosed_income, undisclosed_asset
severitysmallintNOT NULL default 21 low, 2 medium, 3 high
messagetextNOT NULLPlain-English statement of what was found
related_item_iduuidnullable, FK dc_pool_items SET NULLLinks a flag to the item it questions
source_document_iduuidnullable, FK ph2_documents SET NULLEvidence
statusdisclosure_flag_statusNOT NULL default 'open'open, dismissed, resolved
created_attimestamptzNOT NULL default now()Ordering

Indices

Unique: id_users_email_idx, id_invitations_token_idx, id_matter_members_role_idx (matter_id, role), dc_parties_matter_role_idx (matter_id, role). Non-unique: dc_children_matter_idx, dc_pool_items_matter_idx, ad_discovery_requests_matter_idx. Composite primary keys on dc_pool_item_valuations (item_id, party_role) and id_matter_members (matter_id, user_id) double as lookup indices. Recommended additions: id_sessions(user_id), id_invitations(matter_id, status), ph2_documents(matter_id, status), ph2_disclosure_flags(matter_id, status).

Row level security on Supabase

Today all database access goes through Next route handlers using a server connection, and authorisation is a membership check: the handler loads id_matter_members for the session user and the requested matter and returns 403 otherwise. RLS is therefore defence in depth rather than the primary control, and the policies are not yet in migration 0000. Recommended policy shape, applied to every table carrying matter_id:

alter table dc_pool_items enable row level security;
create policy matter_member_rw on dc_pool_items
  for all using (
    exists (
      select 1 from id_matter_members m
      join id_users u on u.id = m.user_id
      where m.matter_id = dc_pool_items.matter_id
        and u.auth_subject = auth.uid()::text
    )
  );
-- dc_pool_item_valuations joins through dc_pool_items.
-- id_users: a user may read and update only their own row.
-- ph2_documents: members may read; only the uploading role may delete.

The server role used by route handlers bypasses RLS, so the membership check in code remains mandatory. If a browser-side Supabase client is introduced later, these policies become the primary control.

Section 8

Settlement methodology

Plain statement. The engine in src/contexts/settlement-modelling/domain/engine.ts is a heuristic model. It is versioned (ENGINE_VERSION = '1.0.0'), deterministic (same inputs, same output, no randomness, no model calls), and explainable (every adjustment is a Factor with a plain-English reason and a legislative basis). It is not legal advice and it does not predict what a particular judge will do.

It follows the four-step approach the Federal Circuit and Family Court of Australia applies under s79 (married) and s90SM (de facto) of the Family Law Act 1975, as restated by the 2024 amendments.

flowchart LR
  S1["Step 1: Identify and value the pool"] --> S2["Step 2: Assess contributions"] --> S3["Step 3: Consider current and future circumstances"] --> S4["Step 4: Just and equitable check"]
  S1 --- N1["summariseDisclosure: low pool, high pool, referencePool = midpoint, disputed items, completeness"]
  S2 --- N2["Factors: INITIAL_CONTRIBUTION, INHERITANCE_GIFT, HOMEMAKER_PARENT. Percent band clamped 25 to 75"]
  S3 --- N3["Factors: INCOME_DISPARITY, CARE_OF_CHILDREN, HEALTH_CAPACITY, AGE_GAP. Points capped at 20"]
  S4 --- N4["Add points to contribution band, clamp 10 to 90, convert to dollars across low and high pool, attach notes, cautions, disclaimer"]
  style S1 fill:#EAF0F6,stroke:#103654
  style S2 fill:#EAF0F6,stroke:#103654
  style S3 fill:#EAF0F6,stroke:#103654
  style S4 fill:#FBE9DA,stroke:#E97819
    
assessSettlement as a pipeline. Each step's output is reported as a StepOutcome with a one-line summary.

Step 1, in plain English

Add up everything both people own and owe, including superannuation, which is shown separately because courts commonly treat it as its own pool. Where the two people disagree about a value, keep both: a 'low' pool using the values least favourable to a big total, and a 'high' pool using the most favourable. The model runs on the midpoint but reports dollars across the whole span.

Step 2, in plain English

Start at 50/50. Move toward the person who brought more in at the start, but less so the longer the relationship lasted, because early contributions get absorbed into the joint effort. Move toward the person who received an inheritance or gift during the relationship, at a discount because the money was usually spent on the household. Homemaker and parenting contributions are treated as equal in worth to earning income, so they do not move the number on their own; they offset the higher earner's financial contribution.

Step 3, in plain English

Look forward. Who earns less, or nothing? Who has the children most of the time, and are any under five? Does either person have a health problem that limits their capacity? Is one person much older with fewer working years left? Each of these moves the number toward the person with greater need, within a cap so future needs never swamp contributions.

Step 4, in plain English

Add the future needs adjustment to the contributions band, make sure the result stays inside 10% to 90%, convert to dollars across the low and high pool, and attach the notes about what widened the range and the cautions about what is missing. Then attach the disclaimer. The output is always a range.

Section 8.1

Exact rules and constants

Constants live in TUNING and are the only numbers a legal reviewer needs to argue with. Points are percentage points of the net pool moving toward party A; negative means toward B. band(low, mid, high) rounds to one decimal.

ConstantValueUsed by
initialContributionErosionYears20Weight of initial contributions falls linearly to the floor over 20 years
initialContributionMinWeight0.15Floor weight; an initial contribution never fully disappears
initialContributionCapPoints20Maximum shift from initial contributions
inheritanceWeight0.6Fixed weighting for inheritances and gifts
inheritanceCapPoints10Maximum shift from inheritances
contributionsFloor, contributionsCeiling25, 75Clamp on the step 2 percentage for A
contributionsBandNarrow, contributionsBandWide2.5, 5Minimum half-width of the contributions band; wide when the net shift exceeds 5 points
futureNeedsCapPoints20Clamp on the step 3 total (each of low, mid, high)
finalFloor, finalCeiling10, 90Clamp on the final percentage
smallPoolThreshold5,000,000 cents ($50,000)Below this a caution about need over contribution is added

Step 2 factors (group contributions)

FactorRule
INITIAL_CONTRIBUTIONSkipped if both parties' initial contributions are zero. weight = clamp(1 - years/20, 0.15, 1). diffShare = (A - B) / referencePool x 100 (0 if the pool is not positive). mid = clamp(diffShare x weight, -20, 20). spread = min(|mid| x 0.4, 5). Band is mid - spread, mid, mid + spread.
INHERITANCE_GIFTSkipped if both are zero. diffShare as above. mid = clamp(diffShare x 0.6, -10, 10). spread = min(|mid| x 0.4, 4).
HOMEMAKER_PARENTAlways present. Band is 0, 0, 0. Explains that homemaker and parenting contributions are treated as equal in worth to income and offset the higher earner's financial contribution.
CombinationSum the bands. bandWidth = |sum.mid| > 5 ? 5 : 2.5. contribMid = clamp(50 + sum.mid, 25, 75). Low is clamp(min(50 + sum.low, contribMid - bandWidth), 25, 75); high is clamp(max(50 + sum.high, contribMid + bandWidth), 25, 75).

Step 3 factors (group future_needs)

FactorRule
INCOME_DISPARITYSkipped if both incomes are zero. Identify the lower earner. If the lower earner has zero income and is unemployed or carer: band 7.5, 10, 12.5 toward them. Otherwise ratio = higher / lower (infinity if lower is zero): ratio below 1.25 gives no factor; 1.25 to under 1.75 gives 2, 2.5, 4; 1.75 to under 3 gives 4, 5, 7.5; 3 or more gives 5, 7.5, 10.
CARE_OF_CHILDRENSkipped if there are no dependent children (under 18) or no primary carer. base = min(2.5 x dependents, 7.5) + (any child under five ? 2.5 : 0). Band is base - 1, base, base + 2.5 toward the primary carer.
HEALTH_CAPACITYOnly when exactly one party has healthOrCapacityConcern. Band 1, 2.5, 4 toward that party.
AGE_GAPOnly when both dates of birth are present and the age difference is 10 years or more. Band 0, 1, 2.5 toward the older party.
CombinationSum the bands, then clamp each of low, mid and high to -20 to 20.

Step 4 arithmetic

lowA  = clamp(contrib.low  + min(fn.low, fn.high), 10, 90)
highA = clamp(contrib.high + max(fn.low, fn.high), 10, 90)
percentA = { low: min(lowA, highA), high: max(lowA, highA) }   // rounded to 0.1
percentB = { low: 100 - percentA.high, high: 100 - percentA.low }
amountA  = { low: max(pool.low.netTotal, 0) x percentA.low / 100,
             high: max(pool.high.netTotal, 0) x percentA.high / 100 }
amountB  = same shape with percentB

Range notes are added when items are disputed, when items have only one figure, and when the relationship is shorter than five years. Cautions are added for a small pool, for unvalued items, and when debts exceed assets on any view. The OUTPUT_DISCLAIMER constant is attached to every result.

Section 8.2

Worked example: family-primary-carer

Produced by running computeResults(snapshot, 'A', new Date('2026-09-16')) against the real scenario with npx tsx. Nothing below is estimated. Sarah Whitfield is A, David Whitfield is B. Married, cohabited 1 June 2009, separated 15 March 2026: 16.8 years.

Step 1: the pool

ItemKindOwnerSarahDavidStateKeeps
Family home, Ashgrove QLDassetjoint1,450,0001,380,000disputed (gap 70,000)A
Westpac home loanliabilityjoint520,000520,000agreed
Joint savingsassetjoint62,00062,000agreed
David everyday accountassetB9,00014,500disputed (gap 5,500)
2021 Kia Carnivalassetjoint38,00038,000agreedA
2023 Toyota HiluxassetB58,00058,000agreedB
Employee share plan (BHP)assetB95,00071,000disputed (gap 24,000)
Contentsassetjoint30,00030,000agreed
Car loan (Hilux)liabilityB31,00031,000agreed
Australian Retirement Trust (Sarah)superA61,00061,000agreed
UniSuper (David)superB486,000486,000agreed
Pool viewAssetsLiabilitiesSuperNet non-superNet total
Low (smaller asset claims)1,648,000551,000547,0001,097,0001,644,000
High (larger asset claims)1,747,500551,000547,0001,196,5001,743,500
Sarah's perspective1,742,000551,000547,0001,191,0001,738,000
David's perspective1,653,500551,000547,0001,102,5001,649,500

Counts: 8 agreed, 3 disputed, 0 awaiting, 0 unvalued; completeness 1.0. referencePool = (1,644,000 + 1,743,500) / 2 = $1,693,750.

Step 2: contributions

Both initial contributions are zero and both inheritance figures are zero, so INITIAL_CONTRIBUTION and INHERITANCE_GIFT are skipped. HOMEMAKER_PARENT records that Sarah is the primary homemaker with a 0, 0, 0 band. Sum mid is 0, so the narrow band width 2.5 applies. Contributions for Sarah: 47.5% / 50% / 52.5%.

Step 3: future needs

FactorWhy it firesLowMidHigh
INCOME_DISPARITYSarah has zero income and status carer; David earns $215,000+7.5+10+12.5
CARE_OF_CHILDRENSarah is primary carer of 3 dependents (Ella 14, Max 11, Ruby 4); base = min(7.5, 7.5) + 2.5 for Ruby = 10+9+10+12.5
HEALTH_CAPACITYNeither party flaggednot applied
AGE_GAPSarah 42, David 45; gap under 10 yearsnot applied
Sum, then cap at 2016.5 / 20 / 25 becomes+16.5+20+20

Step 4: result

lowA = 47.5 + 16.5 = 64.0; highA = 52.5 + 20 = 72.5; both inside 10 to 90. David: 27.5% to 36%. Dollars: Sarah $1,052,160 (64% of the low pool) to $1,264,037.50 (72.5% of the high pool); David $452,100 to $627,660. Range note: '3 items have different values from each party, so the dollar range spans both views of the pool.' No cautions.

Split scenarios (perspective A, pool $1,738,000)

ScenarioSarah %Sarah targetSarah retainsDavid retainsBalancing payment
Equal split50869,0001,075,000 (assets 1,534,000, debts 520,000, super 61,000)663,000 (assets 208,000, debts 31,000, super 486,000)Sarah pays David 206,000
Lower end641,112,320David pays Sarah 37,320
Midpoint68.31,187,054David pays Sarah 112,054
Upper end72.51,260,050David pays Sarah 104,000 plus a super split of 81,050

The Westpac loan follows the home Sarah keeps ('Follows the property Sarah Whitfield is keeping (refinance into their name)'), so her retained position is net of the mortgage. At an equal split the note reads 'Sarah Whitfield owes a balancing payment larger than their readily available cash. This usually means refinancing a property or selling an asset.' At the lower end David pays from cash or investments; at the midpoint he too would need to refinance or sell; at the upper end his non-super assets run out, so $104,000 is paid from cash-like assets and 'a superannuation splitting order of about $81,050 would move part of the balance'.

Section 8.3

Split modelling rules

modelSplit in split-modeller.ts turns a percentage into a practical picture. It uses effectiveValue from the chosen perspective, so the two parties can see slightly different dollar figures until they agree.

Allocation rules, in order

  1. Superannuation goes to its member (shared if jointly owned). Reason: 'Superannuation stays with the member unless a splitting order is made'.
  2. An item flagged retainedBy goes to that party. Reason: 'X has asked to keep this'.
  3. A solely owned item stays with its owner. Reason: 'Solely held, stays with the holder'.
  4. A joint mortgage with no retain flag follows the property when every retained real_property item is kept by the same party: it is allocated to that party with the reason 'Follows the property X is keeping (refinance into their name)'. If retained properties are split between the parties, or none is retained, the next rule applies.
  5. Jointly owned real estate, or a joint mortgage, with no retain flag is otherwise treated as sold and shared 50/50. Reason: 'Jointly owned, modelled as sold with proceeds shared' or 'Discharged from sale proceeds'. Half the sale value counts as cash-like for the payer.
  6. Any other joint item is shared equally.

Balancing the books

  • target = splitByPercent(max(poolTotal, 0), percentA); diffA = target.a - A.netRetained. Differences under one dollar are ignored.
  • If the payer's cash-like assets (bank accounts and shares allocated to them, plus half of shared real estate) cover the amount: a balancing payment 'from cash, investments or sale proceeds'.
  • Else if the payer's non-super net position covers it: a balancing payment, with the note that it usually means refinancing or selling.
  • Else: pay what cash-like assets allow, take the rest from the payer's superannuation as a superSplitSuggestion, and if a remainder is still unreachable, say so and suggest revisiting who keeps what.
  • Positions after adjustment and percentOfPool are reported for both parties. If the family home was modelled as sold, a note invites the user to mark it as kept to see the refinancing picture.

Standard scenarios

standardScenarios returns equal split (50), the low end, the midpoint and the high end of the indicative range, de-duplicated when two coincide. The results page shows these four first, before the range.

Why the mortgage follows the home. In the worked example Sarah keeps the home, so the Westpac loan is allocated to her and her retained position is net of it. This is what a refinance into one name looks like in practice, and it is why the balancing payment at an equal split is $206,000 rather than the far larger figure a sale-and-share model would show. Split modelling changes balancing payments only; the indicative range is untouched.

Section 9

Asset discovery

Deep asset discovery is the paid differentiator. Each registry is a DiscoveryProvider behind a port with one method, search(subject), so the mocks shipped today can be swapped for real adapters without touching the domain or the UI. Findings are leads; a person accepts them into disclosure.

CodeLabelWhat it returnsTierVerified identity
identityIdentity verificationVerifies who you are before searching registries in your own namefreeno
asicASIC companies and directorshipsOfficeholder and shareholder records; suggests business_interest, trust_interest (corporate trustee) or sharesdiscoveryno
ppsrPPSR security interestsRegistered security interests over vehicles and personal property; suggests personal_loandiscoveryno
land_titlesState land titlesRegistered proprietor searches across state and territory registries; suggests real_propertydiscoveryyes
ato_superATO superannuation lookupSuper accounts in your name including lost and unclaimed super; suggests accumulation or smsfdiscoveryyes

Gating

gateDiscovery(provider, tier, identityVerified) is a pure function: if the provider requires the discovery tier and the user is not on it, the request is blocked_tier; else if it requires verified identity and the user is not verified, blocked_identity; else queued. runDiscovery records the blocked request as-is (so the UI can explain), otherwise calls the adapter and records complete with findings or failed with the error. It never throws for expected outcomes.

sequenceDiagram
  autonumber
  actor U as User (discovery page)
  participant API as POST /api/matters/:id/discovery
  participant G as gateDiscovery
  participant R as runDiscovery
  participant P as Provider adapter (mock or real)
  participant DB as ad_discovery_requests
  participant DC as Disclosure
  U->>API: { provider: land_titles, subjectRole: B }
  API->>API: load session user (tier, identityVerified), check membership
  API->>G: gate(land_titles, tier, identityVerified)
  alt tier is free
    G-->>API: blocked_tier
    API->>DB: insert request status blocked_tier
    API-->>U: DiscoveryRequest (UI shows Upgrade to discovery)
  else identity not verified
    G-->>API: blocked_identity
    API->>DB: insert request status blocked_identity
    API-->>U: DiscoveryRequest (UI shows Verify identity)
  else queued
    API->>R: run(cmd, providers)
    R->>P: search(subject)
    P-->>R: Finding[] with confidence and suggestedItem
    R-->>API: request complete
    API->>DB: insert request with findings jsonb
    API-->>U: DiscoveryRequest with findings
    U->>API: POST .../findings/:findingId/accept
    API->>DC: createPoolItem with discoveryRef, recordValuation for requesting role, source discovery
    API-->>U: { item }
  end
    
A discovery run. Upgrade (POST /api/account/upgrade) and identity verification (POST /api/account/verify-identity) are separate calls, both mocked today.
Section 9.1

Mock versus real integration, tiering and pricing

The mock adapters

MockProvider is deterministic: results depend only on the subject's full name (case-insensitive), so demos and tests are repeatable. Two personas have canned findings, Marcus Delfino (ASIC x3, PPSR x1, land titles x1, ATO super x2, including an SMSF and a lost AMP account) and Priya Raman (land titles x1, an explicit 'no records' from ASIC, ATO super x1). Any other name returns one 'No records found' finding that suggests trying previous names. mockVerifyIdentity verifies anyone with a date of birth. Optional latency simulates a real call.

Real integration notes

ProviderReal sourceConstraints to design around
ASICASIC Connect and ASIC Registry Search (paid extracts), or a licensed information brokerPer-search fees; name-based search returns candidates that need disambiguation by date of birth or address; a person search reveals directorships but shareholdings need company extracts.
PPSRPPSR B2G interface, search by grantor (individual requires name and date of birth)Account registration, per-search fee, and the grantor search on an individual is restricted to permitted purposes; legal review of purpose is required.
Land titlesPer-state registries (QLD Titles Registry, NSW LRS, Landata VIC, Landgate WA, and others) via an aggregator brokerName searches are not offered in every state and are fee-bearing; results are title references that then need a title search for encumbrances. The verified-identity gate reflects registry terms of use.
ATO superATO Online via myGovThere is no third-party API for another person's super. The flow is user-consent driven for the user's own accounts. Discovery of a partner's super is by disclosure request, subpoena or court order, which the product should explain honestly.
IdentityA DVS-backed KYC vendor (document plus liveness)Store only the verification reference and result, never the document images.

Tiering

TierIncludesPrice
freeAnonymous modelling, saving, joint mode, results, identity verification, unlimited mattersFree
discoveryEverything in free plus all registry providers for both parties of a matter, and phase two document parsing when it shipsPlaceholder: one-off per-matter fee in the low hundreds of dollars, or pass-through registry fees plus a platform fee. To be validated against real registry costs before launch.

Tier is a property of the user (id_users.tier), so a discovery-tier user can search on any matter they are a member of. POST /api/account/upgrade is a mock payment today; the roadmap names Stripe Checkout for the real flow.

Section 10

Identity and authentication

The free tier needs no account. A user record is created only when someone chooses to save or invite. Authentication is delegated to the AuthProvider port; Settle then issues its own session.

Passwordless flows

StepSix-digit email codeMagic linkGoogle
StartPOST /api/auth/start { email, method: 'email_otp' } returns { challengeId }POST /api/auth/start { email, method: 'magic_link' }GET /api/auth/google?redirectTo= returns 302 to the provider URL
Provider (Supabase)auth.signInWithOtp with shouldCreateUser: true; challengeId is the lowercased email because Supabase verifies by (email, token)Same call with emailRedirectTo = NEXT_PUBLIC_APP_URL{SUPABASE_URL}/auth/v1/authorize?provider=google
Provider (local)Challenge kept in memory, code is always 123456, printed to the server log, expires after 10 minutesSame/api/auth/local-google signs in as google.demo@example.com
VerifyPOST /api/auth/verify { challengeId, code }; isValidOtp checks six digits client-side firstLink lands on the app, which exchanges the token and continues as verifyCallback exchanges code
ResultUpsert id_users by email (set auth_subject on Supabase), insert id_sessions, set httpOnly settle_session cookie, return { user }. Facebook is deliberately not offered.

Anonymous to saved migration

  1. The user presses Save or Invite. The app remembers the intent and sends them to /sign-in.
  2. After sign-in the app reads localStorage['settle.matter.v1'] and calls POST /api/matters { snapshot }. The server accepts the browser-generated ids, saves the aggregate, and creates a membership with snapshot.matter.ownerRole.
  3. The browser keeps working with the same snapshot shape and switches to PUT /api/matters/:id on change. The local copy is kept as a cache until the first successful save, then cleared to avoid two sources of truth.
  4. If the user already has matters, the dashboard offers to save the anonymous one as a new matter rather than merging.

Invitations and membership

  • createInvitation validates the email, lowercases it, offers the opposite role, generates a UUID token, and sets a 14-day expiry (ttlDays default).
  • acceptInvitation fails with INVITATION_NOT_PENDING, INVITATION_EXPIRED or INVITATION_EMAIL_MISMATCH ('Sign in with the email address the invitation was sent to'). Success adds a membership row; the unique (matter_id, role) index prevents two users holding the same role.
  • Inviting moves the matter to joint mode permanently (inviteToJoint).
  • Sessions are server-side rows with an expiry. Sign-out deletes the row and clears the cookie.
Section 11

API contract

Route handlers live under src/app/api. All bodies and responses are JSON. Auth is the settle_session httpOnly cookie. Errors are { error: { code, message } } with status 400 (validation or domain error), 401 (no session), 403 (not a member), 404 (not found). user is { id, email, displayName, tier, identityVerified }.

MethodPathBodyResponse
POST/api/auth/start{ email, method: 'magic_link' | 'email_otp' }{ challengeId }
POST/api/auth/verify{ challengeId, code }{ user } and sets cookie
GET/api/auth/me{ user | null }
POST/api/auth/signout{ ok: true }
GET/api/auth/google?redirectTo=302 to Google (local: signs in as google.demo@example.com)
GET/api/matters{ matters: [{ matterId, role, status, mode, partyNames, updatedAt }] }
POST/api/matters{ snapshot }{ matterId, role } (creates membership as snapshot.matter.ownerRole)
GET/api/matters/:id{ snapshot, role }
PUT/api/matters/:id{ snapshot }{ ok: true }
GET/api/matters/:id/results?perspective=AMatterResults
POST/api/matters/:id/invite{ email }{ invitation, acceptUrl } (acceptUrl for local demos)
POST/api/invitations/:token/accept{ matterId, role }
GET/api/matters/:id/discovery{ requests: DiscoveryRequest[] }
POST/api/matters/:id/discovery{ provider, subjectRole }DiscoveryRequest
POST/api/matters/:id/discovery/:requestId/findings/:findingId/accept{ item } (adds PoolItem with discoveryRef)
POST/api/account/upgrade{ user } tier becomes discovery (mock payment)
POST/api/account/verify-identity{ user } identityVerified becomes true (mock)
GET/api/demo/scenarios{ scenarios: [{ slug, title, tagline, tier, accounts }] }
POST/api/demo/:slug/login{ role: 'A' | 'B' }{ matterId, user } and sets cookie

Conventions

  • Request bodies are validated with zod at the edge; domain errors (DomainError.code) map to 400 with the domain message, which is written for end users.
  • The snapshot is validated as a whole. The server recomputes agreedValue on save and ignores any client-supplied value.
  • In joint mode a PUT from role A may only change A's valuations, A's party row, shared structure (items, children, care) and retain flags; B's valuations are preserved from the stored copy. This prevents one side rewriting the other's claims.
  • GET /api/demo/scenarios and POST /api/demo/:slug/login exist for demos only. They sign a visitor in as a seeded demo account without a code, so they must be disabled in production behind an environment flag (see Appendix B); the handler returns 404 when the flag is off.
  • Phase two endpoints are listed in Section 14 and are not implemented.
Section 12

UI theme and accessibility

Tokens are exported from src/lib/brand.ts as THEME and mirrored as Tailwind theme values. Red is reserved for warnings and destructive actions; disagreement is never red, because disagreement is normal.

TokenHexUse
navy #103654Headings, primary buttons, party A in charts, navigation
terracotta #E97819The one primary action per screen, links, active states, party B in charts, disputed chips, callout borders
surface #F5F2EAPage background
slate #5B6B7ASecondary text, awaiting chips
ink #1B2733Body text
white#FFFFFFCards
success #2E7D5BAgreed chips, saved confirmations
danger #B42318Disclaimer callout border, destructive confirmations (remove item, revoke invitation, delete matter) only

Typography and tone

Plus Jakarta Sans (or the system stack when offline), 16px base, 1.6 line height, generous whitespace. Copy is calm and concrete: 'David pays Sarah $112,054' rather than 'a transfer may be required'. Second person, no jargon without a plain-English gloss, and every legislative term shown with its basis string from the factor.

Accessibility commitments (WCAG 2.2 AA)

  • Colour is never the only carrier of meaning: chips carry text, party bars carry labels, the range shows numbers.
  • Contrast: navy and ink on white exceed 7:1; terracotta on white is used for text at 18px bold or larger, or paired with an underline for links; body-size terracotta text is avoided.
  • All forms have visible labels, described errors (aria-describedby), and the OTP input accepts paste of six digits.
  • Keyboard: the stepper, chips with actions and scenario cards are reachable in DOM order; focus rings are visible on the navy palette.
  • Money inputs accept whole dollars and format on blur; screen readers hear 'one million four hundred fifty thousand dollars', not digit strings.
  • Reduced motion respected for the scenario rail; no auto-advancing content.
  • The disclaimer is a landmark region (role="note", aria-label="Important notice") so it is discoverable by rotor.
Section 12.1

Mobile and React Native readiness

The client wants a web app that works well on a phone now and a path to a native app later. Both are designed in, and docs/MOBILE.md is the working reference.

The web app is mobile-first

ConcernBehaviour
Targets390px is the primary design width; 360px is checked. Desktop is the enhancement, not the baseline.
NavigationHeader collapses to a hamburger under 768px. The matter stepper is a horizontally scrollable chip row above each screen.
ResultsSplit scenarios collapse to a one-line summary with an expander; a sticky indicative-range bar keeps the range one tap away while scrolling; the disclaimer stays on the range card.
DisclosureA sticky bottom bar carries the add-item action so it is reachable from any row.
Touch and input44px minimum touch targets; money inputs use inputMode="numeric"; dates use native pickers; the OTP field accepts a pasted six-digit code.
Device chromeviewport-fit=cover with safe-area inset padding so sticky bars clear the home indicator.
Installablepublic/manifest.webmanifest: name Settle, display: standalone, portrait, theme_color #103654, background_color #F5F2EA, SVG plus 192 and 512 PNG icons, alongside Apple web-app meta tags. Adding to the home screen is the cheapest 'app' to ship first.

The React Native path

  • Platform-agnostic core. src/core/index.ts is a barrel of everything that runs unchanged in the browser, Node and Hermes: the five contexts' domain code (plus runDiscovery), the MatterSnapshot read model and computeResults, the demo scenarios, createApi({ baseUrl, fetchImpl }), format helpers, BRAND and THEME, and the StorageAdapter type. tests/unit/core/platform-agnostic.test.ts walks its transitive imports and fails if any file touches window, document, localStorage, next or react. This is the future @settle/core workspace package.
  • Persistence through a port. src/lib/client/storage.ts defines StorageAdapter { getItem, setItem, removeItem } where each method may return a value or a Promise. webLocalStorageAdapter wraps localStorage with try/catch so private mode and quota errors degrade to no-ops; memoryStorageAdapter serves tests and server rendering; an Expo app supplies an AsyncStorage adapter. Anonymous matters use the same key settle.matter.v1 on every platform, so a snapshot moves between web and native with no conversion.
  • Same API, one change. The native app points createApi at the deployed Next.js API. The API today trusts the settle_session httpOnly cookie; native clients need it to also accept Authorization: Bearer <Supabase access token> and resolve the user the same way. Route shapes do not change; the mobile app injects the header through fetchImpl.
  • Screens mirror the page list. Expo Router screens use the same route names as Section 4: start with a segmented control and native date pickers, children with a 0 to 14 nights slider, disclosure as an assets/debts/super tab bar with card rows and a floating add button, results with collapsed scenario cards and a sticky range bar, share using the native share sheet. Only the UI primitive layer is rewritten; providers and hooks are plain React and port as they are.
  • Auth on device. Supabase Auth directly with expo-secure-store as the session store: email OTP through signInWithOtp and verifyOtp with one-time-code autofill, Google through expo-auth-session and a settle://auth/callback scheme, Facebook still unsupported.
  • Deep links. Register settle:// and the universal link domain and map /invite/[token] to the invite screen; invitation emails keep the web URL, which opens the app when installed.
  • Stays web only. API routes, Postgres and Drizzle, server-side auth providers, the SEO landing page, redirect-based Google sign-in, clipboard fallbacks and the PWA manifest.
flowchart TB
  subgraph core["@settle/core (today src/core/index.ts)"]
    direction LR
    DOM["Five contexts' domain + runDiscovery"]
    RM["MatterSnapshot, computeResults"]
    DEMO["Demo scenarios"]
    API["createApi({ baseUrl, fetchImpl })"]
    FMT["format helpers, BRAND, THEME, StorageAdapter type"]
  end
  subgraph web["apps/web: Next.js"]
    direction LR
    WUI["React pages + Tailwind"]
    WST["webLocalStorageAdapter"]
  end
  subgraph rn["apps/mobile: Expo React Native"]
    direction LR
    RUI["Expo Router screens + RN primitives"]
    RST["asyncStorageAdapter"]
    SEC["expo-secure-store session"]
  end
  SRV["Next.js API on Vercel: cookie session today, plus bearer token for native"]
  DB[("Postgres")]
  WUI --> core
  RUI --> core
  WUI -->|"settle_session cookie"| SRV
  RUI -->|"Authorization: Bearer (Supabase token)"| SRV
  SRV --> DB
  style core fill:#FBE9DA,stroke:#E97819
  style rn stroke-dasharray: 5 5
    
Web and native shells consume the same core package and the same API. The dashed Expo app is planned; the core barrel, storage port and API factory exist today.

Sequence: extract src/core to packages/core and switch web imports (the guard test moves with it); add bearer-token support to the API auth helper; scaffold apps/mobile with MatterProvider on the AsyncStorage adapter; build screens in the order users meet them, results getting the most design time; then auth, invite deep link, dashboard, discovery and demo.

Section 13

Testing strategy and coverage

Vitest runs unit and integration tests; Playwright runs a thin end-to-end layer. vitest.config.ts sets the coverage gates for CI: lines 80, functions 80, branches 75, statements 80, measured with v8 over src/contexts, src/read-models, src/demo, src/db, src/lib and src/components, excluding type declarations and the Supabase auth provider (exercised only against a live project).

Current state

557 tests across 38 files pass via npm test. npm run test:coverage reports lines 97%, statements 97%, functions 95%, branches 96%, comfortably above the gates. Three Playwright journeys run on two projects via npm run test:e2e.

Commands

npm test unit and integration; npm run test:coverage with thresholds; npm run test:e2e Playwright (starts next start on port 3000 unless E2E_NO_SERVER is set); npm run demo:video records the walkthrough.

Playwright e2e3 journeys x 2 projects (Desktop Chrome, Pixel 7)
Integration (Vitest, real Postgres settle_test)repository, server services, route handlers
Unit (Vitest, node and jsdom)domain rules, engine, split modeller, gating, client state, components, core guard
LayerLives inWhat it covers
Unittests/unit/**/*.test.ts(x): shared, disclosure, parenting, settlement-modelling, asset-discovery, identity, read-models, demo, server, client, components (jsdom via environmentMatchGlobs) and core. Fixtures in tests/unit/helpers/fixtures.ts.Money rounding and exact splits; agreed value and agreement states; low and high pools; care bands and the cost percent table; every engine factor boundary, cap and clamp; determinism and six scenario snapshots; allocation order including the mortgage-follows-property rule, balancing and super split fallback; discovery gating and mocks; invitation lifecycle and OTP shape; storage adapters and the API client; UI components. tests/unit/core/platform-agnostic.test.ts fails if the src/core barrel transitively imports window, document, localStorage, next or react.
Integrationtests/integration/**/*.test.ts against a real Postgres database settle_test created through createDb: db-client, matter-repository and server/.MatterRepository save and load round trip preserving the snapshot and recomputing agreed_value; server services for matters, invitations, discovery, demo seeding and auth; a selection of route handlers end to end against the database.
End to endtests/e2e/journeys.spec.ts, Playwright projects desktop (Desktop Chrome) and mobile (Pixel 7).Three journeys: an anonymous user starts a matter, discloses an asset and sees a ranged result; a demo scenario shows splits first, then an indicative range with the disclaimer attached; a signed-in discovery run surfaces the hidden company and adds it to disclosure.

Video walkthrough

scripts/record-walkthrough.ts (npm run demo:video) drives the app in Playwright, records six captioned chapters (intro, anonymous modelling, joint mode, complex pool and discovery, sign-in and share, mobile) with on-screen title cards and captions, and stitches them with ffmpeg into docs/video/walkthrough.mp4. It is regenerated on each tagged build and is the release artefact for client review.

Rules of the pyramid

  • The engine is tested with golden snapshots of all six demo scenarios; a change to TUNING must update the snapshots and bump ENGINE_VERSION.
  • Domain tests never touch the database. Repository tests never assert on engine output.
  • E2E tests use the local auth provider and the demo login endpoint, never a mail inbox.
  • The platform-agnostic guard runs with the unit suite so a stray browser import into the domain fails the build, not the native app.
Section 14 Phase two: designed, not built

Phase two: AI document parsing

Financial disclosure is slow because people transcribe documents by hand. Phase two lets a party upload bank statements, payslips, superannuation statements and tax returns; a worker extracts structured facts; a rule-based checker compares those facts with the disclosed pool and raises flags. Everything the AI produces is a proposal a person accepts or dismisses.

sequenceDiagram
  autonumber
  actor U as Party (browser)
  participant API as Route handlers
  participant STO as Supabase Storage (private bucket)
  participant DB as Postgres
  participant Q as Queue
  participant W as Parsing worker
  participant M as Extraction model
  participant C as Disclosure checker (rules)
  U->>API: POST /api/matters/:id/documents (kind, file)
  API->>STO: put object under matters/:id/:docId
  API->>DB: insert ph2_documents status uploaded
  API-->>U: { document }
  U->>API: POST /api/matters/:id/documents/:docId/parse
  API->>DB: status queued
  API->>Q: enqueue { documentId }
  Q->>W: deliver job
  W->>STO: fetch object (signed URL, short lived)
  W->>W: classify kind, OCR if scanned, redact account numbers to last 4 digits
  W->>M: extract with JSON schema for the kind
  M-->>W: extraction JSON + confidence per field
  W->>W: validate against schema, sanity checks (dates, totals reconcile)
  W->>DB: update ph2_documents extraction, parser_version, status parsed (or failed)
  W->>C: run checks for matter
  C->>DB: read dc_parties, dc_pool_items, dc_pool_item_valuations, all parsed extractions
  C->>DB: upsert ph2_disclosure_flags (open), skip duplicates
  U->>API: GET /api/matters/:id/flags
  API-->>U: flags with suggested actions
  U->>API: POST /api/matters/:id/documents/:docId/suggestions/:n/accept
  API->>DB: createPoolItem or recordValuation (source document), flag resolved
    
Pipeline. The model sees a redacted document and returns structured JSON; the checker is deterministic rules over that JSON and the disclosed pool.

Components

ComponentDesign
StoragePrivate Supabase Storage bucket; object path matters/{matterId}/{documentId}; access only via short-lived signed URLs from the server; encryption at rest by Supabase.
Queue and workerOptions: a Vercel background function triggered by a queue, or a Supabase Edge Function on a database webhook. Decision open; the interface is { documentId }.
Extraction modelA hosted LLM with structured output constrained to the per-kind JSON schema. Prompt receives the redacted document text or page images and the schema, nothing about the pool or the other party.
CheckerPure TypeScript in a new bounded context, documents, with rules R1 to R8 (next page). Runs after every parse and whenever the pool changes.
Versioningparser_version on each document; re-parse on upgrade; flags reference the source document so evidence survives re-parsing.
Section 14.1 Phase two

Extraction schemas per document type

Stored in ph2_documents.extraction. Every money value is integer cents. Every field carries the page it came from and a confidence 0 to 1 in a parallel meta block (omitted below for brevity). Account numbers are kept as last four digits only.

bank_statement

{
  "kind": "bank_statement",
  "institution": "Westpac",
  "accountLast4": "4471",
  "accountType": "transaction",
  "holders": ["Sarah Whitfield", "David Whitfield"],
  "period": { "from": "2026-01-01", "to": "2026-03-31" },
  "openingBalance": 5810000,
  "closingBalance": 6200000,
  "transactions": [
    { "date": "2026-02-01", "description": "Salary BHP",
      "amount": 1245000, "category": "income" },
    { "date": "2026-02-03", "description": "Transfer to 88-2210",
      "amount": -200000, "category": "transfer_out",
      "counterpartyLast4": "2210" },
    { "date": "2026-02-05", "description": "Westpac home loan",
      "amount": -412000, "category": "loan_repayment" }
  ],
  "recurring": [
    { "description": "Transfer to 88-2210", "cadence": "monthly",
      "typicalAmount": -200000 }
  ]
}

payslip

{
  "kind": "payslip",
  "employee": "David Whitfield",
  "employer": "BHP Group Ltd",
  "employerAbn": "49004028077",
  "payDate": "2026-03-12",
  "period": { "from": "2026-02-27", "to": "2026-03-12" },
  "gross": 826900,
  "net": 561200,
  "tax": 231400,
  "superContribution": 95100,
  "superFund": "UniSuper",
  "ytd": { "gross": 15310000, "asAt": "2026-03-12",
           "financialYearStart": "2025-07-01" },
  "annualisedGross": 21600000,
  "allowances": [], "deductions": [
    { "description": "Salary sacrifice super", "amount": 50000 }
  ]
}

super_statement

{
  "kind": "super_statement",
  "member": "David Whitfield",
  "fund": "UniSuper", "fundAbn": "91385943850",
  "memberNumberLast4": "3391",
  "productType": "accumulation",
  "asAt": "2026-06-30",
  "balance": 48600000,
  "preserved": 48600000,
  "insurance": { "death": 50000000, "tpd": 50000000 },
  "contributionsYtd": { "employer": 2050000, "personal": 0 }
}

tax_return (notice of assessment or return summary)

{
  "kind": "tax_return", "taxpayer": "Marcus Delfino", "financialYear": "2025",
  "taxableIncome": 32000000,
  "incomeItems": [
    { "label": "Salary and wages", "amount": 18000000 },
    { "label": "Dividends (franked)", "amount": 2400000, "source": "Delfino Holdings Pty Ltd" },
    { "label": "Trust distributions", "amount": 9500000, "source": "Delfino Family Trust" },
    { "label": "Rental income", "amount": 3600000, "propertyHint": "Bulimba QLD" }
  ],
  "deductions": [{ "label": "Rental property expenses", "amount": 1500000 }],
  "entitiesMentioned": [
    { "name": "Delfino Holdings Pty Ltd", "role": "shareholder" },
    { "name": "Delfino Family Trust", "role": "beneficiary" }
  ],
  "hasBusinessSchedule": true, "hasRentalSchedule": true, "hasCgtEvents": false
}
Section 14.2 Phase two

Gap and inconsistency detection

Rules are deterministic comparisons between extractions and the disclosed pool. Each produces at most one open flag per (rule, subject) pair. Severity: 1 informational, 2 should be resolved, 3 likely undisclosed asset or income.

RuleKindSevConditionSuggested action
R1 missing documentsgap1An employed or self-employed party has no payslip or tax return; any party has no super statement; any bank_account item has no statementRequest upload from that party
R2 income mismatchinconsistency2|annualisedGross - party.annualIncome| / annualIncome > 0.15, or tax return taxable income differs by more than 20%Update income to the documented figure (party confirms)
R3 unknown accountundisclosed_asset3Recurring transfer to a counterpartyLast4 that matches no disclosed bank_account, mortgage or loanAdd bank_account item, unvalued, owner = uploader's counterpart per statement holders
R4 super not disclosedundisclosed_asset3Payslip superFund or super statement fund has no matching superannuation item for that partyAdd accumulation or smsf item with balance as valuation, source document
R5 balance mismatchinconsistency2Statement closing balance or super balance differs from the party's own valuation by more than 10% and $1,000Offer 'use documented value' for that party's claim
R6 undisclosed entityundisclosed_asset3Tax return dividends, trust distributions or entitiesMentioned with no matching business_interest, trust_interest or shares itemAdd item unvalued; also suggest an ASIC discovery run
R7 undisclosed propertyundisclosed_asset3Rental income or rental schedule with no real_property item beyond the family home, or loan repayments to a lender with no matching mortgageAdd real_property or mortgage item; suggest land titles discovery
R8 undisclosed incomeundisclosed_income2Recurring credits categorised income from a payer not matching the party's employer, exceeding 10% of disclosed incomeAsk the party to explain or update income

Human in the loop

  • Flags appear to both parties in joint mode, to the uploader only in solo mode. The party the flag concerns can resolve or dismiss; the other party sees the status. Dismissals record who and when.
  • Accepting a suggestion goes through the existing domain functions (createPoolItem, recordValuation with source: 'document'). The other party's claim is never touched.
  • Low-confidence fields (below 0.7) are shown for confirmation before any suggestion is offered.

Integration points

PointDetail
Tablesph2_documents (exists), ph2_disclosure_flags (exists). related_item_id links to dc_pool_items; source_document_id links to the evidence.
Enum reusevaluation_source = 'document' already exists so documented values are distinguishable from manual ones.
New endpointsPOST /api/matters/:id/documents, GET /api/matters/:id/documents, POST /api/matters/:id/documents/:docId/parse, DELETE /api/matters/:id/documents/:docId, GET /api/matters/:id/flags, POST /api/matters/:id/flags/:flagId/dismiss, POST /api/matters/:id/flags/:flagId/resolve, POST /api/matters/:id/documents/:docId/suggestions/:n/accept.
UIDocuments tab on /matter/disclosure; flag badges on affected rows; a 'Disclosure check' panel on results listing open flags as a caution.
EngineNone. The engine reads pool items exactly as before; flags only affect the cautions list shown next to results.
TierParsing is part of the discovery tier; uploads and R1 are free.

Non-goals. The AI never sets a valuation, never edits a party's claim, never decides a split, and never sees the other party's documents when extracting. It reads documents and proposes; a person decides. The settlement engine remains a published, deterministic method with no model in the loop.

Section 15

Security, privacy and compliance

Australian Privacy Principles

PrincipleHow Settle meets it
APP 1, 5 (open management, notification)A plain-English privacy policy linked from every page footer and from sign-in, stating what is collected, why, where it is stored (Supabase region: Sydney), and how to delete.
APP 3 (collection)Data minimisation by design: no account for the free tier; health is a boolean flag with no free text; children are first name and date of birth only; identity verification stores a reference, not documents; account numbers become last four digits in phase two.
APP 6 (use and disclosure)A party's data is visible only to matter members. Estimates entered about a partner belong to the entering party and are labelled as estimates. No sale or sharing with third parties; registry queries send only the subject fields needed for the search.
APP 8 (cross-border)Hosting and storage in Australia. Phase two extraction models must be hosted in-region or under a contract with equivalent protections; documents are redacted before leaving the platform.
APP 11 (security)TLS everywhere, httpOnly session cookies, server-side sessions with expiry, membership checks on every matter route, RLS as defence in depth, encryption at rest, least-privilege service keys held only in Vercel environment variables.
APP 12, 13 (access, correction)Users can export their snapshot as JSON and edit any of their own data at any time.
DeletionDeleting a matter cascades through every prefixed table via foreign keys. Deleting an account removes memberships and sessions; matters with another remaining member are retained for that member with the leaving party's rows anonymised. Phase two objects in Storage are deleted with the document row.

Family law and financial services boundaries

  • Settle is not a legal practice and does not give legal advice. The OUTPUT_DISCLAIMER is rendered on every result. Marketing copy uses 'modelling' and 'indicative', never 'entitlement' or 'what you will get'.
  • Settle does not give financial product advice. It reports balances and splits the user entered; it does not recommend funds, products or investment actions. Super split suggestions are described as an amount that 'would need to move', with the note that a splitting order requires legal process.
  • The duty of full and frank disclosure belongs to the parties. Discovery and phase two flags help a party meet it and do not replace formal disclosure or court processes.
  • Family violence: the invitation flow never reveals the inviter's location or IP; a party can leave a matter; support links to 1800RESPECT and Family Relationship Advice Line are in the footer. Sensitive data about the other party is limited to what settlement needs.

Application security notes

  • Invitation tokens are UUIDs, single use, 14-day expiry, and require the accepting email to match; there is no enumeration endpoint.
  • OTP codes are hashed at rest, expire in 10 minutes and are single use; rate limiting on /api/auth/start per email and IP is required before launch.
  • Route handlers validate every body with zod and never trust client-supplied agreedValue, tier or identityVerified.
  • Destructive actions (remove item, revoke invitation, delete matter, delete document) require confirmation and are the only red interface elements.
Section 16

Roadmap and open questions

PhaseScope
1 (now)Domain contexts, engine 1.0.0, split modeller, snapshot read model, schema and migration 0000, demo scenarios, mock discovery, local and Supabase auth providers, API contract, App Router pages.
1.5 hardeningRLS policies migration; rate limiting; disable the demo login endpoint in production; bearer-token auth for native clients; Stripe Checkout for the discovery tier; transactional email for magic links and invitations; JSON export and a printable lawyer summary; legal review of TUNING and factor copy.
1.6 nativeExtract src/core to packages/core, add bearer-token auth, scaffold the Expo app (Section 12.1).
2 documentsUpload, parsing worker, extraction schemas, checker rules R1 to R8, flags UI, human-in-the-loop, privacy review of the extraction vendor.
3 real discoveryASIC and PPSR adapters via a broker; land titles aggregator for QLD, NSW and VIC first; DVS-backed identity vendor; ATO super guidance flow (user's own accounts by consent).
LaterParenting plan drafting aid; consent order preparation checklist for lawyers; mediator and lawyer sharing links; WA de facto specifics.

Open questions

QuestionNotes
Product name'Settle' is a working title. Trade mark search and domain availability needed; rename is a one-line change in brand.ts.
PricingPer-matter one-off versus subscription; whether registry fees pass through; whether both parties can share one discovery purchase. Depends on real registry costs.
ATO super accessNo third-party API exists for another person's super. The honest product is guidance for the user's own lookup plus a disclosure request template for the partner's. Decide how prominently to market this provider.
Land titles name searchNot offered in every jurisdiction and fee-bearing. Which states at launch, and whether a broker relationship is viable at the target price.
PPSR permitted purposeIndividual grantor searches are restricted; legal advice needed on whether property settlement is a permitted purpose for a platform acting for a user.
Mortgages when properties are splitResolved for the common case: a joint mortgage follows the party who keeps the property (Section 8.3). Still open: when each party keeps a different property, the modeller falls back to sale-and-share for joint loans; a per-loan link to its secured property would be more precise.
Defined benefit superThe category exists but values are entered as a lump sum. Whether to add a family law valuation helper.
De facto thresholdsThe engine does not check the two-year or child-of-the-relationship gateway for de facto claims, nor the two-year limitation after separation. Decide whether to warn.
Legal review of constantsAll TUNING values and factor bands need family law practitioner review before public launch; version bump on change.
AnalyticsNone included. If added, privacy-preserving and self-hosted, never on matter pages with financial data in URLs.
Appendix

Appendix

A. Legal and domain glossary

s79 / s90SMFamily Law Act 1975 provisions for altering property interests of married and de facto couples respectively.
s75(2) / s79(5) factorsThe current and future circumstances a court considers (income, earning capacity, care of children, age, health). The engine's step 3.
Just and equitableThe overriding requirement that any order be fair in all the circumstances. The engine's step 4.
Full and frank disclosureEach party's duty to disclose all relevant financial information. What the disclosure context and phase two support.
Superannuation splitting orderA court order or agreement that transfers part of one party's super to the other. What superSplitSuggestion describes.
Binding financial agreementA private agreement requiring independent legal advice for each party. Referenced in the disclaimer.
Consent ordersCourt orders made by agreement without a hearing. Out of scope; the lawyer prepares them.
Division 7A loanA loan from a private company to a shareholder or associate, treated as a debt to the company. Appears in the complex demo as a tax_debt.
PPSRPersonal Property Securities Register; records security interests such as car finance.
SMSFSelf-managed superannuation fund.
Care percentageServices Australia's measure of nights of care per year, banded into below regular, regular, shared, primary and above primary.

B. Environment variables

VariablePurposeDefault
DATABASE_URLPostgres connection string (Supabase pooled string in production)postgres://postgres:postgres@localhost:5432/settle_dev
NEXT_PUBLIC_APP_URLPublic origin, used for magic link redirects and accept URLshttp://localhost:3000
NEXT_PUBLIC_SUPABASE_URLSwitches auth to Supabase when set together with the anon keyunset (local auth)
NEXT_PUBLIC_SUPABASE_ANON_KEYSupabase anon key for the auth clientunset
DEMO_ENABLEDEnables /api/demo/* (scenario list and demo login). Must be unset or false in production; the handlers return 404 when disabledtrue in development
NODE_ENVisProduction() toggles secure cookies and log verbositydevelopment
SUPABASE_SERVICE_ROLE_KEY Phase twoServer-side Storage access for documentsunset
DOCUMENTS_BUCKET, EXTRACTION_MODEL, QUEUE_URL Phase twoParsing pipeline configurationunset
STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET LaterReal payments for the discovery tierunset

C. Repository layout

src/
  contexts/                    bounded contexts (DDD). No context imports another's infrastructure.
    shared/domain              Money (integer cents), Result, ids, dates
    identity/                  users, sessions, tiers, invitations; AuthProvider port
      infrastructure/          LocalAuthProvider (dev: code 123456), SupabaseAuthProvider (prod)
    disclosure/domain          Matter, Party, Child, PoolItem with per-party valuations, agreedValue rule
    parenting/domain           CareArrangement, care %, cost %, primary carer
    settlement-modelling/      assessSettlement (4-step, deterministic), modelSplit, standardScenarios
    asset-discovery/           domain (port, gating), application (runDiscovery), infrastructure (mock-providers)
  read-models/matter-snapshot  MatterSnapshot = whole matter as one JSON aggregate; computeResults()
  demo/scenarios               six demo scenarios built via domain constructors (deterministic ids)
  db/                          Drizzle schema (prefixes id_, dc_, pa_, ad_, ph2_), client, MatterRepository
  lib/                         brand + theme tokens, server helpers (env, http, repositories)
  app/                         Next.js App Router (UI + /api route handlers)
drizzle/                       0000_calm_triathlon.sql and meta
tests/unit, tests/integration, tests/e2e
docs/ARCHITECTURE.md, docs/spec/

D. Document conventions

Straight quotes throughout. Money in the prose is whole dollars; money in code and the database is integer cents. 'A' and 'B' are roles, never genders. Dates are ISO in code and Australian in prose. This document was generated against the repository state on 16 September 2026 and should be regenerated when ENGINE_VERSION, the schema, or the API table changes.