Sooner

Sooner finance schema working doc

Working model for the future backend payload, form field catalog, and V2 schema vocabulary. The goal is a clean consumer-finance shape that the form UI can adapt to and that can later slot into Sooner V2 without overfitting to UAE-only fields.

Sources: local APP-1832/APP-1833 notes, Granola note “Sooner Forms Backend” from May 21, 2026, and Sooner fields catalog.

Future Field Catalog

The catalog should be organized around the target backend payload and finance entities, not around the current visual form shape. The UI can adapt its screens and controls to this contract. Prefer nested camelCase payload paths that line up with the backend contract, then map to Prisma fields and lower snake case stored enum values where needed.

Naming Layers

Layer Recommended style Example Why
Frontend form state and API JSON Nested camelCase paths application.propertySearchStatus, parties[0].person.firstName Matches TypeScript and React form conventions while preserving backend structure.
Prisma model fields camelCase fields on PascalCase models Application.propertySearchStatus, Person.firstName Matches Prisma and generated client conventions.
Database tables and columns lower snake case through Prisma mapping application.property_search_status, person.first_name Keeps database names conventional without forcing frontend field names to be snake_case.
Stored enum values lower snake case identified, searching, not_started, co_applicant Matches the naming standard for persistent enum values.

Structured Payload Direction

Field names in this catalog are proposed backend-facing payload paths, not a mirror of the current input components. The submitted payload should prefer statuses and repeatable child arrays where the concept has more than two states or can occur more than once.

{
  "application": {
    "propertySearchStatus": "identified",
    "mortgageSearchStatus": "searching"
  },
  "propertyPreference": {
    "targetAmount": 2500000,
    "currency": "AED"
  },
  "parties": [
    {
      "role": "primary_applicant",
      "person": {
        "firstName": "Amina",
        "middleName": null,
        "lastName": "Khan",
        "legalFullName": "Amina Khan"
      },
      "contactMethods": [
        { "type": "email", "value": "amina@example.com" },
        { "type": "phone", "value": "+971..." }
      ],
      "employment": [],
      "incomeSources": [],
      "liabilities": []
    }
  ]
}

Metadata JSON Column

Use a metadata JSON column only for traceability, UI/progressive-form state, imported raw values, and display-only values that do not need their own lifecycle or query surface yet. Metadata belongs on the record that owns the meaning. Store metadata object keys in lower_snake_case, even when the API payload uses camelCase.

Metadata owner Use for Example keys Do not use for
application.metadata Application-wide imported form state, UI progression, non-canonical display values, and source payload snapshots. forms.property_options, forms.locked_option_id, forms.property_options_completed_at, source_payload_version Applicant identity, parties, employment, income, liabilities, documents, consents, or lender outcomes.
application_property_preference.metadata Optional typed property-intake detail that is not a durable property entity. specific_property.source_url, specific_property.location_text, specific_property.stated_amount, specific_property.availability_status, specific_property.tenure_type Search intent that should be queryable, such as target amount, locations, property types, bedroom/bathroom choices, timeline, intended use, or search status.
application_cost_estimate.metadata, fee_quote.metadata, affordability_assessment.metadata Calculation inputs, formula versions, displayed breakdowns, or provider payload fragments that support an auditable snapshot. display.breakdown_items, calculation.rule_version, source.input_snapshot Values that need first-class reporting, filtering, or workflow status.
document.metadata, document_review.metadata, verification_event.metadata Provider response fragments, extraction hints, page metadata, and review tool state. provider.raw_status, extraction.page_count, review.tool_version The evidence target, requirement status, document type, country, or verification status.

Gabriel Handoff Entity List

These are the entities currently referenced by the field catalog rework. The first group is what Gabriel should shape the form payload around now; later/backend-derived entities should not block the immediate UI capture work.

Group Entities Immediate use
Application shell Application, ApplicationAcknowledgement, ConsentEvent Create the journey, store intake/search statuses, acknowledgements, and consent events.
Applicants and people ApplicationParty, ApplicationPartyRelationship, Person, PersonIdentityProfile, ContactMethod Represent primary applicant and co-applicants as parties, not prefixed field sets.
Applicant identity snapshot ApplicationPartyIdentity, ApplicationPartyNationality, ApplicationPartyResidency, ApplicationPartyTaxResidency Capture submitted identity, nationality, residency, and tax-residency facts for the application.
Employment and affordability inputs Employment, IncomeSource, Asset, Liability, HousingExpense, FundingSource Capture repeatable income, work, debts, rent, savings, and down-payment source.
Institutions and identifiers Institution, InstitutionIdentifier Normalize employers or licensed businesses when needed; otherwise keep text on employment.
Property preference ApplicationPropertyPreference Capture target amount, locations, property types, bedrooms, bathrooms, timeline, intended use, search status, and optional metadata detail.
Documents and evidence Document, DocumentLink, DocumentRequirement, DocumentReview, DocumentExtraction, VerificationEvent Capture uploaded evidence and connect files to the applicant/application facts they support.
Credit CreditProfile, CreditCheck, CreditReport, CreditScore Capture self-reported credit standing now; official provider data can slot into credit check/report/score records later.
Selected services SelectedService, FeeQuote Capture customer-selected Sooner services; keep displayed fee outputs as quote snapshots or metadata.
Later/backend-derived ApplicationCostEstimate, AffordabilityAssessment, LenderSubmission, DecisionInPrinciple, CreditDecision, MortgageOffer, MortgageOfferResponse Use for calculated outputs, lender paths, decisions, offers, and rejection responses. These are not the main form-capture surface.

Boolean Versus Status

When to use Payload shape Examples
Use a boolean for a true binary UI gate or confirmation. true / false, or a timestamp if it is an event application.costBreakdownAcknowledgedAt, contactMethods[].verifiedAt
Use a status when the business state has more than yes/no. Enum value stored as lower snake case application.propertySearchStatus = identified, application.mortgageSearchStatus = searching
Use child rows when the answer can repeat. Array of objects parties[].liabilities[], parties[].employment[], parties[].documents[]

Money Fields

Concept Payload field Database column Notes
Property budget or target propertyPreference.targetAmount + propertyPreference.currency application_property_preference.target_amount + currency Use amount for the money value. The property context already explains what the amount is for.
Specific property stated value propertyPreference.metadata.specific_property.stated_amount + propertyPreference.currency application_property_preference.metadata.specific_property.stated_amount + currency Use only as intake metadata when the form asks about a specific property. Do not introduce durable property tables for this pass.
Loan request requestedTerms.requestedAmount + requestedTerms.currency requested_loan_terms.requested_amount + currency Keep requested borrowing separate from property target and selected-property purchase amount.

Lender Path Entities

Entity What it means Why it exists Typical child records
Application The customer journey submitted through Sooner. One application can involve multiple parties, requested terms, property preferences, documents, and more than one lender path. ApplicationParty, RequestedLoanTerms, ApplicationPropertyPreference, FundingSource, ApplicationAcknowledgement.
LenderSubmission One lender-specific package or assessment path inside an application. Different lenders can receive the same application and return different references, statuses, conditions, declines, decisions, expiry dates, and offers. DecisionInPrinciple, CreditDecision, MortgageOffer, lender-specific document requirements, credit checks, and conditions.
DecisionInPrinciple A preliminary lender decision, commonly called a DIP or AIP in UK mortgage journeys. It captures early eligibility or likely borrowing outcome before the final mortgage offer. It can expire, be declined, or be replaced without closing the whole application. Status, maximum amount, currency, issued date, expiry date, lender reference, conditions, and source lender submission.
CreditDecision A fuller underwriting or credit outcome from a lender. It is separate from a DIP because full assessment can use deeper credit checks, verified income, property details, and lender policy rules. Status, approved amount, decision reason, conditions, decided date, expiry date, and source lender submission.
MortgageOffer The formal lender offer with offer terms. It should not be flattened into application status because one lender can offer while another declines, expires, or remains in review. Offered amount, currency, rate, repayment type, term, monthly payment, issued date, expiry date, conditions, and accepted/withdrawn status.

Party Modeling Note

co_applicant is not a separate table and should not become a prefixed field family. It is a role value on an ApplicationParty row. Each party points to a Person and owns the application-specific facts collected for that participant: employment, income, liabilities, documents, consents, and relationship to the primary applicant. This is the reason the payload uses parties[] instead of primary_* and co_applicant_* columns.

Recommended Catalog Sections

Catalog section Primary entity Purpose
Application intake Application Creates the application container and captures initial status, source, market, target property state, and consent/acknowledgements.
Applicant identity Person, PersonIdentityProfile, ContactMethod, ApplicationParty, ApplicationPartyIdentity Captures reusable person KYC/preload data, contact methods, application participation, role, verification, and submitted applicant identity snapshots.
Employment and income Employment, IncomeSource Captures repeatable applicant employment, business ownership, salary, commission, and other income sources.
Assets, liabilities, and funding Asset, Liability, HousingExpense, FundingSource Captures liquid assets, rent, debt obligations, credit card exposure, down-payment source, and affordability inputs.
Property intent ApplicationPropertyPreference Captures target amount, locations, property type choices, bedroom and bathroom count choices, purchase timeline, intended use, and whether a property is already identified.
Documents and verification DocumentRequirement, Document, DocumentVerification, CreditReport Captures required evidence, uploaded files, extracted data, document verification, and credit bureau/report details.
Application calculations AffordabilityAssessment Stores calculated affordability, commitment, expenditure, stress-test, maximum borrowing, and eligibility result snapshots.

Current Field to Target Payload Map

This table maps Sooner Forms Flow inputs to stable backend payload paths. Form controls can change over time, but submitted data should land on repeatable finance entities, market-portable values, and clear applicant-party ownership.

Current Sooner Forms Flow field Target payload path Target record Target field Mapping note
heroValue / property_budget_aed propertyPreference.targetAmount + propertyPreference.currency ApplicationPropertyPreference targetAmount, currency Use a currency field rather than encoding AED in the name.
foundProperty, found_property, has_found_property application.propertySearchStatus ApplicationPropertyPreference propertySearchStatus Map a positive answer to identified. Map a negative answer to searching or not_started based on the UI wording.
mortgageStatus application.mortgageSearchStatus Application mortgageSearchStatus Use this as an intake/search state: has the applicant started, found, or not needed a mortgage path? Actual lender decisions and offers live under lender submissions.
timeline, property_timeline propertyPreference.purchaseTimeline ApplicationPropertyPreference purchaseTimeline Use purchaseTimeline in the payload. Avoid propertyTimeline because the timeline is specifically the buyer's purchase intent.
areaMulti, property_area, areas propertyPreference.locationData ApplicationPropertyPreference locationData[] items: id, name, address, pslCode, addressId, parentPslCode, addressPslCode, locationConfidence, locationConfidenceScore Use the existing location structure; area-picker UI names should not become backend field names.
propertyType, property_type propertyPreference.preferredPropertyTypes ApplicationPropertyPreference preferredPropertyTypes: propertyType[] Use plural because clients can select multiple property types.
bedrooms, bathrooms propertyPreference.preferredBedrooms, propertyPreference.preferredBathrooms ApplicationPropertyPreference preferredBedrooms: number[], preferredBathrooms: number[] Preferences are selected count options, not a min/max range. Use scalar bedrooms and bathrooms only on a selected property.
residency, uae_residency parties[].identity.residencies[] ApplicationPartyIdentity / ApplicationPartyResidency countryCode, residencyStatus, residencyBasis, startedOn, endedOn, documents[] For lending, residency is an applicant/borrower disclosure used for eligibility. UAE should be a country value, not part of the field name.
nationality parties[].identity.nationalities[] ApplicationPartyIdentity / ApplicationPartyNationality countryCode, status, isPrimary, startedOn, endedOn, documents[] A party can disclose more than one nationality/citizenship for one application. Reusable person-level nationality can preload the form, but source links stay internal.
incomeInput, monthly_income, fixed_income_aed, commissionInput, variable_income, variable_income_aed parties[].incomeSources[].amount, parties[].incomeSources[].type IncomeSource type, amount, currency, cadence, isVariable Use one income-source pattern for salary, commission, bonus, business income, and other repeatable income.
householdIncomeInput, household_income_total affordabilityAssessment.inputSnapshot.householdIncomeTotal AffordabilityAssessment householdIncomeTotal, currency, calculatedAt Use this as a calculated or displayed affordability input when individual party incomes are already collected. Do not let it replace party-owned income-source rows.
partner_income_monthly_aed, partnerCommInput, partner_commission_monthly_aed parties[].incomeSources[].amount, scoped by parties[].role = co_applicant IncomeSource type, amount, currency, cadence Partner is a relationship label; co-applicant is the application capacity. If the partner is not legally applying, store the person as a related contact instead of an applicant party.
fullName, full_name, emailInput, contact_email, phoneInput, contact_phone, co_app_full_name, co_app_email, co_app_phone parties[].person.firstName, middleName, lastName, legalFullName, contactMethods[] Person, PersonIdentityProfile, ContactMethod firstName, middleName, lastName, legalFullName, type, value, normalizedValue Collect structured name parts where possible; keep legalFullName for markets/documents that require the exact full name.
phone_otp_verified parties[].contactMethods[].verifiedAt ContactMethod verifiedAt Use timestamp rather than boolean.
cost_breakdown_confirmed, cost_breakdown_confirmed_at application.acknowledgements[].acknowledgedAt ApplicationAcknowledgement type, acknowledgedAt Application acknowledgement event.
service_selection application.selectedServices[].serviceCode SelectedService serviceCode, selectedAt Use only if Sooner has selectable service bundles. Lender products, decisions, and offers should remain on lender-submission records.
employment_type, employment_type_other, employer_name, role_name, job_title, role_seniority, seniority_level, employer_tenure, employer_tenure_bucket, business_years parties[].employment[] Employment employmentType, employmentTypeOtherText, employerNameText, jobTitle, seniorityLevel, startedOn, tenureMonths, tenureBucket, businessYears Prefer startedOn when exact dates are available. Use buckets or calculated months when the form only asks for a range.
industry_id, industry_code, industry_other, role_family_id, occupation_family_code, role_family_other, employer_ownership_type, employer_size_band, regulated_or_licensed parties[].employment[] Employment industryId, industryOtherText, roleFamilyId, roleFamilyOtherText, employerOwnershipType, employerSizeBand, isRegulatedOrLicensed Use backend reference data for business-facing industries and occupation families. Keep *OtherText scoped to the same employment row.
has_existing_mortgage, mortgage_payment, mortgage_monthly_aed parties[].liabilities[].type, monthlyPayment.amount, monthlyPayment.currency Liability type = mortgage, monthlyPaymentAmount, currency Allows more than one mortgage liability.
has_rent, rent_payment parties[].housingExpenses[].type, monthlyPayment.amount, monthlyPayment.currency HousingExpense type = rent, monthlyPaymentAmount, currency Rent is an obligation, not a loan.
has_personal_loan, personal_loan_payment, has_car_loan, car_loan_payment, has_credit_card, credit_card_payment, credit_card_total_limit, credit_card_outstanding parties[].liabilities[] Liability type, monthlyPaymentAmount, limitAmount, balanceAmount, currency Use repeatable liabilities for personal loans, vehicle loans, credit cards, overdrafts, and other obligations.
other_monthly_commitments, other_monthly_commitments_aed parties[].liabilities[] Liability type = other, monthlyPaymentAmount, currency, description Prefer a known liability type such as lease, support_obligation, or overdraft when the form can identify it.
savings / savings_band parties[].assets[].amountBand Asset type = liquid_savings, amount, amountBand, currency Use exact amount if collected later.
property_usage_raw propertyPreference.intendedUse ApplicationPropertyPreference intendedUse Capture occupancy/use intent with values such as primary_residence, secondary_residence, buy_to_let, investment_property, and other. Do not suffix raw unless a normalized pair exists.
down_payment_source application.fundingSources[].type FundingSource type, amount, currency Supports AML/provider workflows later.
down_payment_source_other application.fundingSources[].otherText FundingSource otherText Only populate this when the selected source type is other.
aecb_score, aecb_band parties[].creditProfile CreditProfile provider = aecb, source = self_reported, selfReportedScore, selfReportedBand, declaredAt This is applicant-declared credit standing. Official bureau data belongs to CreditCheck, CreditReport, and credit-score child records.
has_co_applicant, co_app_*, co_applicant_* parties[] with role = co_applicant ApplicationParty role = co_applicant, plus the same identity, contact, employment, income, liability, housing expense, asset, credit profile, consent, and document child records used by the primary applicant The gate controls whether a second party is collected. Durable state is the presence of a co-applicant party, not a standalone boolean.
has_specific_property propertyPreference.propertySearchStatus ApplicationPropertyPreference propertySearchStatus This is a search-state answer from the form. It should not create a separate durable property model.
listing_url, building_or_sub_community, exact_price, ready_to_move_in, freehold_status, property_usage propertyPreference.metadata.specific_property ApplicationPropertyPreference.metadata specific_property.source_url, location_text, stated_amount, availability_status, tenure_type, intended_use Only keep these if the intake flow needs to remember what the applicant typed. Store persisted metadata keys as lower snake case.
property_options, locked_option_id, property_options_complete application.metadata.forms.property_options Application.metadata forms.property_options, forms.locked_option_id, forms.property_options_completed_at Treat as UI/progressive form state unless Sooner later decides to productize recommendations.
ownership_split application.partyRelationships[].ownershipSharePercent ApplicationPartyRelationship ownershipSharePercent, financialContributionPercent Capture applicant-party intent only. Do not model property title or property interests in this document.
trade_license_number, po-biz-license parties[].employment[].tradeLicenseNumberText or institution.identifiers[] Employment / InstitutionIdentifier tradeLicenseNumberText or identifierType = trade_license, identifierValue, countryCode Use an institution identifier when the business is normalized; otherwise keep a self-employed employment snapshot value.
reliable_submission_consent application.consents[].capturedAt ConsentEvent type, status, capturedAt Submission accuracy confirmation.
rejection_reason, rejection_other mortgageOffer.response or lenderSubmission.response MortgageOfferResponse / LenderSubmission response = rejected, reasonCode, reasonOtherText, respondedAt Scope rejection feedback to the offer or lender path being rejected, not to the whole person record.

Derived And Display-Only Values

These values appear in Sooner Forms Flow but are not borrower-submitted facts. Persist them only when Sooner needs an audit trail, quote snapshot, decision snapshot, analytics event, or short-term UI trace. If they are only display/supporting values, store them under the relevant owner record's metadata JSON column with lower snake case keys.

Current form value Recommended target Handling
cost_breakdown, total_upfront_aed, closing_costs_aed, sooner_savings_aed ApplicationCostEstimate Calculated quote output. If audit history matters, store first-class quote fields. If not, use application_cost_estimate.metadata.display keys such as total_upfront_amount, closing_costs_amount, and sooner_savings_amount.
sooner_reveal ApplicationAcknowledgement or product analytics Display event. Persist only if the business wants proof that the reveal was viewed. Otherwise use analytics or application.metadata.forms.sooner_reveal.
fee_tier_id, fee_tier_percent FeeQuote / SelectedService Derived from selected services and pricing rules. Store as a quote snapshot if the fee shown to the customer must be auditable; otherwise keep supporting display details in fee_quote.metadata.display.
indicative_scs, final_scs, tier, decision, decision_at, gates_failed, categories_below_3 AffordabilityAssessment, DecisionInPrinciple, CreditDecision Backend-derived underwriting outputs. Persist as decision or assessment snapshots when auditable; otherwise keep non-queryable display support in snapshot metadata.

Document Catalog Rework

Current doc tile Recommended payload path Target entity Target fields Notes
emirates_id_front, emirates_id_back parties[].identity.documents[] Document + DocumentLink(applicationPartyIdentityId) documentType = national_identity_document, countryCode, localDocumentType, documentSide For UAE, localDocumentType = emirates_id. This is evidence for the applicant party identity in this application. If the document already exists in the person's KYC profile, preserve that source link too.
passport parties[].identity.documents[] Document + DocumentLink(applicationPartyIdentityId) documentType = passport, countryCode Country can be issuing country if captured. Link to the application-party identity snapshot; reusable KYC copies can also link to person_identity_profile.
visa_page parties[].identity.documents[] Document + DocumentLink(applicationPartyResidencyId) documentType = residency_permit, countryCode, localDocumentType For UAE, local type can identify visa page. Link to the party residency disclosure for this application.
bank_statements parties[].documents[] Document documentType = bank_statement, periodStart, periodEnd, institutionName One document row per statement file.
aecb_report parties[].creditChecks[] + creditReports[] + documents[] Document, CreditCheck, CreditReport, CreditScore documentType = credit_report, provider = aecb, reportedAt, score, scoreBand For UAE, AECB is the provider. Consent to fetch the report should be captured as a credit-check consent event when no report is uploaded.
salary_certificate, employment_letter parties[].employment[].documents[] Document, Employment documentType = employment_income_evidence, evidenceType, employmentId Evidence type distinguishes salary certificate and employment letter.
trade_license_pdf, audited_financials parties[].employment[].documents[] Document, Employment documentType, evidenceType, employmentId, optional periodStart, periodEnd Trade license PDF is evidence for business/self-employment and pairs with typed trade license number. Audited financials should use documentType = financial_statement and evidenceType = audited_financials.
co_app_emirates_id, co_app_passport, co_app_visa, co_app_salary_certificate, co_app_bank_statements parties[].documents[], scoped by parties[].role = co_applicant Document, ApplicationParty applicationPartyId, same document fields as primary applicant Document type stays the same; party role identifies co-applicant.

Glossary

Entity Meaning
ApplicationThe main finance journey record. One started application, with statuses like property search state, mortgage search state, submitted, nullified, or withdrawn.
ApplicationAcknowledgementTimestamped confirmations, such as cost-breakdown confirmation or acceptance of an application statement.
ConsentEventConsent history for credit checks, document processing, lender sharing, marketing, contact permission, or revocation.
ApplicationPartyA person participating in an application. Primary applicant and co-applicant are both rows here.
ApplicationPartyRelationshipRelationship between parties on the same application, such as spouse, partner, donor, dependent, or representative.
PersonStable human record, independent of any one application.
PersonIdentityProfileReusable KYC/profile facts for a person, useful for prefill across applications.
ContactMethodReusable email, phone, or WhatsApp contact value.
ApplicationPartyIdentityThe identity snapshot submitted for one party on this application.
ApplicationPartyNationalityOne nationality or citizenship disclosed by an application party.
ApplicationPartyResidencyOne legal or immigration residency disclosed by an application party.
ApplicationPartyTaxResidencyTax-residency disclosure for an application party.
EmploymentOne work, employer, business, or role profile for an applicant.
IncomeSourceOne income component, such as salary, commission, bonus, self-employment income, or rental income.
AssetSavings, deposit funds, investments, gift funds, or other assets.
LiabilityDebt and credit obligations, such as personal loan, car loan, credit card, mortgage liability, or overdraft.
HousingExpenseRent or housing outgoing that affects affordability but is not a debt liability.
FundingSourceSource of deposit or down-payment funds, such as savings, gift, sale proceeds, or employer support.
InstitutionReusable real-world organisation, such as employer, lender, broker, credit provider, solicitor firm, or provider.
InstitutionIdentifierOfficial identifiers for an institution, such as trade licence number or registration number.
ApplicationPropertyPreferenceProperty search intent: target amount, currency, locations, property types, bedrooms, bathrooms, timeline, intended use, property search status, and metadata for optional specific-property detail.
DocumentUploaded or generated file metadata.
DocumentLinkSays what a document supports, for example identity, residency, income, employment, liability, or funding source.
DocumentRequirementEvidence request, such as bank statement required or passport required.
DocumentReviewReview state for a submitted document.
DocumentExtractionExtracted fields from a document, usually OCR, provider, or AI output.
VerificationEventProvider or human verification event for a fact or document.
CreditProfileSelf-reported credit standing, such as AECB score or band captured from the form.
CreditCheckRequest or search event with a credit provider.
CreditReportReturned credit report from a provider.
CreditScoreScore or score band from a report, provider, or self-report.
SelectedServiceCustomer-selected Sooner service or package.
FeeQuoteDisplayed fee or pricing snapshot derived from selected services.
ApplicationCostEstimateCalculated displayed cost breakdown.
AffordabilityAssessmentBackend affordability or eligibility calculation.
LenderSubmissionOne lender-specific path inside an application.
DecisionInPrinciplePreliminary DIP/AIP-style lender decision.
CreditDecisionFuller underwriting or lender decision.
MortgageOfferFormal lender offer.
MortgageOfferResponseApplicant response, rejection, or acceptance of an offer.

Sooner V2.1 Entities

Target database entity catalog for V2.1. Names in this tab are lower snake case because this tab represents database tables, columns, and stored enum values. API payloads can still use nested camelCase and map into these database entities.

Identity, Contacts, And Institutions

Identity is split so reusable person facts, platform access, contact methods, and application snapshots can change independently.

country vs nationality fact

Why
Nationality, residency, document issuing country, address country, institution country, tax residency, and market selection should use the same country code source.
Stores
country is a backend-owned reference table sourced from ISO 3166 country codes and served to clients through a reference-data endpoint. person_nationality and application_party_nationality store the fact that a person or party holds a nationality/citizenship.
Sanity check
Do not make countries a Prisma enum or a frontend-only static list. Country/reference changes should be backend data updates, not schema migrations or UI redeploys.

person vs person_identity_profile

Why
A human exists beyond one application, while KYC facts can change and be reused for prefill.
Stores
Stable person data on person; reusable names, birth date, nationality, residency, tax residency, and evidence links on profile children.
Sanity check
If Sooner will not reuse KYC across applications, collapse this for now and keep only application snapshots.

Person KYC vs application snapshot

Why
Closed, submitted, withdrawn, and nullified applications need the facts declared at that time.
Stores
person_identity_profile preloads data; application_party_identity preserves the submitted lending snapshot.
Sanity check
If identity facts never freeze per application, the snapshot table is too much. For auditable lending records, keep it.

Platform access vs finance person

Why
Login/session identity, tenant access, and bundle permissions are platform concerns, not applicant identity.
Stores
platform_account links a signed-in Sooner account to a person. authorization_assignment and bundle_assignment store Sooner-IAM tenant access and scoped bundles.
Sanity check
Do not store provider-specific legacy columns here. Do not make platform access the applicant or customer model.

Contact methods and related contacts

Why
Email, phone, and WhatsApp values may be verified, reused, or attached to different owners. Postal addresses need structured fields.
Stores
contact_method stores email/phone/WhatsApp values. address stores structured addresses. Owner joins store label, primary flag, and verification state.
Sanity check
If contact details are only one-off form fields, simplify. If they power auth, messaging, CRM, and applications, keep the split.

institution vs IAM organisation

Why
institution is a real-world finance ecosystem entity, not a Sooner access-control tenant.
Stores
Lenders, employers, brokers, solicitors, conveyancers, valuers, insurers, credit providers, and agencies.
Sanity check
Do not create institutions for every one-off human contact. Use institutions when the organisation matters beyond free text.
Table Purpose Owner Columns
country Shared country and nationality selection reference. Reference data. country_code as ISO 3166-1 alpha-2 primary key, alpha_3_code, numeric_code, source, source_version, last_synced_at, is_supported_market, is_selectable, is_active, display_order, created_at, updated_at. Use this as the FK target for nationality, residency, tax residency, address country, institution country, document country, and market/country selectors. The backend should seed and update this table, then expose it through a reference-data API such as GET /reference/countries for form dropdowns.
country_label Display labels for country and nationality selectors. country. country_code, locale, country_name, short_country_name, nationality_label, created_at, updated_at. Keep labels backend-managed so the UI can render country and nationality dropdowns from the same reference source without hardcoded frontend lists.
industry, industry_label Backend-owned industry choices for employment and underwriting forms. Reference data. industry: industry_code primary key, parent_industry_code, is_selectable, is_active, display_order, created_at, updated_at. industry_label: industry_code, locale, label, created_at, updated_at. Seed business-readable values such as hospitality, aviation, banking_financial_services, healthcare, real_estate, and information_technology.
person Stable human identity independent of application lifecycle. Root identity record. id, first_name, middle_name, last_name, legal_full_name, preferred_name, date_of_birth, created_at, updated_at, deleted_at. Keep reusable KYC/identity facts in child records rather than UAE-specific scalar columns. Use related_contact for one-off contacts until they need canonical person identity.
person_identity_profile Reusable KYC/identity profile for a person across applications. person. id, person_id, legal_full_name, first_name, middle_name, last_name, date_of_birth, verification_status, verified_at, expires_at, source, created_at, updated_at. Use this as reusable identity data that can preload a new application; do not mutate closed application snapshots when this changes.
person_nationality, person_residency, person_tax_residency Reusable current or historical identity facts known about a person. person_identity_profile. Use the same core shape as the application-party disclosure rows: country_code referencing country.country_code, status, effective dates, verification fields, and timestamps. A new application may copy or reference these as source records, but lender-facing disclosures are still stored on application_party_identity child rows.
person_role Platform or product lifecycle role held by a person. person. id, person_id, role, starts_at, ends_at, created_at, updated_at. Keep this to broad lifecycle roles such as user, customer, and operator. Institution-specific roles belong in person_institution_role; application capacities belong in application_party_role.
platform_account Sooner product account for a person with platform access. person. id, person_id, iam_subject_id, account_status, profile_picture_url, created_at, updated_at, deleted_at. Keep this provider-neutral. It links Sooner's signed-in account to the finance-domain person; it is not the applicant/customer model by itself.
authorization_assignment Tenant-scoped Sooner IAM authorization subject assignment. IAM tenant/workspace; optionally platform_account. id, tenant_id, subject_type, subject_id, platform_account_id, created_at, updated_at. Uniqueness is scoped by tenant_id, subject_type, and subject_id, matching the Sooner-IAM subject model. platform_account_id is a local convenience link for human account subjects.
bundle_assignment Permission/capability bundle assigned to an authorization assignment. authorization_assignment. id, tenant_id, authorization_assignment_id, bundle_name, created_at, updated_at. Uniqueness should be scoped by tenant_id, assignment, and bundle name.
contact_method Reusable contact method value, such as an email, phone, or WhatsApp number. Owned through person_contact_method or institution_contact_method. id, type, value, normalized_value, created_at, updated_at. Do not infer identity from a shared phone or email by itself.
person_contact_method, institution_contact_method Owner-specific use, verification, and primary status for a contact method. person or institution plus contact_method. person_contact_method: person_id, contact_method_id, label, is_primary, verified_at, verification_source, created_at, updated_at. institution_contact_method uses the same shape with institution_id. Primary contact uniqueness is scoped by owner and type.
address, person_address, institution_address Structured postal or physical address. person or institution plus address. address: id, line_1, line_2, city, region, postal_code, country_code, formatted_address, created_at, updated_at. Owner joins store address_type, is_primary, verified_at, and timestamps. Use an application-scoped address row later if lender forms need address snapshots.
related_contact Reusable contact connected to a person or application without requiring a canonical person record. person or application, optionally linked to person or institution. id, owner_person_id, related_person_id, institution_id, display_name, relationship_type, institution_name_text, job_title, is_primary, notes, created_at, updated_at. Contact details attach through related_contact_contact_method using contact_method.
application_related_contact Application-specific use of a reusable related contact. application, optionally application_party, plus related_contact. application_related_contact: id, application_id, application_party_id, related_contact_id, created_at, updated_at. application_related_contact_purpose: application_related_contact_id, purpose. When application_party_id is set, the party must belong to the same application; the related contact does not need to be a full person row.
institution Real-world institution: lender, employer, broker, solicitor/conveyancer, provider, insurer, or agency. Root institution record. institution: id, name, legal_name, country_code, created_at, updated_at, deleted_at. institution_classification: institution_id, classification. institution_identifier: institution_id, identifier_type, identifier_value, country_code, issuer, timestamps. Use child rows because one institution can have multiple roles and registrations.
person_institution_role Person's role at or relationship to an institution. person + institution. id, person_id, institution_id, institution_name_text, relationship_type, title, department, is_primary, started_on, ended_on, created_at, updated_at. Use institution_name_text when the employer/provider is only a snapshot and should not be normalized yet.
imported_record_link Lineage to migrated or imported source rows. Any canonical subject. id, subject_type, subject_id, source_system, source_table, source_record_id, source_url, imported_at, metadata. Use this only when the purpose is traceability back to V1, Supabase, imported forms, or another source dataset.
Application, Parties, And Financial Profile

These boundaries explain why applicant, role, relationship, finance terms, and affordability facts are split instead of stored on one application row.

application vs application_party

Why
A finance journey can have multiple people, and one person can have multiple journeys over time.
Stores
application owns lifecycle and market. application_party says which person participates in that journey.
Sanity check
Needed once there are co-applicants, guarantors, sponsors, representatives, or restarted/nullified applications.

Party roles vs party relationships

Why
Legal capacity is different from relationship to another party.
Stores
Role rows store borrower, guarantor, occupier, representative, or adviser capacity. Relationship rows store spouse, partner, donor, dependent, or representative-for links.
Sanity check
If every application is one applicant only, this can be delayed. If co-applicant logic exists, avoid co_app_* columns.

Requested terms vs lender outcome

Why
The customer ask can change, and lender decisions/offers are not the same as application status.
Stores
requested_loan_terms stores the ask. lender_submission, DIP/AIP, credit decision, and offer records store lender-specific outcomes.
Sanity check
If Sooner only needs one internal estimate, simplify. If lender paths matter, keep this split.

Financial facts as repeatable records

Why
Applicants can have more than one job, income source, asset, liability, housing expense, or funding source.
Stores
Employment, income, assets, liabilities, housing expenses, funding sources, ownership/responsibility links, currency, verification, and evidence.
Sanity check
Needed when the UI can collect multiple records or underwriting needs to explain each component separately.
Table Purpose Owner Columns
application Finance journey container for one or more parties. Root finance record. id, application_number, market, intake_channel, status, property_search_status, mortgage_search_status, metadata, submitted_at, nullified_at, withdrawn_at, completed_at, created_at, updated_at, deleted_at. mortgage_search_status is intake state only. Lender outcomes live under lender submissions. Imported or migrated source-row IDs live in imported_record_link. Use metadata for form/UI trace values with lower snake case keys.
application_status_history Append-only application lifecycle events. application. id, application_id, from_status, to_status, reason, changed_by_person_id, changed_at, metadata. Nullified or restarted journeys preserve history and new applications can link back through a restart lineage field or imported_record_link when created from an imported source record.
application_party Person participating in an application. application + person. id, application_id, person_id, is_primary, created_at, updated_at. Legal/workflow capacities live in application_party_role; relationships live in application_party_relationship. Identity, nationality, residency, and tax-residency disclosures live in party identity records so each application preserves what was declared at that time. Consent lives in consent_event, not as one scalar party status.
application_party_identity Application-time identity profile disclosed for one party. application_party. id, application_party_id, source_person_identity_profile_id, legal_full_name, first_name, middle_name, last_name, date_of_birth, declared_at, verified_at, verification_status, created_at, updated_at. This is the lending/application identity snapshot. It can be preloaded from person_identity_profile, but it must remain stable after the application is submitted or closed.
application_party_nationality Nationality/citizenship disclosed for a party on one application. application_party_identity. id, application_party_identity_id, source_person_nationality_id, country_code referencing country.country_code, status, is_primary, created_at, updated_at. A party can disclose multiple active nationalities for one application. Evidence documents attach through document_link and document_requirement_document.
application_party_residency Legal or immigration-residence status disclosed for a party on one application. application_party_identity. id, application_party_identity_id, source_person_residency_id, country_code referencing country.country_code, residency_status, residency_basis, started_on, ended_on, verification_status, created_at, updated_at. A party can disclose multiple active or historical residencies. Evidence documents attach through document_link and document_requirement_document.
application_party_tax_residency Tax-residency disclosure for a party on one application. application_party_identity. id, application_party_identity_id, source_person_tax_residency_id, country_code referencing country.country_code, tax_identifier, status, self_certified_at, created_at, updated_at. Keep this separate from legal/immigration residency. Evidence documents attach through document_link and document_requirement_document.
application_party_role Repeatable legal or workflow capacity held by a party in an application. application_party. id, application_party_id, role, starts_at, ends_at, created_at, updated_at. Supports combinations such as primary applicant + borrower, spouse + non-borrowing occupier, guarantor, sponsor, or representative without overloading one field. Broker, lender, solicitor, and conveyancer contacts belong in institution/contact rows.
consent_event Consent, revocation, or permission event for credit checks, document processing, marketing, contact, lender sharing, or data reuse. application, optionally application_party, person, and platform_account. id, application_id, application_party_id, person_id, platform_account_id, consent_type, status, captured_at, revoked_at, source, evidence_document_id, metadata, timestamps. Current consent state can be derived from the latest event by type.
application_institution_role Institution participation in an application when the legal/workflow participant is not a person. application + institution. id, application_id, institution_id, role, status, created_at, updated_at. Use for company borrower, trust, corporate guarantor, institutional donor, broker firm, lender, solicitor firm, conveyancer firm, valuation provider, or insurer. Do not create fake person rows for non-human legal entities.
application_party_relationship Relationship between parties on one application. application. id, application_id, from_application_party_id, to_application_party_id, relationship_type, ownership_share_percent, financial_contribution_percent, created_at, updated_at. This is the canonical relationship source; do not duplicate relationship-to-primary on application_party.
requested_loan_terms Requested finance terms before lender decision. application. id, application_id, version, requested_amount, currency, term_months, repayment_type, rate_preference, purpose_of_borrowing, loan_to_value_percent, effective_at, superseded_at, created_at, updated_at. Current terms are the row with no superseded_at; avoid both current pointers and status flags for the same versioning concept.
lender_submission One lender-specific package, application route, or assessment path. application, institution, and requested terms. id, application_id, lender_institution_id, requested_loan_terms_id, submission_reference, stage, status, submitted_at, responded_at, expires_at, created_at, updated_at. Use this between the application and DIP/AIP, full underwriting, conditions, decisions, and offers. One application can have multiple lender submissions.
employment Work, employer, business, or role profile. application_party. id, application_party_id, employment_type, employer_institution_id, employer_name_text, job_title, occupation_family_code, seniority_level, industry_code, employer_ownership_type, started_on, tenure_months, business_years, verification_status, created_at, updated_at. industry_code references backend industry reference data selected in the form.
income_source Repeatable income component. application_party, optionally employment. id, application_party_id, employment_id, type, amount, currency, cadence, is_variable, stability_months, commission_percent, verification_status, source_document_id, created_at, updated_at.
asset Savings, deposit, investment, gift fund, or other asset. application, owned through asset_owner. asset: id, application_id, type, amount, amount_band, currency, institution_id, institution_name_text, account_reference, verified_at, source_document_id, created_at, updated_at. asset_owner: asset_id, application_party_id, ownership_percent, created_at, updated_at, with a unique pair per asset and party.
liability Debt or credit obligation. application, responsible parties through liability_party. liability: id, application_id, credit_report_id, credit_report_tradeline_id, type, lender_institution_id, lender_name_text, balance_amount, limit_amount, monthly_payment_amount, currency, opened_on, verified_at, created_at, updated_at. liability_party: liability_id, application_party_id, responsibility_type, share_percent, with a unique pair per liability and party.
housing_expense Rent or housing outgoing that affects affordability but may not be a debt. application, optionally shared through housing_expense_party. id, application_id, type, monthly_amount, currency, is_current, will_continue_after_completion, starts_on, ends_on, verified_at, created_at, updated_at. Use housing_expense_party when multiple parties share the expense.
funding_source Deposit/down-payment and other source-of-funds record. application, optionally application_party. id, application_id, application_party_id, donor_application_party_id, type, amount, currency, country_code, verification_status, source_document_id, created_at, updated_at. funding_source_asset links one funding source to one or more assets and prevents double-counting deposit money.
Credit, Property, Documents, Decision, And Operations

These boundaries explain why evidence, property, lender decisions, and workflow are not flattened into generic status fields.

Credit check vs credit report

Why
The search/check event, the returned report, the score, and tradelines are different records with different provider timestamps.
Stores
Credit request consent and status, returned bureau report, score model, score band, and reported account lines.
Sanity check
If credit is only one uploaded PDF, start simpler. If provider integrations or tradelines matter, keep the split.

Property preference only

Why
Gabriel's current flow captures search intent and optional typed property detail, not a lender property model.
Stores
Budget, locations, property types, bedroom and bathroom choices, timeline, intended use, search status, and optional form detail.
Sanity check
Do not add property, listing, valuation, ownership, or lender-property tables until a product flow explicitly needs them.

Document file vs evidence meaning

Why
The same file can support more than one fact, and one required evidence item can be satisfied by multiple files.
Stores
document stores file metadata. document_link says what the file supports. Requirements, review, extraction, and verification keep their own status.
Sanity check
If files are only stored uploads with no review meaning, this can be reduced. If evidence is reviewed, reused, extracted, or requested, keep it.

Workflow vs domain model

Why
Tasks, activities, messages, assistant context, webhooks, and audit rows should attach to real finance subjects without becoming those subjects.
Stores
Operational case work linked to application, party, lender submission, mortgage case, document, conversation, or offer.
Sanity check
Use typed links for active workflow. Use generic subject fields only for immutable audit/event payloads where FK enforcement is not required.
Table Purpose Owner Columns
credit_check, credit_report, credit_score, credit_report_tradeline Credit search/check event, report, score, score band, and reported credit accounts. Provider is an institution or provider value. institution, application_party, optionally lender_submission. credit_check: id, application_id, application_party_id, lender_submission_id, provider_institution_id, country_code, search_type, purpose, consent_event_id, requested_at, responded_at, status. credit_report: id, credit_check_id, report_reference, reported_at, document_id, raw_status, status. credit_score: credit_report_id, score, score_band, score_model, source, reported_at. credit_report_tradeline: credit_report_id, account_type, lender_institution_id, lender_name_text, balance_amount, limit_amount, monthly_payment_amount, currency, opened_on, raw_status, status.
application_property_preference Search intent and the property preference data Gabriel's forms collect. application. target_amount, currency, preferred_property_types, preferred_bedrooms, preferred_bathrooms, purchase_timeline, intended_use, property_search_status, location_data, is_location_agnostic, location_confidence, location_confidence_score, metadata. preferred_property_types, preferred_bedrooms, and preferred_bathrooms store selected array options. location_data stores location snapshots with id, name, address, psl_code, address_id, parent_psl_code, and address_psl_code. Keep location_confidence as the coarse quality/source label and location_confidence_score as the numeric confidence score. Optional specific-property intake values live under metadata.specific_property with lower snake case keys.
document, document_requirement, document_review, document_extraction, verification_event Typed evidence, requirements, review, extraction, and provider verification. application plus typed target relations. document: file metadata and storage location. document_link: document_id, application_id, typed nullable target FKs such as person_identity_profile_id, person_nationality_id, person_residency_id, person_tax_residency_id, application_party_id, application_party_identity_id, application_party_nationality_id, application_party_residency_id, application_party_tax_residency_id, employment_id, income_source_id, asset_id, liability_id, funding_source_id, lender_submission_id, mortgage_offer_id, plus link_type. document_requirement: application_id, source, typed target FKs, status, required_by, due_at. document_requirement_document is the many-to-many satisfaction link. Requirement/review/extraction/verification records own status, confidence, notes, extracted fields, and timestamps.
application_cost_estimate, fee_quote, affordability_assessment, decision_in_principle, credit_decision, mortgage_offer Displayed cost/fee snapshots, eligibility calculation, preliminary decision, formal decision, and offer. application, usually through lender_submission. application_cost_estimate stores displayed cost breakdown values such as total upfront amount, closing costs, Sooner savings, currency, and calculated time. fee_quote stores displayed fee tier and fee percentage derived from selected services. affordability_assessment: application_id, lender_submission_id, requested_loan_terms_id, income snapshot, commitment snapshot, expenditure lines, dependants, stress assumptions, policy/rule version, max borrowing, result, calculated time. decision_in_principle, credit_decision, and mortgage_offer reference lender_submission_id, requested terms, status, approved/offered amount, rate, term, expiry, conditions, and timestamps. Treat all of these as backend-derived snapshots, not submitted capture fields.
mortgage_case, mortgage_case_party, mortgage_case_institution_role, workflow_track, workflow_stage, activity, task Operational case file and workflow. application, lender_submission, or accepted mortgage_offer. mortgage_case: application_id, lender_submission_id, mortgage_offer_id, case_number, lender_case_reference, status, assigned_to_person_id, target_completion_date, completed_at. mortgage_case_party and mortgage_case_institution_role attach customer-side contacts, broker adviser, lender contact, solicitor, conveyancer, valuation provider, and insurer. Workflow/activity/task records should use concrete application, case, submission, party, document, or offer links where active relations need FK safety.
conversation, message, assistant_conversation, assistant_context, extracted_fact, webhook, audit_log Communication, AI context, extracted facts, integration, and audit. Infrastructure and canonical subjects. Use typed conversation links for active relations, such as conversation_application, conversation_application_party, conversation_mortgage_case, and conversation_lender_submission. Use typed extracted-fact targets where the fact updates domain data. Keep subject_type and subject_id only for immutable audit/webhook/event payloads where FK enforcement is not required.

Aligned Modeling Decisions

These are no longer deferred scalar-column questions. They are V2.1 modeling choices that keep repeatable, versioned, auditable, or relational facts out of overloaded scalar columns.

Area Do not model as V2.1 target shape Reason
IAM authorization Provider-specific account columns or product-domain applicant roles. platform_account(iam_subject_id) links a Sooner account to person. authorization_assignment(subject_type, subject_id) and bundle_assignment(bundle_name) follow the Sooner-IAM subject and bundle model. IAM owns access. The finance schema only needs a provider-neutral account link and local authorization mirror when product queries need it.
IAM authorization Local authorization_bundle tables in the Sooner product schema. Keep bundle_assignment.bundle_name as the Sooner-IAM catalog key. Bundle definitions belong to Sooner-IAM/catalog tooling. The product DB should not duplicate the bundle catalog unless Sooner becomes the catalog owner.
Contact methods One email/phone/WhatsApp column per owner. contact_method for the reusable value; person_contact_method / institution_contact_method for owner usage; verification_event for durable verification history. Contact methods can be shared, verified more than once, used for messaging/auth/CRM, and attached to different owners.
Addresses contact_method(type = address, value). address plus person_address / institution_address; add application-scoped address snapshots only when lender forms need them. Addresses need structured fields, country normalization, verification, and sometimes application-time snapshots.
Contact relationships One application_related_contact.purpose scalar. application_related_contact plus repeatable application_related_contact_purpose rows. One contact can be a representative, donor, employer contact, verification contact, and emergency contact in the same application.
Institution One institution.type scalar. institution plus repeatable institution_classification rows. One institution can be a lender, broker, insurer, employer, credit provider, solicitor, and valuation provider across different contexts.
Institution institution.registration_number and institution.regulator_reference as singleton columns. institution_identifier(identifier_type, identifier_value, country_code, issuer). Institutions can have multiple registrations, licences, regulator references, and market-specific identifiers.
Application party application_party.consent_status. consent_event scoped to application, party, person, or platform account, with consent_type, status, captured_at, and revoked_at. Consent is scoped, revocable, auditable, and can differ for credit checks, document processing, marketing, contact, lender sharing, and data reuse.
Application party identity application_party.current_application_party_identity_id for V2.1. One application_party_identity row per party for the submitted snapshot. Add identity versioning later only if the product needs multiple identity snapshots per party. A current pointer adds drift risk unless identity is explicitly versioned.
Application source Generic source-reference tables, generic external_id columns, or a single overloaded application.source. application.intake_channel for simple channel/source-of-intake label. imported_record_link for source-row lineage. Add application_source later only if marketing/channel attribution becomes product data. V1 buyer ID, Supabase row ID, form submission ID, chat IDs, lender case references, and attribution are different concepts.
Mortgage search intake Lender outcome names such as preapproval_status for the simple form question. Use application.mortgage_search_status when the question means whether the applicant has started, found, or does not need a mortgage path. Actual DIP/AIP, underwriting, decline, and offer outcomes still live on lender-submission records.
Property search Flattened application fields such as application.preferredAreas, application.preferredPropertyTypes, or application.targetAmount. application_property_preference.location_data using the canonical location payload, plus application_property_preference.preferred_property_types, preferred_bedrooms, and preferred_bathrooms as arrays. Location preference should preserve canonical PSL/address references, display names, confidence, and history without creating local location tables.
Credit profile Singleton credit_profile, aecb_band, or self-reported score fields. credit_check, credit_report, credit_score, and credit_report_tradeline; mark self-reported data by source. Self-report, provider reports, scores, bands, and tradelines have different provenance and timestamps.
Status history Only current status columns for long-running workflows. Keep current status on the main row and add status history/event rows where lifecycle movement matters. Transitions need reasons, actors, revisions, expiry, and audit trail without making every read reconstruct current state.
Ownership ownership_split or application.ownership.structure. application_party_relationship when the form captures applicant ownership or contribution intent. For this pass, ownership split is an application-party intake answer, not a property title model.
Enums

Keep an enum only when the database should reject unknown values for a lifecycle state, legal role, repeatable classification, or market/provider-independent type. Use backend reference tables for countries, nationalities, industries, and market-specific legal classifications.

country, country_label, industry, and industry_label are backend-owned reference data, not Prisma enums.

platform_account_status:
  invited
  active
  suspended
  disabled
  deleted

person_role_kind:
  user
  customer
  operator

contact_method_type:
  email
  phone
  whatsapp

address_type:
  residential
  correspondence
  previous_residential
  registered_office

related_contact_relationship_type:
  spouse
  civil_partner
  partner
  parent
  child
  sibling
  business_partner
  representative
  other

application_related_contact_purpose:
  verification
  emergency
  employer
  broker
  solicitor
  conveyancer
  accountant
  lender
  representative
  servicing
  other

institution_classification:
  company
  trust
  lender
  broker
  employer
  solicitor
  conveyancer
  valuation_provider
  credit_data_provider
  insurer
  government_agency
  service_provider
  other

institution_identifier_type:
  registration_number
  regulator_reference
  tax_identifier
  licence_number
  company_number
  provider_reference
  other

application_status:
  draft
  intake_started
  submitted
  in_review
  withdrawn
  nullified
  expired
  completed

application_party_role:
  primary_applicant
  co_applicant
  borrower
  co_borrower
  guarantor
  sponsor
  non_borrowing_occupier
  representative

application_institution_role:
  company_borrower
  trust
  corporate_guarantor
  institutional_donor
  broker_firm
  lender
  solicitor_firm
  conveyancer_firm
  valuation_provider
  insurer
  other

application_institution_role_status:
  active
  inactive
  superseded

application_party_relationship_type:
  spouse
  civil_partner
  partner
  parent
  child
  sibling
  business_partner
  guarantor_for
  representative_for
  other

property_search_status:
  not_started
  searching
  identified
  under_offer
  not_required

mortgage_search_status:
  not_started
  searching
  found
  not_required
  unknown

lender_submission_stage:
  decision_in_principle
  full_application
  underwriting
  offer
  completion

lender_submission_status:
  draft
  submitted
  in_review
  additional_information_required
  approved
  declined
  withdrawn
  expired

property_type:
  apartment
  villa
  townhouse
  detached_house
  semi_detached_house
  terraced_house
  maisonette
  plot
  commercial_unit
  mixed_use
  other

property_intended_use:
  primary_residence
  secondary_residence
  buy_to_let
  investment_property
  other

employment_type:
  salaried
  self_employed
  business_owner
  contractor
  commission_only
  unemployed
  retired
  other

income_source_type:
  salary
  commission
  bonus
  self_employment
  business_income
  rental_income
  investment_income
  pension
  other

asset_type:
  savings
  current_account
  fixed_deposit
  investment
  gift
  sale_proceeds
  other

liability_type:
  mortgage
  personal_loan
  vehicle_loan
  credit_card
  lease
  overdraft
  support_obligation
  other

liability_responsibility_type:
  sole
  joint
  guarantor
  authorised_user
  supplementary_cardholder
  other

housing_expense_type:
  rent
  service_charge
  maintenance
  utilities
  other

funding_source_type:
  savings
  gift
  sale_proceeds
  investment_liquidation
  inheritance
  employer_support
  family_support
  loan
  other

document_type:
  passport
  national_identity_document
  residency_permit
  bank_statement
  credit_report
  salary_certificate
  employment_letter
  trade_license
  audited_financials
  property_title
  purchase_agreement
  valuation_report
  proof_of_address
  other

document_side:
  front
  back
  full_document
  page

document_link_type:
  attached_to
  evidence_for
  generated_from
  supports_decision
  migrated_from
  other

nationality_status:
  active
  renounced
  historical
  unknown

residency_status:
  resident
  non_resident
  former_resident
  unknown

residency_basis:
  citizen
  permanent
  temporary
  visa
  other

tax_residency_status:
  active
  historical
  unknown

consent_type:
  credit_check
  document_processing
  marketing_contact
  service_contact
  lender_sharing
  data_reuse

consent_status:
  granted
  revoked
  declined
  expired

verification_status:
  not_started
  pending
  verified
  rejected
  expired
  manual_review_required

document_requirement_status:
  required
  requested
  received
  satisfied
  waived
  expired

document_requirement_source:
  sooner
  lender
  solicitor
  conveyancer
  credit_provider
  valuation_provider
  regulation
  customer

document_review_status:
  pending
  approved
  rejected
  needs_more_information
  manual_review_required

credit_report_status:
  requested
  available
  unavailable
  expired
  failed

credit_check_search_type:
  soft
  hard

credit_check_purpose:
  decision_in_principle
  full_application
  affordability
  verification

affordability_result:
  eligible
  referred
  ineligible
  insufficient_data

decision_in_principle_status:
  requested
  issued
  declined
  expired
  withdrawn

credit_decision_status:
  pending
  approved
  approved_with_conditions
  declined
  withdrawn
  expired

mortgage_offer_status:
  issued
  accepted
  revised
  withdrawn
  expired

rate_type:
  fixed
  variable
  tracker
  mixed

repayment_type:
  repayment
  interest_only
  part_and_part

workflow_kind:
  intake
  prequalification
  application
  documents
  underwriting
  offer
  completion
  servicing

task_status:
  open
  in_progress
  blocked
  completed
  cancelled
Relationship Constraints
Relationship Constraint Reason
application_party Unique active application_id + person_id. Only one active primary party per application. Prevents duplicate applicant rows and contradictory primary applicant state.
application_party_relationship Both party IDs must belong to the same application. The relationship row is the canonical relationship source. Prevents relationship-to-primary drift and cross-application relationship mistakes.
application_related_contact If scoped to application_party, that party must belong to the same application. The contact can remain contact-only or optionally link to a canonical person. Keeps application-specific contact usage attached to the right application without forcing every contact into person.
person_contact_method, institution_contact_method Unique owner/contact pair. Primary contact uniqueness is scoped by owner and contact type. A shared phone/email should not force global identity merge or multiple primary records.
asset_owner, liability_party, housing_expense_party, funding_source_asset Use composite uniqueness for each owner/link pair and keep same-application validation. Percent totals may be incomplete during intake but should be validated before submission/underwriting. Prevents duplicate ownership/responsibility rows and cross-application financial facts.
document_link, document_requirement Use typed target columns for active domain links. Keep polymorphic subject_type/subject_id only for immutable audit/webhook payloads. Preserves Prisma foreign keys, cascade behavior, includes, and safe renames.
requested_loan_terms, decision_in_principle, credit_decision, mortgage_offer Store versions and current pointers rather than overwriting finance terms or lender outcomes. Finance workflows need auditability across restarts, revised terms, expired DIPs/AIPs, declined submissions, and revised offers.

Relationship Model Guide

Start from the finance journey, then move outward. The database tables are still visible, but the first view explains what each part of the model is for and when it appears in the mortgage flow.

Identity Boundary

Person identity can be reused for KYC and prefill. Application-party identity is the submitted lending snapshot, so closed or nullified applications do not change when the person profile is updated later.

Reusable person identity
  • person
  • person_identity_profile
  • person_nationality
  • person_residency
  • person_tax_residency
  • document_link to person KYC facts
Application-party snapshot
  • application_party
  • application_party_identity
  • application_party_nationality
  • application_party_residency
  • application_party_tax_residency
  • document_link to submitted evidence
Center of the model Application as the finance case file

The application groups who is involved, what they want, what evidence supports it, which lenders review it, and what operational work follows.

Journey View

Choose a stage to see the records that matter at that moment.

Example Paths

These examples explain why the relationship tables exist without showing every table at once.

Technical ERD Reference

Compact reference for implementation review. Use the guide above for the business mental model.

Area Core relationships
Application and parties application -> application_party -> person; application_party -> application_party_role; application -> application_party_relationship.
Financial profile application_party -> employment -> income_source; application -> asset -> asset_owner -> application_party; application -> liability -> liability_party -> application_party.
Lender path application -> requested_loan_terms -> lender_submission; institution -> lender_submission; lender_submission -> decision_in_principle / credit_decision / mortgage_offer.
Property preference application -> application_property_preference. Optional specific-property intake detail stays under application_property_preference.metadata.specific_property.
Documents and case work document -> document_link; document_requirement -> document_requirement_document -> document; mortgage_offer -> mortgage_case -> task.

Next V2 Modeling

Target schema language for the next V2 pass, using the desired finance payload shape, the paused V2 schema, MISMO residential mortgage modeling, and UK mortgage language. The model should support multiple applications per person, multiple parties per application, repeatable financial facts, market-portable documents, and separate contact, credit, lender-path, property preference, document, and operational case records.

Application Name

Use Application as the schema model inside a finance bounded context. Use MortgageApplication if the bounded context is explicitly mortgage-only. Use LoanApplication only when the product is legally and operationally a loan application rather than a broader home-finance or credit journey. Keep FinanceApplication only when the model must live in a broad core namespace where Application is ambiguous.

Case Name

Use Application for the borrower-submitted journey. Use MortgageCase or FinanceCase for the assembled operational file that brokers/operators work after intake, lender submission, underwriting, offer, and completion. Do not use Deal as the finance aggregate unless the object is specifically a property transaction.

Target Model Set

Area Target entities Relationships V2.1 direction
Identity and contacts person, person_identity_profile, person_nationality, person_residency, person_tax_residency, person_role, platform_account, authorization_assignment, bundle_assignment, contact_method, person_contact_method, address, person_address, imported_record_link person owns stable human identity. person_identity_profile and its nationality/residency children own reusable KYC/preload facts. platform_account links Sooner product access to a person without becoming the applicant/customer model. Authorization assignments and bundle assignments mirror the Sooner-IAM permission model. Contact methods live in contact_method and are linked through owner-specific join tables. Keep reusable person KYC separate from platform access, IAM authorization, contact methods, addresses, source-system lineage, and application-party snapshots. Do not revive catch-all identity or source-reference names.
Institutions and contact-only people institution, institution_classification, institution_identifier, institution_contact_method, institution_address, person_institution_role, related_contact, application_related_contact, application_related_contact_purpose Institutions represent employers, lenders, brokers, solicitors, conveyancers, valuers, credit providers, and other finance ecosystem entities. Classifications and identifiers are child rows because an institution can have multiple roles and registrations. related_contact captures partner, emergency, employer, adviser, lender, solicitor, or conveyancer contacts that are not necessarily application parties. Use institution only when the counterparty/provider is reusable or has regulatory/commercial meaning. Use related_contact for contact-only people or one-off contacts attached to one relationship or application.
Application application, application_status_history, application_party, application_party_identity, application_party_nationality, application_party_residency, application_party_tax_residency, application_party_role, application_party_relationship, application_institution_role One application can have many parties. Participation, application-time identity disclosure, legal/workflow capacity, and party-to-party relationship are separate records. Branch flags such as has_co_applicant become child records or statuses, not durable booleans. Use application as the finance journey in the finance context. Use party rows instead of person_id, primary/co-applicant columns, one overloaded role field, or person-level residency/nationality scalars. Use property_search_status for property-search state.
Requested finance requested_loan_terms, selected_service, fee_quote Requested amount, term, rate preference, repayment type, purpose, LTV, and customer-selected service live outside applicant identity. Displayed fee tier and percentage live in fee_quote when they need an auditable quote snapshot. Do not use application_product as the center of lender progress. Requested terms are the ask; lender-specific progress belongs to lender_submission. Fee tier is derived from selected services, not a borrower-submitted fact.
Applicant finances employment, income_source, asset, liability, housing_expense, funding_source Financial facts are repeatable, party-scoped or application-scoped, currency-aware, and source-verifiable. Move income, employer, rent, debts, savings, and deposit funding out of application scalar fields. Do not introduce real-estate ownership tables for this forms pass; use assets/liabilities only where Gabriel's capture flow actually asks for them.
Credit credit_check, credit_report, credit_score, credit_report_tradeline, consent_event AECB, Experian, Equifax, TransUnion, and other bureaus/providers are provider institutions or provider values, not table names. Model the search request as credit_check; model returned files and data as credit_report, scores, and tradelines. Consent is a timestamped event, not a generic credit table.
Lender path and outcomes application_cost_estimate, lender_submission, affordability_assessment, decision_in_principle, credit_decision, mortgage_offer One application can have displayed cost estimates and be packaged for one or more lenders. DIP/AIP, underwriting, conditions, credit decisions, and offers hang from the lender path. Do not flatten displayed costs, SCS scores, gate failures, or lender outcomes into submitted fields or one application status. Persist them as cost/fee/assessment/decision snapshots when audit history is needed.
Property preference application_property_preference Preferences describe search intent and purchase timeline: target amount, currency, locations, property types, bedrooms, bathrooms, intended use, purchase timeline, and search status. Keep Gabriel's specific-property prompts as optional intake detail under application_property_preference.metadata.specific_property. Do not introduce selected-property, listing, valuation, ownership, or lender-property entities in this pass.
Documents document, document_link, document_requirement, document_requirement_document, document_review, document_extraction, verification_event Documents are files. Links say what the file supports. Requirements say what evidence is needed. The satisfaction join says which documents fulfil which requirements. Replace one-column document slots and generic active subject links with typed evidence links and typed requirement targets.
Operations mortgage_case, mortgage_case_party, mortgage_case_institution_role, workflow_track, workflow_stage, activity, task Case work starts from an application, lender submission, or accepted offer. Active workflow records should use concrete typed links where the database needs FK safety. Demote or remove lead from finance core. Keep CRM intake separate from application, lender submission, and mortgage case records.
Communication and AI conversation, typed conversation links, assistant_conversation, assistant_context, extracted_fact, audit_log, webhook Conversations and extracted facts can reference application, party, case, submission, document, property, or institution through typed active links. Reserve polymorphic subject_type/subject_id for immutable audit, webhook, import, and event payloads where FK enforcement is not required.

Lifecycle Model

Lifecycle concept Target entity Status values Notes
Started journey application draft, intake_started A person may have more than one application over time; nullified or restarted journeys create new records.
Initial eligibility affordability_assessment, decision_in_principle requested, eligible, referred, declined, expired UK products commonly call this Decision in Principle or Agreement in Principle. US products often call this preapproval or prequalification.
Lender path lender_submission draft, submitted, in_review, conditions_required, approved, declined, withdrawn Use one lender submission for each lender-specific package. DIP/AIP, underwriting, conditions, decisions, and offers attach here.
Full application application, document_requirement submitted, documents_required, in_review Submission is not the same as eligibility. Required documents should be tracked as requirements and satisfaction events.
Underwriting credit_decision, document_review pending, approved, approved_with_conditions, declined Separate applicant affordability, credit, and document verification outcomes. Attach lender-specific outcomes to lender_submission.
Offer mortgage_offer issued, accepted, revised, withdrawn, expired The formal offer has terms, expiry, lender, conditions, and status.
Completion and customer mortgage_case, person_role completed, customer_active After approval/completion, the person can gain customer role while preserving application history.

Next Rename Set

Paused V2 name V2.1 target Action
FinanceApplication application Use Application as the Prisma model in the finance context and map the table to application. Use MortgageApplication only if the bounded context becomes explicitly mortgage-only.
FinanceApplication.personId, co_app_*, relationshipToPrimary application_party, application_party_role, application_party_relationship Support primary applicant, joint applicant, guarantor, sponsor, spouse, donor, occupier, and representative without adding columns or overloading one role field.
IdentityLink person_identity_profile, platform_account, authorization_assignment, bundle_assignment, contact_method, address, owner join tables, imported_record_link Remove as a catch-all. Split reusable KYC/profile facts, platform login/account state, IAM authorization, reusable contact methods, and imported source-record lineage.
Lead inquiry or removed Keep only as CRM intake before application creation. Do not make it the canonical finance object.
LeadTrack, PipelineStage, LeadActivity workflow_track, workflow_stage, activity Attach workflow to concrete application, lender submission, mortgage case, party, document, property, or conversation links when FK safety matters.
Deal mortgage_case Use case for the operational file. Reserve deal for accepted property/commercial transaction if that remains a separate module.
Property Do not add a canonical table in this pass Gabriel's current forms capture preferences and optional typed detail, not a durable property record.
PropertyPreference application_property_preference Store application-time purchase intent separately from any saved person-level search preferences.
SelectedService, FinanceApplicationProduct selected_service + fee_quote + requested_loan_terms Selected services describe the customer's Sooner package choices. Fee quote stores the displayed derived fee tier. Requested terms describe the finance ask; lender-specific packages belong to lender_submission.
cost_breakdown, sooner_reveal, fee_tier_id, indicative_scs, decision application_cost_estimate, fee_quote, affordability_assessment, decision_in_principle, credit_decision Keep derived/display-only values out of submitted capture. Persist them only as quote, cost, assessment, or decision snapshots when Sooner needs audit history.
has_co_applicant, has_specific_property application_party, application_property_preference.property_search_status These are branch controls. Durable state comes from child records and statuses: a co-applicant party exists, or property-search status says whether a property has been identified.
property_options, locked_option_id, property_options_complete application.metadata.forms.property_options Treat as UI/progressive form state unless Sooner later decides to productize recommendations.
trade_license_number, trade_license_pdf employment.trade_license_number_text or institution_identifier + document_link Typed business licence values and PDF evidence are separate concepts.
LenderOffer lender_submission + decision_in_principle + credit_decision + mortgage_offer Decision and offer should not be one overloaded record.
DocumentVerification document_review or verification_event Use review for human/operator workflow; use verification for provider-backed verification events.
DocumentEntityType, active subjectType/subjectId links document_link, document_requirement, typed conversation/workflow links Prefer explicit relations for active core subjects. Keep polymorphic subject_type/subject_id only for immutable audit/webhook/event payloads.

US vs UK Language

Keep schema names market-portable, then localize product copy and provider values. The UK-facing product language should prefer mortgage, applicant, affordability, credit reference agency, Decision in Principle, mortgage offer, and conveyancer/solicitor where those concepts are visible to users.

Terminology Map

Canonical schema term UK-facing label US/common data-standard term Modeling note
Application Mortgage application Loan application Schema can stay Application; product copy can say mortgage application in UK markets.
DecisionInPrinciple Decision in Principle / Agreement in Principle Preapproval / prequalification Separate this from the full application and formal offer.
ApplicationParty Applicant, joint applicant, guarantor, representative Borrower, co-borrower, additional borrower, party role Use roles on the application rather than duplicate applicant columns.
ApplicationPropertyPreference Property search preferences Property preferences / search criteria For this pass, store search intent and optional form detail only. Do not add separate property, option, valuation, or lender-property entities.
Liability Credit commitment / monthly commitment / outgoing Liability Use Liability for debts and credit exposure; use HousingExpense or Expense for rent/outgoings that are not debts.
HousingExpense Rent / housing cost / monthly outgoing Housing expense Use this for rent and housing costs that affect affordability but are not debt liabilities.
ApplicationCostEstimate, FeeQuote Cost breakdown / fee quote Loan estimate / cost estimate / pricing quote Displayed cost and fee outputs are derived snapshots, not borrower-submitted fields.
CreditDataProvider Credit reference agency Credit bureau Provider values can include aecb, experian, equifax, transunion, and market-specific providers.
MortgageOffer Mortgage offer Loan approval / commitment letter Keep offer separate from preliminary decision and underwriting decision.
Completion Completion Closing Use status values that can localize: completion_pending can display as closing in US markets.
Conveyancer Solicitor / licensed conveyancer Closing agent / settlement agent / attorney Use role values on Institution or CaseParty if legal/closing participants are modeled.

UK-Aligned Choices

  • Use MortgageApplication only where a visible schema/API name must be product-specific.
  • Use DecisionInPrinciple for the preliminary lender decision record.
  • Use AffordabilityAssessment for income, expenditure, commitments, stress, and eligibility calculations.
  • Use ApplicationCostEstimate and FeeQuote for displayed quote outputs when audit history is needed.
  • Use CreditReferenceAgency as a label, but keep the schema provider-neutral as CreditDataProvider.
  • Use Completion in UK workflows; map to closing only in US-facing products.

Portable Schema Choices

  • Store currency, not currency in field names.
  • Store countryCode, documentType, and localDocumentType, not permanent columns for one market's identity document.
  • Store provider for credit data, document checks, and open-banking sources.
  • Store application-party roles rather than primary/co-applicant prefixed canonical columns.
  • Store property-search state on ApplicationPropertyPreference. Keep option comparison and specific-property typed detail in owner metadata with lower snake case keys unless the product later needs a canonical property module.
  • Store rent as HousingExpense; keep debt, cards, leases, and overdrafts in Liability.
  • Use lower snake case enum values in the database and localize labels at the edge.

Research References

Reference Language to align with Schema implication
MISMO Reference Model Residential mortgage lifecycle, logical data dictionary, documents, loan application, parties, and reference model extensions. Model the application as connected containers rather than one flat row.
Fannie Mae MISMO/ULDD guide MESSAGE, DEAL, LOAN, PARTY, COLLATERAL, ASSET, and party roles. Use parties and roles; avoid encoding borrower/co-borrower as fixed columns.
HSBC UK Decision in Principle Decision in Principle, Agreement in Principle, Mortgage in Principle, full mortgage application, mortgage offer, solicitor/conveyancer. Add preliminary decision and offer records rather than overloading application status.
Santander UK Decision in Principle DIP/AIP naming, full mortgage application, hard credit check, proof of income, changed offer after deeper checks. Track preliminary decision, full submission, credit checks, proof requirements, and offer separately.
FCA MCOB 11A Mortgage lender, mortgage credit intermediary, consumer, affordability assessment. Use affordability assessment as a first-class finance record, not just a derived UI value.

Current V2 Naming Remodel

Current V2 is a paused work-in-progress snapshot. It is useful as context for what exists today, but it is not the target answer. The remodel below uses the current schema only as a reference point for what should be kept, renamed, split, or removed.

In a finance bounded context, use Application, ApplicationParty, SelectedService, and related names. In a broad core context, keep the Finance prefix to avoid ambiguity.

From-scratch recommendation Application in finance context Party roles over lead Repeatable financial facts Derived outputs as snapshots Property intent vs subject property

Recommended Domain Shape

  • identity or core: Person, PersonIdentityProfile, PersonNationality, PersonResidency, PersonTaxResidency, ContactMethod, PersonRole.
  • finance: Application, ApplicationParty, ApplicationPartyIdentity, Employment, IncomeSource, Asset, Liability, HousingExpense, CreditCheck, CreditReport, RequestedLoanTerms, SelectedService, FeeQuote, ApplicationCostEstimate, LenderSubmission, AffordabilityAssessment, MortgageOffer.
  • institutions: Institution, InstitutionContactMethod, PersonInstitutionRole, and application-specific institution roles.
  • property preference: ApplicationPropertyPreference only for this pass.
  • documents or shared core: Document, DocumentLink, DocumentRequirement, DocumentReview, DocumentExtraction.
  • communication: Conversation, Message, MessageChannel, MessageTemplate.
  • operations: Task, WorkflowStage, Activity, labels/custom fields/audit.
  • intelligence: assistant context, extracted facts, prompt versions, telemetry, feedback.

Application Naming

In lending and consumer finance, application is established domain language. For the core journey object, use Application inside the finance bounded context. Use MortgageApplication if the model is intentionally mortgage-specific. Use LoanApplication only if the product is legally and operationally a loan. Use CreditApplication when the product is a broader request for credit and may not always become a loan. Use FinanceApplication only as a namespace-disambiguation name in a broad core context.

  • Application: preferred model name in a finance bounded context.
  • MortgageApplication: use when the product and schema are explicitly mortgage-only.
  • LoanApplication: use when the application is specifically for a loan product.
  • CreditApplication: use when the legal concept is a request for an extension of credit, not necessarily a loan.
  • FinanceApplication: use only when the codebase lacks a finance context and needs disambiguation.
  • /v1/applications when the API boundary is already finance-specific; otherwise use a prefixed route.
  • model Application @@schema("finance") @@map("application") for the clean bounded-context model.

Application Naming Research

Reference Uses Modeling implication
Fannie Mae / Freddie Mac URLA Uniform Residential Loan Application, Uniform Loan Application Dataset, borrower, additional borrower. Application is normal in a lending context. Use LoanApplication only if the product is literally a loan origination flow.
CFPB Regulation B Application, applicant, credit, extension of credit, completed application. CreditApplication is the broader regulatory concept. It is useful if Sooner’s product is a request for credit that may not always be a loan.
ULAD / MISMO relationships DEAL, LOAN, PARTY, ROLE, BORROWER, ASSET, LIABILITY, EMPLOYER, income items. Model people through parties/roles attached to the application or loan case, not through one applicant column set.
Fannie Mae application package loan application, Form 1003, borrower, loan file. Application can be the intake object; LoanFile or Casefile are better names for assembled underwriting/submission packages.
MISMO Reference Model Mortgage lifecycle model with logical data model, XML schema, logical data dictionary, SMART Doc standards. Use finance-domain objects and relationships; do not flatten application data into one person/customer row.
HSBC UK Decision in Principle Decision in Principle, Agreement in Principle, full mortgage application, mortgage offer. For UK-facing mortgage flows, model preliminary decision, full application, and formal offer separately.
Santander UK Decision in Principle DIP/AIP naming, full mortgage application, proof of income, hard credit check, changed mortgage offer after detailed checks. Do not let one application status carry all eligibility, verification, credit, and offer meanings.

Current V2 Naming Review

Current V2 name Assessment Remodeled name Why
Person Keep. Person Good canonical identity noun. Remove UAE-specific fields like emiratesId from the person row.
PersonRoleKind.applicant, customer, missing user Needs lifecycle clarity. user, applicant, customer, operator Person becomes a user on signup, an applicant through an application party, and a customer after approval/onboarding into the system.
FinanceApplication Safe but verbose. Application inside finance context; otherwise keep FinanceApplication Standard lending language says loan/application. The bounded context removes ambiguity.
FinanceApplication.personId Too single-borrower. ApplicationParty Applications can have multiple people with roles: primary applicant, co-applicant/co-borrower, guarantor, sponsor, spouse, representative.
FinanceApplication.monthlyIncome, employerNameText Too flat. Employment + IncomeSource Income and employment are repeatable and party-specific. They should not live as scalar fields on the application.
Missing liabilities/assets Major gap. Liability, HousingExpense, Asset Lending systems evaluate assets, liabilities, housing expense, and debt-to-income. Rent belongs in HousingExpense; do not add property ownership/interest tables for this forms pass.
Missing credit provider/report model Major gap. CreditCheck, CreditReport, CreditScore, CreditReportTradeline AECB should be a provider/institution value, not a schema name. Other markets can plug in their own credit reference agencies or bureaus.
DocumentType.emiratesId, salarySlip, noc Too narrow. DocumentType + documentCountry + localDocumentType + documentSide Country-specific IDs and provider documents should be taxonomy values, not the only canonical set.
DocumentEntityType with polymorphic entityId Useful but under-specified. DocumentLink, DocumentRequirement, typed relation tables Use typed target foreign keys for active lending documents. Reserve subjectType/subjectId for immutable audit/webhook/event payloads.
PropertyPreference.personId Partly right, but application context matters. ApplicationPropertyPreference The form preference is application-time intent: target amount, location choices, property types, bedroom/bathroom count choices, purchase timeline, and intended use. A person-level saved preference can be separate later.
Property recommendation/match models, selected_property, property_options Too much for current capture. ApplicationPropertyPreference plus owner metadata Gabriel's flow captures preferences, optional typed detail, and option-selection UI state. Keep this out of canonical property tables for now.
Deal Not the finance aggregate. MortgageCase or property transaction record Use MortgageCase for the operational file. Reserve deal for an accepted property/commercial transaction if that becomes first-class.
Lead Questionable as canonical. Remove or demote to WorkflowCase / PipelineItem Person/application state can replace much of Lead. If CRM needs a queue item, make it workflow-specific rather than domain identity.
LeadTrack Useful concept, wrong anchor. WorkflowTrack linked to Application, MortgageCase, LenderSubmission, or Person Tracks like customer/mortgage/property/listing are operational state machines, not necessarily lead-owned.
PipelineStage Keep with rename. WorkflowStage “Pipeline” is CRM-specific; workflow stage works across onboarding, mortgage, property, and document review.
Task, LeadActivity Need broader subject naming. Task, Activity with subjectType/subjectId Tasks/activities should attach to application, person, deal, document, or conversation, not only lead.
Generic external/source ID fields Too broad. imported_record_link for migrated/source lineage; domain-specific provider reference fields for lender, credit, document, and messaging records. V1 buyer ID, Supabase ID, Rasayel ID, and credit bureau IDs are not the same concept and should not share one vague source-reference field.
ResidencyStatus.uaeResident Too market-specific. ResidencyStatus.resident + residencyCountry Market should be a field; enum values should not hard-code UAE when the concept is global.
Hyphenated enum storage values Conflicts with current naming standard. lower snake case values Use lower_snake_case for DB enum values: in_review, co_applicant, property_finder.
AssistantContext.leadId, ExtractedFact.leadId Likely wrong anchor. Link to Application, ApplicationParty, Person, or Conversation Facts extracted from conversations should project into canonical application/person concepts, not a possibly removable lead.
cost_breakdown, fee_tier_id, indicative_scs, decision Derived/display-only, not capture. ApplicationCostEstimate, FeeQuote, AffordabilityAssessment, DecisionInPrinciple, CreditDecision Persist only when Sooner needs an auditable quote or decision snapshot.
has_co_applicant, has_specific_property Form branch flags. ApplicationParty, ApplicationPropertyPreference.propertySearchStatus Durable state is represented by child records or statuses, not standalone booleans.
trade_license_number, trade_license_pdf Typed value plus evidence document. Employment.tradeLicenseNumberText or InstitutionIdentifier plus DocumentLink Do not merge the scalar licence number and the PDF evidence into one document field.

From-Scratch Core Model Set

Area Models Notes
Identity Person, PersonIdentityProfile, PersonNationality, PersonResidency, PersonTaxResidency, PersonRole, ContactMethod, Institution, PersonInstitutionRole, ImportedRecordLink Keep person identity stable, make reusable KYC facts preloadable, and keep application-party identity as the submitted lending snapshot.
Application Application, ApplicationParty, ApplicationStatusHistory, SelectedService, ApplicationAcknowledgement, ConsentEvent Use this if there is a finance bounded context. Otherwise prefix these with Finance. Branch flags become statuses, child rows, or events.
Financial profile Employment, IncomeSource, Asset, Liability, HousingExpense, FundingSource Repeatable, party-scoped, currency-aware, and source-verifiable. Rent is a housing expense, not a loan liability.
Credit and verification CreditCheck, CreditReport, CreditScore, CreditReportTradeline, VerificationEvent AECB is a provider/institution value. Verification events should preserve source and timestamp.
Documents Document, DocumentRequirement, DocumentVerification, DocumentExtraction Requirements attach to application/party/product; documents satisfy requirements.
Property preference and case ApplicationPropertyPreference, MortgageCase Keep search intent on the application. Do not add selected-property, option-comparison, ownership, valuation, or lender-property models until a product flow requires them.
Derived outputs ApplicationCostEstimate, FeeQuote, AffordabilityAssessment, DecisionInPrinciple, CreditDecision Displayed costs, fee tiers, SCS scores, gates, and decisions are snapshots generated by Sooner or lenders, not submitted form fields.
Operations WorkflowTrack, WorkflowStage, Activity, Task, LabelDefinition, CustomFieldDefinition Workflow attaches to real subjects rather than owning domain identity.
Messaging and AI Conversation, Message, AssistantSession, ExtractedFact, KnowledgeEntry, PromptVersion, AssistantTelemetry AI objects should reference canonical subjects and preserve provenance.

Suggested Role Values

  • PersonRoleKind: user, customer, operator, agent, broker, lender_contact.
  • ApplicationPartyRole: primary_applicant, co_applicant, borrower, co_borrower, guarantor, sponsor, spouse, representative.
  • ApplicationStatus: draft, submitted, in_review, approved, declined, withdrawn, nullified, expired.
  • LiabilityType: mortgage, personal_loan, vehicle_loan, credit_card, lease, overdraft, support_obligation, other.
  • HousingExpenseType: rent, service_charge, maintenance, utilities, other.

Research Backing

This remodel follows the shape used by established mortgage data standards: application/loan container, borrower/additional borrower parties, personal information, employment/income, financial assets, liabilities, real estate, loan/property information, declarations, acknowledgements, and document/credit verification.

  • MISMO Reference Model: industry reference model and logical data dictionary for mortgage business processes.
  • Fannie Mae URLA/Form 1003: standardized residential loan application and ULAD mapping to MISMO.
  • URLA instructions: sections for borrower info, assets/liabilities, loan/property, declarations, and acknowledgements.
  • Fannie Mae liabilities guide: liabilities include recurring obligations such as housing payment, revolving accounts, installment debts, leases, real estate loans, support obligations, and other debts.

Core Position

Treat the current forms catalog as a capture surface, not as the canonical database model. The durable model should separate person, application, application_party, application_party_role, financial profile records, lender_submission, application_property_preference, document requirements, derived quote/decision snapshots, and operational case work. Form fields can still be copied into metadata.v1 or metadata.forms for traceability, using lower snake case JSON keys, but Prisma names should describe business concepts, not the current UI step or UAE-only document slot.

ULID IDs lower snake enum storage business-readable public nouns market-portable document model typed active links lender path as first-class record derived values as snapshots form gates are not durable booleans

Prefer These Nouns

  • application: the finance journey container inside the finance bounded context.
  • application_party: a person participating in one application.
  • application_party_role: repeatable legal/workflow capacity such as borrower, guarantor, occupier, representative, or adviser.
  • application_party_relationship: relationship between parties, such as spouse, partner, donor, dependent, or representative-for.
  • contact_method, address, and related_contact: reusable email/phone/WhatsApp values, structured addresses, and contact-only people who may later be promoted to a full person record.
  • application_related_contact: the link that says a contact matters to one application, without pretending the contact is an applicant.
  • lender_submission: one lender-specific path for DIP/AIP, underwriting, conditions, decisions, and offers.
  • credit_check: the soft/hard credit search event before reports, scores, and tradelines.
  • application_property_preference: the buyer's search intent, including target amount, locations, property type choices, bedrooms/bathrooms, purchase timeline, intended use, search status, and optional specific-property intake detail.
  • housing_expense: rent or other housing cost that is not a loan liability.
  • application_cost_estimate, fee_quote, and affordability_assessment: calculated outputs and quote/decision snapshots, not borrower-submitted form facts.
  • document_link + document_requirement: typed evidence links and evidence requests, not one column per document tile.

Avoid These Anchors

  • Do not make emiratesIdFront, visaPage, or aecbReport permanent person columns.
  • Do not use co_app_* as durable DB shape; use another application_party row with role assignments and relationships.
  • Do not use buyer for new V2 canonical objects. Preserve V1 buyer identity as source lineage, then map it into person/application/party records.
  • Do not assume Lead is the correct V2 center. A person can become user, applicant, and customer through roles and application state.
  • Do not flatten repeatable finance facts into one row if a user can have multiple jobs, liabilities, or documents.
  • Do not persist intake gates such as has_co_applicant or has_specific_property as durable source-of-truth booleans when the state is represented by child records or statuses.
  • Do not make display-only values such as cost_breakdown, sooner_reveal, fee_tier_id, or SCS decisions look like borrower-submitted fields.
  • Do not model rent as a loan liability. Use housing_expense for rent and liability for debts, cards, leases, overdrafts, and other obligations.
  • Do not hang active document, workflow, conversation, or extracted-fact links on generic polymorphic subjects when typed foreign keys are needed.
  • Do not force every partner, emergency contact, employer contact, or lender contact into person. Use related_contact until the system needs a canonical person identity.
  • Do not store lender outcomes directly as a single application status; use lender_submission and outcome records.
  • Do not bake UAE, AED, AECB, or Emirates-specific naming into generic tables unless the table is explicitly market-specific reference data.

Concrete Rename Prompts

Current / likely name Prompt Candidate direction
buyer_lead Classify this as a form submission, person identity, or application start. Use form_submission for capture and Application for the financing journey. Re-evaluate whether Lead remains needed.
buyer_document Is the owner a buyer, an applicant, an application, or a person? Use document for the file, document_link with typed target FKs for evidence, and document_requirement for required docs.
co_app_full_name, co_app_income Would this support two co-applicants without adding more columns? Replace with another application_party, then assign capacity through application_party_role and store income/liabilities as child rows.
partner_income_monthly_aed Is “partner” legally the same as co-applicant? Use application_party_relationship for the relationship and application_party_role for legal capacity; keep income on income_source.
partner_email, emergency_contact_phone, lender_contact_name Is this a person in our system or only contact information needed for this application? Use contact_method for email/phone/WhatsApp, address for structured addresses, and related_contact or application_related_contact for contact-only relationships.
employment_type, business_years, employer_tenure_bucket Can one applicant have current employment plus side business? Model Employment records with employmentType, startedOn or tenure bucket, and employer institution.
fixed_income_aed, variable_income_aed Are these attributes of the person, the application, or a specific employment record? Use IncomeSource with amount, cadence, currency, verification status, and source employment.
has_personal_loan + personal_loan_payment What happens when an applicant has two personal loans? Use repeatable Liability rows with type, monthlyPayment, currency, and verification source.
emirates_id_front, emirates_id_back Is this a UAE-only field or a document type in a global identity-document taxonomy? Use DocumentType = emirates_id plus DocumentSide = front/back, or two files grouped by requirement.
aecb_report Is AECB the concept, or just the UAE provider of a credit report? Use credit_check for the search event, credit_report for the returned report, and provider institution/value aecb for UAE.
residency Is this legal residency, nationality, tax residency, or mortgage eligibility category? Use application_party_residency for application-time legal/immigration residency and application_party_tax_residency for tax residency. Avoid UAE-only values in generic enums.
property_budget_aed Is this desired price, approved budget, or max purchase amount? Use application_property_preference.target_amount + currency; reserve assessed borrowing capacity for affordability_assessment.
service_selection Is this product selection, bundle selection, or fee tier input? Use selected_service for customer-selected Sooner services. Store derived commercial outputs in fee_quote when the shown fee tier needs an auditable snapshot.
mortgage_status, preapproval_status Is this the applicant's mortgage-search state, or an actual lender-specific outcome? Use application.mortgage_search_status for intake/search state. Use lender_submission, decision_in_principle, credit_decision, and mortgage_offer only for lender outcomes.
selected_property, specific_property Is this only typed intake detail from the form? Keep it under application_property_preference.metadata.specific_property unless a future product flow needs a canonical property module.
timeline, property_timeline Is this a generic property timeline or the buyer's purchase timeline? Use application_property_preference.purchase_timeline. Avoid broad names such as property_timeline.
has_co_applicant Is this only a form branch or the durable application state? Use the presence of an application_party with role = co_applicant as durable state. Keep the boolean as intake/UI gate only.
has_specific_property, listing_url, exact_price, freehold_status Is this search intent or optional typed intake detail? Use application_property_preference.property_search_status for the search state and optional metadata.specific_property for values the applicant typed.
property_options, locked_option_id, property_options_complete Is this just the form's option-selection state? Treat these as application.metadata.forms values with lower snake case keys unless Sooner later productizes recommendations.
rent_payment, has_rent Is rent a debt liability? Use housing_expense(type = rent) with monthly amount and currency. Keep loan/card obligations in liability.
trade_license_number, trade_license_pdf Is this the typed licence number or the evidence document? Use employment.trade_license_number_text or institution_identifier(type = trade_license) for the value; use document and typed links for the PDF evidence.
cost_breakdown, sooner_reveal, fee_tier_id, indicative_scs, decision Is this submitted by the borrower or calculated/displayed by Sooner? Keep display-only values out of the capture payload. Persist through application_cost_estimate, fee_quote, affordability_assessment, decision_in_principle, or credit_decision only when audit history is needed.
rejection_reason, rejection_other What exactly was rejected? Scope rejection feedback to mortgage_offer, lender_submission, credit_decision, or document review, not to the whole person.

Prisma Naming Prompts

  1. If the name contains a UI step, can it be renamed to the domain object that survives after the form changes?
  2. If the name contains buyer, should it be Person, Application, ApplicationParty, or lineage in imported_record_link?
  3. If the name contains co_app, should it be another application_party with role assignments and relationships instead of prefixed columns?
  4. If the name contains aed, should the model instead store amount plus currency?
  5. If the name contains a country/provider noun, is that the generic concept or only a value in a provider/type taxonomy?
  6. If the name contains lender approval, DIP, AIP, decision, underwriting, or offer, should it hang from lender_submission rather than the application row?
  7. If the name contains selected property, listing, valuation, ownership, or title, is that actually in Gabriel's capture flow? If not, keep the model at application_property_preference plus owner metadata with lower snake case keys.
  8. If the name contains purchase timeline, is it a buyer preference? Prefer purchase_timeline on property preference over generic property_timeline.
  9. If the name starts with has_, is it just a form branch? Prefer durable child records or statuses over standalone booleans.
  10. If the name is a boolean, is it a stable fact, a workflow acknowledgement, or a timestamped event?
  11. If the name is display-only or calculated, should it live in a quote, estimate, assessment, decision, or analytics event instead of the submitted capture payload?
  12. If the name is status, what lifecycle dimension does it describe? Rename to verificationStatus, applicationStatus, or deliveryStatus when ambiguous.
  13. If a relationship must be enforced by the database, can it use typed FKs instead of generic subjectType/subjectId?
  14. If the field can occur more than once, make it a child entity rather than adding numbered or prefixed columns.

Enum Prompts

  1. Can the enum type name describe the dimension without repeating the model name unnecessarily?
  2. Are stored values lower snake case? Current hyphenated values like uae-resident should be revisited if we are standardizing on lower snake case.
  3. Does the API expose the same literal as the DB? If not, is there a deliberate translation boundary?
  4. Are provider-specific enum values clearly provider values, e.g. creditReportProvider = aecb, rather than generic statuses?
  5. Are values short and contextless, e.g. submitted, not application_submitted?

Suggested Durable Model Boundaries

Boundary Owns Does not own
Person Identity-neutral human profile, reusable KYC/identity profile, reusable nationality/residency/tax-residency facts, contact-only/person promotion state. Application-specific income, co-applicant relationship, document slots, submitted applicant identity snapshots, and lender-specific disclosures.
Application The finance journey/case file, lifecycle history, current requested terms pointer, current decision/offer summary pointers, market, submitted/nullified/restarted events, and search statuses. All personal facts flattened into one row or all lender decisions stored as one status.
ApplicationParty Person-to-application participation, party kind, primary flag, application-time identity snapshot, nationality/residency/tax-residency disclosures copied or declared for this application, and consent state. Legal/workflow capacity or inter-party relationships; use application_party_role and application_party_relationship.
ContactMethod / RelatedContact Reusable contact methods and contact-only people or institutions used by an application. Applicant identity, legal participation, or verified platform-user identity.
LenderSubmission One lender-specific path for requested terms, DIP/AIP, underwriting, conditions, decisions, and offers. A generic application status or product-selection field.
ApplicationPropertyPreference Search intent, target amount, locations, selected count options, purchase timeline, intended use, property search status, and optional form-entered specific-property detail. Canonical property, listing, ownership, valuation, or recommendation-product records.
Employment Employer/business, role, seniority, tenure, industry fallback, verification state. Income totals that belong to multiple employments or application-level affordability.
IncomeSource Amount, currency, cadence, source type, stability/variable-income classification. Employment identity; link to employment when available.
Liability Debt/card/lease/overdraft exposure, monthly payment, balance/limit, lender/provider, verification source. Rent, boolean existence flags as canonical storage, or display-only affordability totals.
HousingExpense Rent and housing costs that affect affordability without being debt liabilities. Loan, card, lease, or overdraft obligations.
ApplicationCostEstimate / FeeQuote / AffordabilityAssessment Calculated cost breakdown, displayed fee tier, affordability inputs, score snapshots, and decision outputs when audit history is required. Borrower-submitted identity, income, property, or document facts.
Document Uploaded/generated file metadata. Evidence meaning belongs to document_link; required evidence belongs to document_requirement. One column per document tile or generic active links that cannot be enforced.

Answered Questions

  1. Canonical journey name: use Application inside the finance bounded context. Use FinanceApplication only when the model lives in a broad context where Application would be ambiguous.
  2. Multiple applications: allow multiple applications per person. A nullified, canceled, or restarted journey should create a new record and preserve history.
  3. Application party roles: do not assume co-applicants are always equal legal participants. Keep party participation, role/capacity assignment, and party-to-party relationship separate.
  4. Employment and liabilities: define and store them canonically now so the form payload can adapt to the target backend shape and slot into the future migration cleanly.
  5. Market portability: do not optimize for one next market. Store market-specific concepts through currency, documentType, countryCode, provider institutions, and local-type fields.
  6. Customer and lead: rework current V2 assumptions. A Person can be a platform user, an applicant when attached to an application, and a customer after an approved application brings them into the system. The Lead concept may be removable or reduced to CRM-only workflow, not canonical identity.
  7. V1 buyer ID lineage: the form ULID should behave like V1 buyer.id. Preserve that lineage through imported_record_link, not a generic externalId column on the domain model.

Schema Comment Seeds

These are draft comments that can later move near Prisma models once names settle.

  • Application: “A finance journey/case file for one or more application parties. A person may have multiple applications over time; nullified or restarted journeys create new records.”
  • ApplicationParty: “A person participating in an application. Keep participation separate from legal/workflow capacity and party-to-party relationship.”
  • ApplicationPartyRoleAssignment: “Repeatable role/capacity for an application party, such as borrower, guarantor, occupier, representative, broker adviser, or lender contact.”
  • RelatedContact: “A contact-only person or institution that may support an application without becoming an application party.”
  • ContactMethod: “Reusable email, phone, or WhatsApp method that can belong to a person, institution, related contact, or application context.”
  • Address: “Structured postal or physical address. Link it to people, institutions, properties, or application snapshots through explicit owner rows.”
  • LenderSubmission: “One lender-specific path for a set of requested terms. DIP/AIP, underwriting, decisions, conditions, and offers hang from this path.”
  • ApplicationPropertyPreference: “A buyer’s property-search intent, including target amount, location choices, property type choices, bedroom/bathroom count choices, purchase timeline, intended use, search status, and optional specific-property intake detail.”
  • Employment: “Repeatable employment or business profile for an application party. Do not flatten co-applicant employment into prefixed canonical columns.”
  • IncomeSource: “Repeatable income component with amount, currency, cadence, and verification state. Use this for salary, commission, self-employment income, and future market-specific income types.”
  • CreditCheck: “Soft or hard credit search event. Provider-specific reports, scores, and tradelines hang from the returned report.”
  • Liability: “Repeatable obligation used for affordability. AECB is one provider of verified credit data, not the model name.”
  • HousingExpense: “Repeatable housing cost such as rent. Keep it separate from debt liabilities.”
  • ApplicationCostEstimate: “Calculated quote/cost snapshot shown to the applicant. Do not treat displayed costs as submitted capture fields.”
  • FeeQuote: “Displayed fee-tier snapshot derived from selected services and pricing rules.”
  • Document: “Generic uploaded or generated file. Country-specific IDs are document types; evidence meaning lives in typed document links and requirements.”

Agent Naming Prompt

Draft guidance for AGENTS.md or CLAUDE.md. This is written as a standing rule for schema work: Sooner should use established lending, mortgage, consumer-finance, and data-standard language before inventing new names.

Prompt Text

Candidate text to add to agent instructions.

## Finance Domain Naming Standards

Sooner is not inventing a new consumer-finance data model. When naming Prisma models, fields, enums, API resources, events, and workflow concepts, prefer established lending and mortgage terminology before creating local product names.

Use current industry vocabulary from mortgage and consumer-finance systems, including MISMO-style concepts where relevant: application, party, role/capacity, borrower/applicant, income, employment, asset, liability, credit check, credit report, property preference, document, requirement, lender submission, decision, offer, case, and workflow.

Use UK-facing language where the product or user experience is UK-aligned: mortgage application, applicant, joint applicant, affordability assessment, credit reference agency, Decision in Principle, Agreement in Principle, mortgage offer, solicitor, conveyancer, and completion.

Use the casing that matches the artifact being edited. In database-facing schema docs, describe tables, columns, and stored enum values in lower_snake_case. In Prisma schema code, use PascalCase model names and camelCase field names only when those are mapped deliberately to lower_snake_case database names. Frontend form state and API JSON may use nested camelCase payload paths, such as application.propertySearchStatus, parties[].person.firstName, parties[].incomeSources[].amount, or parties[].documents[].documentType.

Keep schema names market-portable. Do not hard-code UAE, AED, AECB, Emirates ID, visa, or any other country/provider-specific concept into generic table or field names unless the model is explicitly market-specific reference data. Store these as values such as countryCode, currency, documentType, localDocumentType, provider, creditDataProvider, or market.

Prefer bounded-context clarity over long prefixes. Inside a finance schema or finance module, Application is clear. Use MortgageApplication only when the domain is explicitly mortgage-only. Use LoanApplication only when the product is legally and operationally a loan application. Use FinanceApplication only when the model lives in a broad core namespace and Application would be ambiguous.

Model relationships instead of flattening roles into columns. A person can have multiple applications. An application can have multiple parties. Use ApplicationParty for participation, ApplicationPartyRoleAssignment for legal/workflow capacity, and ApplicationPartyRelationship for relationships between parties. Do not overload one role field with borrower status, spouse relationship, representative-for semantics, and workflow role.

Treat form branch flags as intake controls, not durable schema facts. A field such as hasCoApplicant should create or omit an ApplicationParty with role co_applicant. A field such as hasSpecificProperty should set ApplicationPropertyPreference.propertySearchStatus and may store typed intake detail in metadata.specific_property when the form captures it.

Use metadata JSON deliberately. Make the metadata destination explicit as metadata JSON on the owning record, with snake_case JSON keys. Add metadata only to the owner record that gives the data meaning, such as application.metadata for application-wide form/UI state, application_property_preference.metadata for specific-property intake detail, fee_quote.metadata for supporting displayed quote detail, or document_review.metadata for provider/tool payload fragments. Persist metadata object keys in lower_snake_case. Do not hide queryable, repeatable, auditable, or workflow-relevant finance facts in metadata.

Separate identity from contact information. Use ContactMethod for reusable email, phone, and WhatsApp methods. Use Address for structured postal or physical addresses. Use RelatedContact and ApplicationRelatedContact for partner, emergency, employer, adviser, lender, solicitor, conveyancer, or other contacts that matter to an application but are not necessarily canonical platform people or application parties.

Use LenderSubmission as the lender-specific path inside an application. Decision in Principle, Agreement in Principle, full underwriting, lender conditions, credit decisions, and mortgage offers should hang from the lender submission rather than being flattened into one application status.

Model repeatable financial facts as child records. Employment, income sources, assets, liabilities, housing expenses, funding sources, credit checks, credit reports, decisions, and offers should not be collapsed into one application row or duplicated through primary/co-applicant-prefixed canonical columns.

Keep housing expenses separate from debt liabilities. Rent belongs in HousingExpense, while mortgages, personal loans, vehicle loans, credit cards, overdrafts, leases, support obligations, and other debts belong in Liability.

Keep this pass focused on property preference capture. Use ApplicationPropertyPreference for target amount, selected locations, property type choices, bedroom/bathroom count choices, purchase timeline, intended use, property-search status, and optional specific-property intake detail in metadata.specific_property. Keep option comparison state in application.metadata.forms with lower snake case keys unless Sooner later productizes recommendations.

Do not turn display-only outputs into submitted fields. Cost breakdowns, Sooner reveal events, fee tiers, SCS scores, decisions, and gate failures are derived outputs. Persist them through ApplicationCostEstimate, FeeQuote, AffordabilityAssessment, DecisionInPrinciple, or CreditDecision only when audit history or customer-visible quote history is required.

Keep Person as stable identity. A person can become a platform user, an applicant through an application party, and a customer after approval/completion. Do not use Lead, Buyer, or Customer as the canonical identity object unless there is a deliberate lifecycle or CRM boundary.

Treat Lead as CRM intake only, not the core finance aggregate. If active workflow state is needed, prefer concrete typed links to Application, MortgageCase, LenderSubmission, ApplicationParty, Document, Conversation, or Institution. Use polymorphic subjectType/subjectId only for immutable audit, webhook, or event payloads where database-level FK enforcement is not required.

Use documents as typed evidence. Do not create permanent columns for one market's document tiles, such as emiratesIdFront, visaPage, or aecbReport. Use Document for the file, DocumentLink for typed evidence targets, DocumentRequirement for evidence requests, DocumentRequirementDocument for satisfaction, and DocumentReview/DocumentExtraction for review and extraction state.

Separate typed values from document evidence. A trade license number is an Employment snapshot value or InstitutionIdentifier. A trade license PDF is Document evidence linked to the employment/business or identifier. Apply the same rule to credit reports: AECB is the provider, not the model name.

Do not introduce canonical selected-property, property ownership, valuation, listing, or lender-property models from Gabriel's current forms. The current capture surface supports property preferences and optional typed intake detail only.

Separate institution from IAM. Use Institution for real-world finance ecosystem entities such as lenders, employers, brokers, solicitors, conveyancers, valuers, insurers, government agencies, and credit reference agencies. Use PlatformAccount for the Sooner product account that links platform access to a Person. Use AuthorizationAssignment and BundleAssignment for tenant-scoped Sooner-IAM access. Use ImportedRecordLink only for lineage back to migrated or imported source rows.

Model reusable KYC facts separately from application snapshots. A person can have multiple nationalities, legal/immigration residencies, tax residencies, passports, and identity documents across time. Store reusable person facts under PersonIdentityProfile and its child records, then copy or reference them into ApplicationPartyIdentity, ApplicationPartyNationality, ApplicationPartyResidency, and ApplicationPartyTaxResidency for the submitted lending snapshot. Do not use scalar nationalityCountryCode or residencyCountryCode columns on Person.

Separate lifecycle records. Preliminary eligibility, affordability assessment, credit decision, underwriting result, formal mortgage offer, completion, cancellation, withdrawal, expiry, and nullification should not all be overloaded into one generic status field.

Scope rejection feedback to the rejected object. Offer rejection reasons belong to the MortgageOffer or LenderSubmission response. Credit or document rejection reasons belong to CreditDecision or DocumentReview. Do not attach generic rejectionReason fields to Person or Application without a clear subject.

Use lower_snake_case stored enum values. Keep enum values short and context-aware, such as in_review, co_applicant, credit_report, decision_in_principle, mortgage_offer, withdrawn, nullified, and expired.

Before adding or renaming a finance-domain model or field, check whether the same concept already has a standard lending, mortgage, MISMO, FCA, lender, or credit-reporting name. If there is uncertainty, record the naming question in the working schema notes instead of inventing a local abstraction.

Checklist For Future Agents

Question Expected naming move
Is this a known lending or mortgage concept? Use the established term first; only invent a product-specific name when the standard term is wrong.
Does the name contain a country, currency, provider, or local document name? Move it into countryCode, currency, provider, documentType, or localDocumentType.
Does the name encode primary/co-applicant as columns? Use ApplicationParty rows, ApplicationPartyRoleAssignment, and ApplicationPartyRelationship.
Is this a branch flag such as has_co_applicant or has_specific_property? Use child records or statuses as durable state: ApplicationParty(role = co_applicant) or ApplicationPropertyPreference.propertySearchStatus.
Is this only an email, phone, address, or named contact? Use ContactMethod, RelatedContact, or ApplicationRelatedContact; promote to Person only when canonical identity is needed.
Is this a platform account, IAM subject, email, or phone? Use PlatformAccount for the Sooner product account and ContactMethod for reusable email or phone. Do not add one column per auth provider.
Is this tenant-scoped IAM authorization? Use AuthorizationAssignment and BundleAssignment, matching the Sooner-IAM subject and bundle model.
Is this a V1, Supabase, form, or imported source row ID? Use ImportedRecordLink only for source-row lineage. Provider/lender case references should live on their domain records.
Can the party disclose more than one nationality or residency? Use reusable PersonNationality, PersonResidency, and PersonTaxResidency when the fact belongs to the person profile. Use ApplicationPartyNationality, ApplicationPartyResidency, and ApplicationPartyTaxResidency when it is the submitted application snapshot.
Can the fact repeat? Make it a child entity: Employment, IncomeSource, Asset, Liability, HousingExpense, FundingSource, Document, CreditCheck, CreditReport, or MortgageOffer.
Is this rent or another housing cost? Use HousingExpense, not Liability, unless it is a debt obligation.
Is this purchase/search intent or optional form-entered property detail? Use ApplicationPropertyPreference for intent and owner metadata with lower snake case keys for optional typed detail.
Is this a calculated cost, fee tier, SCS, or decision output? Use ApplicationCostEstimate, FeeQuote, AffordabilityAssessment, DecisionInPrinciple, or CreditDecision. Do not treat it as a submitted capture field.
Is a status mixing multiple lifecycle dimensions? Split into specific records or statuses: application status, lender submission status, document review status, credit decision status, offer status, workflow stage.
Is a decision, DIP/AIP, underwriting result, or mortgage offer attached directly to application? Route it through LenderSubmission unless it is a derived application summary.
Is a document, task, conversation, or extracted fact linked through generic subject fields? Use typed links for active domain relations; reserve polymorphic subject fields for immutable audit/webhook/event payloads.
Is this a typed value plus document evidence, such as trade license number and PDF? Store the typed value on the domain record or identifier table; store the file as Document evidence through typed links.
Is this rejection feedback? Attach it to the object being rejected: MortgageOffer, LenderSubmission, CreditDecision, or DocumentReview.
Is Lead, Buyer, or Customer being used as the center? Check whether the durable center should be Person, Application, ApplicationParty, or MortgageCase.