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 propertyPreference.propertySearchStatus, parties[0].person.firstName Matches TypeScript and React form conventions while preserving backend structure.
Prisma model fields camelCase fields on PascalCase models ApplicationPropertyPreference.propertySearchStatus, Person.firstName Matches Prisma and generated client conventions.
Database tables and columns lower snake case through Prisma mapping application_property_preference.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": {
    "mortgageSearchStatus": "searching"
  },
  "propertyPreference": {
    "propertySearchStatus": "identified",
    "targetAmount": 2500000,
    "currency": "AED"
  },
  "parties": [
    {
      "roles": ["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, ContactMethod Represent primary applicant and co-applicants as parties, not prefixed field sets.
Applicant identity facts IdentityProfile, Nationality, Residency, TaxResidency 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 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 RequestedLoanTerms, 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 propertyPreference.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 in ApplicationPartyRole for an ApplicationParty. 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, mortgage search state, and consent/acknowledgements.
Applicant identity Person, ContactMethod, ApplicationParty, IdentityProfile, Nationality, Residency, TaxResidency Captures person identity, contact methods, application participation, role, verification, and submitted applicant identity, nationality, residency, and tax-residency facts.
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, DocumentLink, DocumentReview, VerificationEvent, CreditReport Captures required evidence, uploaded files, typed document links, extracted data, document review, verification events, and credit 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 propertyPreference.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[].residencies[] Residency 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[].nationalities[] Nationality countryCode, status, isPrimary, startedOn, endedOn, documents[] A party can disclose more than one nationality/citizenship for one application.
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 an ApplicationPartyRole(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, IdentityProfile, 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[].creditScores[] CreditScore provider = aecb, source = self_reported, score, score_band, declared_at This is applicant-declared credit standing. Official bureau data belongs to CreditCheck, CreditReport, and provider-sourced CreditScore records.
has_co_applicant, co_app_*, co_applicant_* parties[] with an ApplicationPartyRole(role = co_applicant) ApplicationParty ApplicationPartyRole(role = co_applicant), plus the same identity, contact, employment, income, liability, housing expense, asset, credit score, 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. By itself it sets preference state; it only creates property/listing records when the flow captures or imports a sourced property option.
listing_url, building_or_sub_community, exact_price, ready_to_move_in, freehold_status, property_usage propertyListing + applicationPropertyMatch; intake-only fallback is propertyPreference.metadata.specific_property PropertyListing, ApplicationPropertyMatch, fallback ApplicationPropertyPreference.metadata sourceUrl, locationData or metadata.location_text, askingAmount, listingStatus, metadata.tenure_type, metadata.intended_use If this represents a sourced property option, create a lightweight Property, a PropertyListing, and an ApplicationPropertyMatch. If the form only needs to remember text the applicant typed before any sourced option exists, store lower_snake_case metadata on the preference.
property_options, locked_option_id, property_options_complete applicationPropertyMatches[]; UI-only fallback is application.metadata.forms.property_options ApplicationPropertyMatch, fallback Application.metadata status, propertyListingId, workflowStageId, metadata; fallback forms.property_options, forms.locked_option_id, forms.property_options_completed_at If options are actual sourced listings/candidates, model them as matches. If they are transient UI comparison state only, keep them in application metadata with lower_snake_case keys.
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[].identityProfile.documents[] Document + DocumentLink(identityProfileId) 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 same file is reused from another application, keep the same Document row and add a new DocumentLink to this party's IdentityProfile.
passport parties[].identityProfile.documents[] Document + DocumentLink(identityProfileId) documentType = passport, countryCode Country can be issuing country if captured. Link through DocumentLink(identityProfileId) for this application.
visa_page parties[].residencies[].documents[] Document + DocumentLink(residencyId) 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 an ApplicationPartyRole(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 mortgage search state, submitted, nullified, or withdrawn. Property search state belongs to ApplicationPropertyPreference.
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 capacity is assigned through ApplicationPartyRole.
ApplicationPartyRoleRepeatable legal or workflow capacity for an application party, such as primary applicant, co-applicant, borrower, guarantor, sponsor, or representative.
ApplicationPartyRelationshipRelationship between parties on the same application, such as spouse, partner, donor, dependent, or representative.
PersonStable human record, independent of any one application.
ContactMethodReusable email, phone, or WhatsApp contact value.
IdentityProfileThe identity facts submitted for one party on this application.
NationalityOne nationality or citizenship disclosed by an application party.
ResidencyOne legal or immigration residency disclosed by an application party.
TaxResidencyTax-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 institution, 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.
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.
RequestedLoanTermsThe requested amount, term, repayment type, rate preference, and borrowing purpose for the application.
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 kept simple for V2.1: person is the stable human, and application-submitted identity facts live under application_party.

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. nationality, residency, and tax_residency use that same country source.
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 party identity

Why
A human can exist in the platform without carrying every lending disclosure on the root person row.
Stores
person stores stable human identity. identity_profile, nationality, residency, and tax_residency store the facts submitted by an application party.
Sanity check
Add person-level identity profile tables later only if Sooner needs cross-application prefill or long-lived verification profiles.

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 root person simple. Application-specific nationality, residency, tax residency, and submitted identity facts live under application_party. Use related_contact for one-off contacts until they need canonical person identity.
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, 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. Property-search state lives on application_property_preference.property_search_status. 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, created_at, updated_at. Primary applicant, co-applicant, borrower, guarantor, and other legal/workflow capacities live in application_party_role; relationships live in application_party_relationship. Identity, nationality, residency, and tax-residency facts live in child records scoped by application_party_id. Consent lives in consent_event, not as one scalar party status.
identity_profile Identity profile disclosed for one party on one application. application_party. id, application_party_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 identity data submitted for the party in this application.
nationality Nationality/citizenship disclosed for a party on one application. application_party. id, application_party_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.
residency Legal or immigration-residence status disclosed for a party on one application. application_party. id, application_party_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.
tax_residency Tax-residency disclosure for a party on one application. application_party. id, application_party_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 intent and sourced options

Why
Gabriel's current flow captures search intent, while V1 migration has real sourced listing/candidate workflow that should not be hidden in metadata.
Stores
Preference stores budget, locations, property types, bedroom and bathroom choices, timeline, intended use, and search status. Property/listing/match records store sourced options when they exist.
Sanity check
Keep this narrow: add property, property listing, and application property match; do not add ownership, valuation, security-property, lender-property, or recommendation-engine tables.

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, extracted facts, 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.
property Stable property/location anchor used when Sooner has a sourced property option or migrated listing row. Root property record. id, location_data, address_text, property_type, metadata, created_at, updated_at, deleted_at. Keep this lightweight. It is not a property ownership, title, valuation, security, or lender collateral model.
property_listing Sourced property option/listing with unit-like listing facts merged into one simple table. property. id, property_id, source, source_reference, source_url, title, description, transaction_type, sale_type, listing_status, property_type, bedrooms, bathrooms, area_sqm, area_sqft, asking_amount, currency, furnishing, parking_type, available_from, location_data, agent_contact_metadata, source_metadata, metadata, timestamps. Use asking_amount instead of price_amount. location_data may hold listing-stated location when it differs from the property anchor.
application_property_match Application/preference-specific candidate state for one sourced property listing. application_property_preference + property_listing. id, application_property_preference_id, property_listing_id, status, workflow_stage_id, matched_at, archived_at, metadata, created_at, updated_at. V1 listing.status maps here because it is buyer/preference-specific, not global market availability.
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 application_party_id, identity_profile_id, nationality_id, residency_id, 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, mortgage_offer_response Displayed cost/fee snapshots, eligibility calculation, preliminary decision, formal decision, offer, and applicant response to an 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. mortgage_offer_response: mortgage_offer_id, application_party_id, response_status, response_reason, responded_at, metadata, timestamps. Treat all of these as backend-derived or applicant-response 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, extracted_fact, webhook, audit_log Communication, staged extracted data, 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. extracted_fact is staged extracted data from documents, provider payloads, imports, or conversations before projection into canonical V2.1 records. 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.
Person-level identity profile Long-lived person-level identity profile tables for the first V2.1 pass. identity_profile, nationality, residency, and tax_residency scoped directly by application_party_id. Keeps Gabriel's capture model simple. Add person-level identity profile tables later only when the product needs cross-application prefill or verification reuse.
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

property_listing_status:
  unknown
  active
  inactive
  reserved
  sold
  withdrawn
  expired

application_property_match_status:
  new
  shortlisted
  agent_contacted
  viewing_scheduled
  offer_made
  rejected
  archived

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
  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, application_party_role Unique active application_id + person_id on application_party. Only one active primary_applicant role per application through application_party_role. Prevents duplicate applicant rows and contradictory primary applicant capacity.
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

Keep the first pass simple: person is the stable human, application_party places that human inside one application, and identity/legal disclosures are child records of that party.

Stable person
  • person
  • contact_method
  • person_role
Application-party facts
  • application_party
  • identity_profile
  • nationality
  • residency
  • 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.

Lifecycle Model

Use this as the status ownership guide: not every journey state belongs on application.status.

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.

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 and sourced options application -> application_property_preference -> application_property_match -> property_listing -> property. Preference is search intent; match is applicant-specific candidate state; listing is the sourced option.
Documents and case work document -> document_link; document_requirement -> document_requirement_document -> document; mortgage_offer -> mortgage_case -> task.

Glossary

Entity Meaning
ApplicationThe main finance journey record. One started application, with statuses like mortgage search state, submitted, nullified, or withdrawn. Property search state belongs to ApplicationPropertyPreference.
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 capacity is assigned through ApplicationPartyRole.
ApplicationPartyRoleRepeatable legal or workflow capacity for an application party, such as primary applicant, co-applicant, borrower, guarantor, sponsor, or representative.
ApplicationPartyRelationshipRelationship between parties on the same application, such as spouse, partner, donor, dependent, or representative.
PersonStable human record, independent of any one application.
ContactMethodReusable email, phone, or WhatsApp contact value.
IdentityProfileThe identity facts submitted for one party on this application.
NationalityOne nationality or citizenship disclosed by an application party.
ResidencyOne legal or immigration residency disclosed by an application party.
TaxResidencyTax-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 institution, 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.
PropertyStable lightweight property/location anchor for sourced options. Not ownership, valuation, title, collateral, or lender-security modeling.
PropertyListingSourced property option/listing with unit-like facts merged into one table: source, title, type, bedroom/bathroom counts, area, asking amount, currency, location data, agent/source metadata, and availability status.
ApplicationPropertyMatchApplicant/application-specific candidate state linking a property preference to a property listing. V1 listing workflow status belongs here.
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.
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.
RequestedLoanTermsThe requested amount, term, repayment type, rate preference, and borrowing purpose for the application.
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.
Table And Field Glossary

Prisma modeling reference for the target V2.1 tables. Unless noted otherwise, id is the ULID primary key, created_at / updated_at are audit timestamps, deleted_at means soft deletion, and metadata stores owner-scoped JSON with lower_snake_case keys.

Table Fields Modeling idea
country
country_code
ISO 3166-1 alpha-2 key used by nationality, residency, address, institution, document, and market selectors.
alpha_3_code, numeric_code
Other ISO identifiers for interoperability.
source, source_version, last_synced_at
Reference-data provenance and refresh tracking.
is_supported_market, is_selectable, is_active, display_order
Controls market availability and dropdown behavior without code deploys.
Backend-owned reference table. Do not model countries or nationalities as Prisma enums.
country_label
country_code, locale
Composite label scope for one country and language/market locale.
country_name, short_country_name, nationality_label
Display labels for country and nationality selectors.
Lets UI render country/nationality options from backend reference data.
industry, industry_label
industry_code
Stable backend code such as hospitality, aviation, or banking_financial_services.
parent_industry_code
Optional hierarchy for grouped industry choices.
is_selectable, is_active, display_order
Controls which values appear in forms.
locale, label
Localized display label on industry_label.
Backend-owned employment/underwriting reference data, not an enum.
person
first_name, middle_name, last_name
Stable human name parts.
legal_full_name, preferred_name
Legal display name and preferred display name.
date_of_birth
Stable birth date when known; application-submitted identity facts still live under identity_profile.
The durable human. Do not put application-specific nationality, residency, tax residency, or documents here in this pass.
person_role
person_id
Person holding the role.
role
Broad lifecycle role: user, customer, or operator.
starts_at, ends_at
Role validity window.
Global product/person role. Application capacities belong in application_party_role.
platform_account
person_id
Local person represented by this product account.
iam_subject_id
Sooner-IAM subject identifier.
account_status
Lifecycle of platform access: invited, active, suspended, disabled, deleted.
profile_picture_url
Optional account avatar/display image.
Separates login/platform account from finance applicant/customer identity.
authorization_assignment, bundle_assignment
tenant_id
Sooner-IAM tenant/workspace scope.
subject_type, subject_id
IAM authorization subject represented locally.
platform_account_id
Convenience link when the subject is a human account.
authorization_assignment_id, bundle_name
Bundle assigned to that IAM authorization subject.
Local mirror of Sooner-IAM access; bundle definitions remain owned by IAM/catalog tooling.
contact_method, owner joins
type, value, normalized_value
Email/phone/WhatsApp value and normalized lookup value.
person_contact_method.person_id, institution_contact_method.institution_id
Owner of this use of the contact value.
label, is_primary, verified_at, verification_source
Owner-specific label, primary flag, and verification state.
Do not infer one global identity from a shared phone/email. Ownership and verification are scoped.
address, owner joins
line_1, line_2, city, region, postal_code, country_code
Structured address parts.
formatted_address
Display-ready address string.
address_type, is_primary, verified_at
Owner-specific address usage and verification fields on person_address or institution_address.
Addresses are structured data, not contact-method strings.
related_contact, application_related_contact
owner_person_id, related_person_id, institution_id
Canonical links when the contact is already known.
display_name, institution_name_text, job_title, notes
Contact-only details when a full person/institution row is not warranted.
relationship_type, is_primary
Relationship label and primary contact flag.
application_id, application_party_id, related_contact_id, purpose
Application usage and repeatable purposes such as verification, solicitor, lender, representative, or emergency.
For partner, donor, adviser, employer, solicitor, lender, or emergency contacts that are not necessarily application parties.
institution, classifications, identifiers, roles
name, legal_name, country_code
Reusable organization identity.
classification
Repeatable role/category: lender, broker, employer, insurer, solicitor, conveyancer, credit provider, etc.
identifier_type, identifier_value, issuer
Registration, licence, regulator, tax, company, or provider identifiers.
person_institution_role.relationship_type, title, department, started_on, ended_on
Person's relationship to an institution, such as employment or adviser role.
Use for reusable finance ecosystem entities, not IAM organizations or one-off human contacts.
imported_record_link
subject_type, subject_id
Canonical row this lineage belongs to.
source_system, source_table, source_record_id, source_url
Source dataset and row reference.
imported_at, metadata
Import time and source-specific trace details.
Use only for lineage to V1, Supabase, imported forms, or other source rows.
application, application_status_history
application_number, market, intake_channel
Readable application reference, market/country context, and origin channel.
status, mortgage_search_status
Overall journey lifecycle and simple mortgage-search intake answer.
submitted_at, nullified_at, withdrawn_at, completed_at
Important lifecycle timestamps.
from_status, to_status, reason, changed_by_person_id, changed_at
Append-only status transition history.
Application is the finance journey; do not overload it with every lender, document, or property status.
application_party, identity children
application_id, person_id
Places one person inside one application.
identity_profile.legal_full_name, first_name, middle_name, last_name, date_of_birth
Identity facts submitted for this application party.
declared_at, verified_at, verification_status
Disclosure and verification state.
nationality.country_code, status, is_primary
Repeatable nationality/citizenship disclosures.
residency.country_code, residency_status, residency_basis, started_on, ended_on
Repeatable legal or immigration residency disclosures.
tax_residency.country_code, tax_identifier, status, self_certified_at
Repeatable tax-residency disclosures.
Application-party records hold submitted applicant facts. Person-level identity reuse can be added later.
application_party_role, application_party_relationship
application_party_id, role, starts_at, ends_at
Repeatable legal/workflow capacity for a party.
from_application_party_id, to_application_party_id, relationship_type
Directional relationship between parties in the same application.
ownership_share_percent, financial_contribution_percent
Optional contribution/ownership-intent data when captured by the form.
Primary applicant and co-applicant are roles, not scalar columns on application_party.
application_institution_role
application_id, institution_id
Institution participating in one application.
role, status
Institutional capacity and lifecycle, such as lender, broker firm, solicitor firm, valuation provider, company borrower, or corporate guarantor.
reference, started_at, ended_at
Optional lender/provider/case reference and role validity window.
Use when an institution, not a person, has a formal role in the application.
consent_event, application_acknowledgement
application_id, application_party_id, person_id, platform_account_id
Scopes the consent or acknowledgement to the correct subject.
consent_type, status, captured_at, revoked_at
Consent category, current event state, grant time, and revocation time.
acknowledgement_type, version, confirmed_at, ip_address
Versioned user confirmation, such as cost-breakdown acknowledgement.
Use events for revocable/auditable consent rather than one boolean on application or party.
requested_loan_terms, selected_service, fee_quote
requested_amount, currency, term_months, repayment_type, rate_preference, purpose_of_borrowing
The finance ask from the applicant.
deposit_amount, estimated_property_value, loan_to_value_percent
Requested terms used for mortgage calculations.
service_code, selected_at, status
Customer-selected Sooner service/package state.
fee_tier, fee_percent, quoted_at, expires_at, metadata
Displayed fee quote snapshot.
Keep requested borrowing, selected Sooner services, and displayed fee quote separate.
employment, income_source
application_party_id, employment_type, employer_institution_id, employer_name_text
Party-scoped employment or business profile with normalized or text employer.
job_title, occupation_family_code, seniority_level, industry_code
Work classification and underwriting context.
started_on, tenure_months, business_years, verification_status
Stability and verification indicators.
income_source.type, amount, currency, cadence, is_variable, stability_months, commission_percent
Repeatable income components linked to party and optionally employment.
One applicant can have multiple employments and income sources; do not flatten income onto application.
asset, liability, housing_expense, funding_source
type, amount, amount_band, currency, institution_name, account_reference, verified_at
Asset details, including savings, deposits, investments, or gift funds.
asset_owner.application_party_id, ownership_percent
Who owns the asset and optional percentage.
lender_name, balance_amount, limit_amount, monthly_payment_amount, opened_on
Liability/debt exposure used for affordability.
liability_party.responsibility_type, responsibility_percent
Party responsibility for a liability.
housing_expense.monthly_amount, starts_on, ends_on
Rent or housing outgoing that affects affordability without being debt.
funding_source.country_code, verification_status, funding_source_asset.asset_id
Deposit/down-payment source and optional link to assets.
Financial facts are repeatable and party/application scoped; evidence attaches through documents.
credit_check, credit_report, credit_score, credit_report_tradeline
provider_institution_id, country_code, search_type, purpose
Credit provider/reference agency and check purpose.
consent_event_id, requested_at, responded_at, status
Consent and lifecycle of the credit check request.
report_reference, reported_at, document_id, raw_status
Returned credit report reference, timestamp, file, and provider-native status.
score, score_band, score_model, source
Score facts from provider or self-report.
account_type, balance_amount, limit_amount, monthly_payment_amount
Reported tradeline/account details.
AECB is a provider value/institution, not a model name.
application_property_preference
target_amount, currency
Target property/purchase amount and currency.
preferred_property_types, preferred_bedrooms, preferred_bathrooms
Selected array preferences.
purchase_timeline, intended_use, property_search_status
Purchase timing, residence/investment intent, and search state.
location_data
Location snapshots with PSL/address references, names, and address IDs.
is_location_agnostic, location_confidence, location_confidence_score
Location flexibility and source/quality confidence.
metadata.specific_property
Optional typed intake details when the applicant has a specific property in mind.
Application property intent. Sourced property options use property_listing and applicant-specific candidate state uses application_property_match.
property
location_data
Canonical location payload when the property/location anchor is known. Use PSL/address references rather than scalar area/community fields.
address_text
Human-readable address or unresolved source text when a structured location has not been resolved.
property_type
Broad property type when known, using the same enum vocabulary as preferences/listings.
metadata
Owner-scoped details that are not yet query-critical, with lower_snake_case keys.
Stable lightweight property/location anchor. Do not use this as title ownership, valuation, collateral/security-property, or lender-property model.
property_listing
property_id
Property/location anchor this sourced option belongs to.
source, source_reference, source_url
Listing source, upstream ID, and source URL when available. V1/third-party lineage can also be tracked in imported_record_link.
title, description
Display text from the listing/source.
transaction_type, sale_type, listing_status
Listing-side classification and market availability; not applicant workflow.
property_type, bedrooms, bathrooms, area_sqm, area_sqft
Unit-like physical facts merged into the listing for this pass.
asking_amount, currency
Advertised or stated asking amount. Avoid price_amount because price and amount duplicate the same meaning.
location_data
Listing-stated location payload when the source location is more specific or less verified than the linked property anchor.
agent_contact_metadata, source_metadata, metadata
Agent/source payload fragments and non-canonical details that should not become first-class columns yet.
Merges the proposed unit/listing concepts into one simple sourced option table. Add property_unit later only if Sooner needs independent unit identity or multiple listings for the same unit.
application_property_match
application_property_preference_id
The applicant's search intent this candidate is being considered against.
property_listing_id
The sourced property option being considered.
status
Applicant/preference-specific candidate progress, such as new, shortlisted, agent contacted, viewing scheduled, offer made, rejected, or archived.
workflow_stage_id
Optional CRM workflow stage when operator pipeline stages are configurable.
matched_at, archived_at
Candidate lifecycle timestamps.
metadata
Migration and workflow detail that is not yet canonical, with lower_snake_case keys.
V1 listing.status maps here because it describes Sooner's progress for one applicant/preference, not global listing availability.
document, document_link, document_requirement
document_type, country_code, local_document_type, document_side
Portable document taxonomy plus market-local detail such as Emirates ID.
period_start, period_end, provider, storage_key, mime_type
Statement/report coverage dates, provider, and storage metadata.
document_link.application_id, typed target FKs, link_type
What the document supports, such as party, identity, residency, income, asset, liability, funding, lender submission, or offer.
document_requirement.source, typed target FKs, status, required_by, due_at
Evidence request and its owner/source.
document_requirement_document.document_id
Many-to-many satisfaction link between requirements and documents.
Documents are files; links and requirements explain evidence meaning.
document_review, document_extraction, verification_event
status, reviewed_by_person_id, reviewed_at, notes
Human/operator document review state.
extracted_fields, confidence, provider
OCR/provider/AI extraction payload and confidence.
target_type, target_id, verification_status, verified_at
Provider or human verification of a fact or document.
Review, extraction, and verification are separate workflows with different provenance.
application_cost_estimate, affordability_assessment
total_upfront_amount, closing_cost_amount, sooner_savings_amount, calculated_at
Displayed cost estimate snapshot.
requested_loan_terms_id, income_snapshot, commitment_snapshot, expenditure_lines
Inputs used for affordability calculation.
dependants, stress_assumptions, policy_version, max_borrowing_amount, result
Assessment assumptions, policy version, output, and result.
Derived snapshots; do not treat display-only values as submitted form facts.
lender_submission, decision_in_principle, credit_decision, mortgage_offer, mortgage_offer_response
lender_institution_id, requested_loan_terms_id, stage, status, submitted_at
One lender-specific path/package.
approved_amount, offered_amount, rate_type, interest_rate_percent, term_months
DIP, decision, or offer financial terms.
expires_at, conditions, decline_reason, decision_at
Outcome lifecycle, conditions, and reason capture.
response_status, responded_at, response_reason
Applicant response to an offer.
One application can have multiple lender paths. Do not flatten lender outcomes into application.status.
mortgage_case, case parties, workflow, activity, task
case_number, lender_case_reference, status, assigned_to_person_id, target_completion_date, completed_at
Operational case file after application/lender path progresses.
mortgage_case_party, mortgage_case_institution_role
Customer-side people and institutions attached to the case.
workflow_track.kind, current_stage_id, workflow_stage.key, group, position
Operational state machine.
activity.type, body, performed_by_person_id, occurred_at
Case/application timeline event.
task.subject, priority, status, due_at, completed_at
Action item attached to a concrete finance subject.
Operational workflow records should attach to real subjects without becoming the finance aggregate.
conversation, message, extracted_fact, webhook, audit
conversation_application, conversation_application_party, conversation_mortgage_case, conversation_lender_submission
Typed conversation links for active relations.
message.sender, body, attachments, sent_at
Communication content and delivery metadata.
extracted_fact.subject, fact_path, fact_value, confidence, evidence
Staged extracted data from documents, provider payloads, imports, or conversations before projection into canonical V2.1 records.
webhook.event_type, payload, delivered_at, status
Integration event delivery state.
audit_log.actor, resource, resource_id, diff, created_at
Immutable audit event for business or system changes.
Infrastructure context around the finance model; use typed links for active records and generic subjects for immutable event payloads.
Enum Glossary

Every enum value should be stored in lower_snake_case. Countries, nationality labels, and industries are backend reference data, not Prisma enums.

Enum Values Meaning
platform_account_statusinvited, active, suspended, disabled, deletedLifecycle of Sooner product access.
person_role_kinduser, customer, operatorBroad person lifecycle roles, not application capacities.
contact_method_typeemail, phone, whatsappSupported reusable contact value types.
address_typeresidential, correspondence, previous_residential, registered_officeOwner-specific address usage.
related_contact_relationship_typespouse, civil_partner, partner, parent, child, sibling, business_partner, representative, otherHow a reusable related contact is connected to a person or application.
application_related_contact_purposeverification, emergency, employer, broker, solicitor, conveyancer, accountant, lender, representative, servicing, otherWhy a related contact is used on an application; repeatable per contact.
institution_classificationcompany, trust, lender, broker, employer, solicitor, conveyancer, valuation_provider, credit_data_provider, insurer, government_agency, service_provider, otherRepeatable classification for real-world institutions.
institution_identifier_typeregistration_number, regulator_reference, tax_identifier, licence_number, company_number, provider_reference, otherKind of official identifier stored for an institution.
application_statusdraft, intake_started, submitted, in_review, withdrawn, nullified, expired, completedOverall application lifecycle only; lender/document/offer statuses live elsewhere.
application_party_roleprimary_applicant, co_applicant, borrower, co_borrower, guarantor, sponsor, non_borrowing_occupier, representativeLegal/workflow capacity for a person in one application.
application_institution_rolecompany_borrower, trust, corporate_guarantor, institutional_donor, broker_firm, lender, solicitor_firm, conveyancer_firm, valuation_provider, insurer, otherInstitutional capacity on an application.
application_institution_role_statusactive, inactive, supersededLifecycle of an institution's application role.
application_party_relationship_typespouse, civil_partner, partner, parent, child, sibling, business_partner, guarantor_for, representative_for, otherRelationship between two parties in the same application.
property_search_statusnot_started, searching, identified, under_offer, not_requiredApplicant's property-search state on application_property_preference.
property_listing_statusunknown, active, inactive, reserved, sold, withdrawn, expiredMarket/source availability of a property listing. Do not use this for applicant workflow progress.
application_property_match_statusnew, shortlisted, agent_contacted, viewing_scheduled, offer_made, rejected, archivedApplicant/preference-specific progress for a sourced property option.
mortgage_search_statusnot_started, searching, found, not_required, unknownSimple intake answer for whether the applicant has a mortgage path.
lender_submission_stagedecision_in_principle, full_application, underwriting, offer, completionStage of one lender-specific path.
lender_submission_statusdraft, submitted, in_review, additional_information_required, approved, declined, withdrawn, expiredStatus of one lender submission package.
property_typeapartment, villa, townhouse, detached_house, semi_detached_house, terraced_house, maisonette, plot, commercial_unit, mixed_use, otherProperty preference type options.
property_intended_useprimary_residence, secondary_residence, buy_to_let, investment_property, otherApplicant's intended use for the target property.
employment_typesalaried, self_employed, business_owner, contractor, commission_only, unemployed, retired, otherKind of employment profile disclosed by a party.
income_source_typesalary, commission, bonus, self_employment, business_income, rental_income, investment_income, pension, otherRepeatable income component type.
asset_typesavings, current_account, fixed_deposit, investment, gift, sale_proceeds, otherAsset category used for deposit funds and affordability.
liability_typemortgage, personal_loan, vehicle_loan, credit_card, lease, overdraft, support_obligation, otherDebt or commitment category.
liability_responsibility_typesole, joint, guarantor, authorised_user, supplementary_cardholder, otherParty responsibility for a liability.
housing_expense_typerent, service_charge, maintenance, utilities, otherHousing outgoing that affects affordability but is not necessarily debt.
funding_source_typesavings, gift, sale_proceeds, investment_liquidation, inheritance, employer_support, family_support, loan, otherSource of deposit/down-payment or other funds.
document_typepassport, national_identity_document, residency_permit, bank_statement, credit_report, salary_certificate, employment_letter, trade_license, audited_financials, proof_of_address, otherPortable document category. Market-local document names go in local_document_type.
document_sidefront, back, full_document, pageWhich side/page of a document file was uploaded.
document_link_typeattached_to, evidence_for, generated_from, supports_decision, migrated_from, otherHow a document relates to a target record.
nationality_statusactive, renounced, historical, unknownStatus of a disclosed nationality/citizenship.
residency_statusresident, non_resident, former_resident, unknownCurrent legal/immigration residency state for a country.
residency_basiscitizen, permanent, temporary, visa, otherBasis for residency.
tax_residency_statusactive, historical, unknownStatus of a tax-residency disclosure.
consent_typecredit_check, document_processing, marketing_contact, service_contact, lender_sharing, data_reuseConsent category.
consent_statusgranted, revoked, declined, expiredConsent event state.
verification_statusnot_started, pending, verified, rejected, expired, manual_review_requiredReusable verification lifecycle for facts or evidence.
document_requirement_statusrequired, requested, received, satisfied, waived, expiredLifecycle of an evidence requirement.
document_requirement_sourcesooner, lender, solicitor, conveyancer, credit_provider, valuation_provider, regulation, customerWho or what generated an evidence requirement.
document_review_statuspending, approved, rejected, needs_more_information, manual_review_requiredHuman/operator review state for submitted documents.
credit_report_statusrequested, available, unavailable, expired, failedReturned credit report availability.
credit_check_search_typesoft, hardSoft or hard credit search.
credit_check_purposedecision_in_principle, full_application, affordability, verificationWhy the credit check was requested.
affordability_resulteligible, referred, ineligible, insufficient_dataAffordability assessment output.
decision_in_principle_statusrequested, issued, declined, expired, withdrawnDIP/AIP lifecycle.
credit_decision_statuspending, approved, approved_with_conditions, declined, withdrawn, expiredUnderwriting or lender credit decision lifecycle.
mortgage_offer_statusissued, accepted, revised, withdrawn, expiredFormal offer lifecycle.
rate_typefixed, variable, tracker, mixedRate structure on requested or offered mortgage terms.
repayment_typerepayment, interest_only, part_and_partMortgage repayment structure.
workflow_kindintake, prequalification, application, documents, underwriting, offer, completion, servicingOperational workflow track kind.
task_statusopen, in_progress, blocked, completed, cancelledTask lifecycle.

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.
Institution / provider value 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 user-facing label, but keep the schema provider-neutral with Institution or a provider value.
  • 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. Use Property, PropertyListing, and ApplicationPropertyMatch only for sourced property options and applicant-specific candidate workflow; do not introduce valuation, ownership, security-property, or lender-property models in this pass.
  • 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.

Sooner Prisma V2.1 Full Rewrite Plan

This plan uses the current Sooner Prisma workspace as reference material for a full schema rewrite. surat is only an internal workspace codename, not a separate product or schema vocabulary. This tab is not a data-preserving migration plan and it is not the target entity catalog; it is the implementation bridge from the current paused V2 Prisma shape into the target V2.1 entities described in the first tab.

The target truth remains Sooner V2.1 entities. The current paused V2 schema can be fully replaced. Data loss from the paused V2 schema is acceptable, so the implementation should favor a clean V2.1 baseline over rename/copy/backfill migrations.

Source: current Sooner Prisma Target: V2.1 entities Full schema rewrite Paused V2 data loss accepted Remove ai.prisma Remove/recreate schema.prisma config Keep core and crm inputs Lower snake case enum values

File Scope

Current Sooner file Rewrite decision Reasoning Required action
packages/prisma/prisma/schema.prisma Remove/recreate the current root config; do not treat it as a domain model source. The file currently only defines Prisma generator/datasource and includes schemas = ["core", "crm", "ai"]. V2.1 should not keep the current config unchanged because ai is being removed from this pass. Remove the current root schema.prisma shape from the migration and recreate a fresh V2.1 root config. If Prisma still requires a root schema.prisma, keep it as config-only; remove the current ai schema entry and any stale assumptions from the paused V2 setup.
packages/prisma/prisma/models/core.prisma Keep as the main reference input and rewrite into the V2.1 core/finance shape. Most durable concepts are here: person, institution-like records, contact identity, property preference, application, documents, messages, webhook, and audit. Rewrite the file around the V2.1 target entities. Do not optimize for preserving current paused V2 rows or ULIDs.
packages/prisma/prisma/models/crm.prisma Keep as the workflow reference input. The current CRM file has useful operational concepts, but the lead naming is not the target domain language. Completely rework CRM as an extension over core V2.1 subjects. Remove Lead; keep task, activity, workflow stage/track, labels, and custom fields only where they attach to real target subjects.
packages/prisma/prisma/models/ai.prisma Remove from the V2.1 rewrite scope. The V2.1 finance data model should not carry the paused assistant schema forward. These tables also depend on current Lead, Person, Property, and Conversation relationships that are being renamed or re-scoped. Delete ai.prisma for this pass, remove ai from the datasource schema list, and remove assistant relations from core.prisma and crm.prisma. Reintroduce AI later as a fresh bounded context if the product needs it.
packages/prisma/prisma/migrations/20260508000000_init_sooner_v2/migration.sql Discard for the V2.1 baseline. The migration represents the paused V2 schema, including tables and enum values that no longer match the target. Create a clean V2.1 baseline migration instead of carrying forward the paused V2 migration history.

Core Model Migration Map

Current Sooner model Action Target V2.1 model Reasoning
Person Keep and slim down. person Person remains the stable human record. Move market-specific identity, residency, nationality, and contact details into child tables.
Person.emiratesId, Person.residency Remove scalar columns and split. identity_profile, nationality, residency, tax_residency These facts are repeatable, market-aware, and sometimes application-sensitive. They should not be single UAE-specific scalar fields on person.
IdentityLink Rename and split ownership. contact, person_contact_method, institution_contact_method, imported_record_link Email, phone, and WhatsApp values can be shared or reused. The contact value should be separate from the person or institution that claims it.
Organization, OrganizationType Rename and generalize. institution, institution_identifier, institution_contact_method Institution is clearer for lenders, employers, credit providers, brokers, government bodies, and service providers. Organization was too broad and easy to confuse with IAM organizations.
PersonOrganization, PersonRole.organizationId Split role meaning. person_institution_role, person_role A person’s platform role is not the same as their relationship to a lender, broker, employer, or provider institution.
PropertyPreference Rename and move from person scope to application scope. application_property_preference Gabriel’s form captures application-time property search intent. Preferred locations, property types, bedroom values, bathroom values, and search status belong to the application.
Property, PropertyListing, PropertyMedia, PropertyValuation, PropertyMatch Rewrite into the smaller V2.1 property option model. property, property_listing, application_property_match Keep property as a lightweight property/location anchor. Merge unit-like fields into property_listing instead of adding property_unit now. Use application_property_match for applicant/preference-specific candidate status. Do not carry forward property media, valuation, ownership, security-property, lender-property, or marketplace-sharing assumptions unless a product flow explicitly needs them.
Deal Remove or defer. mortgage_case only if an operational case file is needed later. Current capture starts with an application and applicant facts. A deal/property transaction table is not needed for Gabriel’s field catalog handoff.
FinanceApplication Rename and split. application, application_party, application_party_role, application_status_history The current model assumes one person per finance application. The target allows primary applicant, co-applicant, guarantor, sponsor, representative, and future roles.
FinanceApplication.monthlyIncome, FinanceApplication.employerName Remove scalar columns and split. employment, income_source Employment and income are repeatable party facts. They need their own records, currencies, dates, verification state, and document links.
FinanceApplicationProduct Split by meaning. selected_service, requested_loan_terms A selected Sooner service/package is not the same fact as requested borrowing amount, term, repayment type, or loan purpose.
LenderOffer Split lender process stages. lender_submission, decision_in_principle, credit_decision, mortgage_offer DIP/AIP, underwriting decisions, lender submissions, and formal offers are different lifecycle records and should not be compressed into one offer table.
Commission Remove or defer. No first-pass V2.1 target. Commission is commercial operations data, not part of Gabriel’s application field capture or the first finance identity model.
Document Keep and split subject links. document, document_link The file is one thing; the facts it supports are separate. Replace polymorphic entity_type/entity_id with explicit document links.
DocumentRequirement Keep and re-anchor. document_requirement Requirements should attach to application, party, employment, income, liability, funding source, identity, or residency contexts as needed.
DocumentVerification Split review, extraction, and verification events. document_review, document_extraction, verification_event Human review state, extracted OCR/provider payload, and verification events are different facts with different provenance.
MessageChannel, Conversation, Message, MessageTemplate Keep only conversation/message as the V2.1 target; remove channel/template tables from this rewrite unless product scope explicitly requires them. conversation, message Messaging history remains useful, but this schema pass should not carry provider/channel configuration or template approval tables forward by default. Conversations and messages also must not depend on Lead or assistant tables removed in this pass.
Webhook, AuditLog Keep and align subject naming. webhook, audit_log These are infrastructure records. Keep them, but make actor/resource names match IAM and V2.1 entity names.
Missing from current core Add new. asset, liability, housing_expense, funding_source, credit_check, credit_report, credit_score These are standard lending facts and should be first-class instead of buried in application metadata.

V1 Listing Migration Shape

V1 listing rows should not be collapsed into generic metadata, but they also should not reintroduce a full marketplace or property-security model. Use the smallest first-class shape that preserves sourced property options and applicant-specific workflow.

V1 concept V2.1 target Reasoning
listing row identity and imported source IDs property_listing plus imported_record_link Keep a first-class sourced option and trace the original V1 row or upstream inventory reference without copying Rematch inventory naming.
listing.status application_property_match.status V1 listing status is scoped to one buyer/preference workflow. It is not global market availability for the listing.
Listing availability or source-market state property_listing.listing_status Use this only for market/source availability such as active, inactive, reserved, sold, withdrawn, or expired.
preference.area_community or source location text application_property_preference.location_data when resolved; otherwise owner metadata Area/community is location data. Do not create scalar area, community, sub_community, or building_or_sub_community columns. Preserve unresolved text with low/unknown confidence.
Unit-like listing fields property_listing Merge unit-like fields into the listing for this pass: property type, bedrooms, bathrooms, area, asking amount, currency, furnishing, parking, and source metadata. Add property_unit later only if independent unit identity becomes necessary.
Operator stages around a listing candidate application_property_match.workflow_stage_id or CRM workflow linked to the match Pipeline progress is operational workflow around a candidate, not a property/listing fact.

CRM Impact Scope

Rewrite Boundary

The CRM schema should be completely reworked as an operational extension of core Sooner entities. CRM should not own a parallel customer, applicant, property, deal, or application aggregate. Core owns durable business records such as person, application, application_party, application_property_preference, document, lender_submission, and conversation. CRM can add workflow state, stages, assignment, activity, tasks, labels, and custom operational fields around those records.

Lead should not survive the V2.1 rewrite. Do not rename it to another umbrella aggregate such as inquiry, workflow case, or customer case. If workflow is needed, model the workflow itself and attach it to the core subject being worked.

Smoke Data Signal

The smoke database supports this direction. crm.lead has 15 rows, every lead points to a core.person, all 15 lead people already have person_role = customer, no lead points to a deal, and in inspected samples lead.id equals person.id. That makes lead look like a V1 buyer wrapper over person, not a durable V2.1 domain object.

The useful signal is workflow, not lead ownership: lead_track has 45 rows, exactly three tracks per lead, and lead_activity has 28 migrated note rows. Preserve the concepts of workflow and activity by re-anchoring them to V2.1 core subjects.

CRM rewrite rule What it means
No surviving lead aggregate Remove Lead, lead_id, LeadStatus, and lead-owned lifecycle assumptions. Use application, person, and application_party as durable anchors.
Workflow attaches to core Customer workflow attaches to person or person_role; property-search workflow attaches to application_property_preference for overall search state or application_property_match for a sourced option; mortgage workflow attaches to application or lender_submission.
Typed links first For operational records such as workflow, task, and activity, prefer typed foreign keys or typed join tables over broad subject_type / subject_id. Generic subject references are acceptable for audit, webhook, or immutable event payloads, not as a shortcut around CRM modeling.
CRM depends on core, not the reverse CRM tables may reference core tables. Core finance entities should not need CRM tables to explain their identity, lifecycle, document state, lender state, or customer status.

CRM Schema Rewrite Map

Current Sooner model Action Target V2.1 model Reasoning
Lead Remove completely. No replacement table. Lead is a V1 buyer wrapper over person/preference/workflow in the smoke data. V2.1 should not keep a parallel CRM aggregate for identity or application state.
LeadTrack Rebuild as subject-owned workflow. workflow_track or workflow_instance, with typed subject links. The track concept is useful, but it must attach to the real subject being worked: person/customer lifecycle, application, property preference, document requirement, or lender submission.
LeadActivity Rebuild as subject-owned activity. activity Activity should be append-only operator history linked to concrete V2.1 subjects. Current migrated notes are useful, but lead_id should not survive.
Task Keep concept, rebuild ownership. task The task concept is useful, but task ownership should use typed V2.1 subject links. Remove special lead_id and paused deal_id anchoring.
PipelineStage Rebuild as workflow configuration. workflow_stage Workflow stage is operational configuration for CRM. It should not encode lead lifecycle as the product lifecycle.
LabelDefinition, CustomFieldDefinition Keep concept only if needed, rebuild entity targets. Same names, with renamed entity/subject values. These are CRM extension configuration tables. Their enum values should reference V2.1 subjects, not lead, deal, or property_match.

Enum Rewrite

Current enum family Rewrite action Target direction
Role and institution enums Rename values and split platform roles from institution relationships. person_role_kind, institution_kind, person_institution_role_kind, application_institution_role_kind.
IdentityLinkType Rename around contact method values and imported record lineage. contact_type for email, phone, whatsapp; separate provider/source identifiers into imported_record_link or provider-specific reference fields.
Residency and country enums Remove market-specific values from generic enums. Use backend reference tables for countries/nationalities and enum values like resident, non_resident, unknown where enum state is still needed.
Property enums Keep only preference-level enums needed by the forms flow. Keep property_type, property_purpose, location_confidence; remove/defer listing, media, match, valuation, and deal enums unless product scope brings those tables back.
Finance application/product/offer enums Rename around application lifecycle and lender process stages. application_status, selected_service_type, loan_purpose, lender_submission_status, decision_status, mortgage_offer_status.
Document verification enums Split actor/status meanings across review, extraction, and verification event concepts. document_review_status, document_extraction_status, verification_event_type, verification_event_source.
CRM lead/pipeline enums Remove lead-owned enums and rebuild only the workflow enums that remain useful. LeadSource becomes application.intake_channel or application metadata. LeadStatus is removed. LeadTrackKind is replaced by target-subject workflow values. Keep/rebuild workflow_track_kind, workflow_stage_group, activity_type, and task_status only where CRM workflow needs them.
AI enums Remove in this pass. No target V2.1 enum. If assistant functionality returns later, define fresh enums in a new AI bounded context after finance entities stabilize.
Hyphenated stored values Convert to lower snake case. lender-contact becomes lender_contact, in-review becomes in_review, property-finder becomes property_finder, and so on.

Rewrite Phases

Phase Work Reasoning
1. Freeze the current target Use the V2.1 entities tab as the schema target and treat the paused V2 schema as reference material only. No data-preserving migration is required. Data loss from paused V2 is acceptable, so the implementation can start from a clean baseline.
2. Remove the AI schema from this pass Delete models/ai.prisma, remove ai from root datasource schemas, remove assistant relations from current core/crm models, and drop generated AI migration output. The assistant schema is not needed for the V2.1 finance entity rewrite and would force stale lead/property relationships into the new model.
3. Rebuild identity and IAM-adjacent records Rewrite Person, split contacts, add IAM subject/account references only where Sooner needs local finance joins, and replace organization naming with institution naming. This creates stable anchors before application, documents, messaging, and workflow rows are reattached.
4. Rebuild application and party records Rename/split FinanceApplication into application, parties, party roles, status history, selected services, requested loan terms, property preference, and financial profile child tables. This removes the current single-person/flat-income limitation and aligns with standard lending data modeling.
5. Rebuild documents and verification Replace polymorphic document ownership with document links, re-anchor requirements, and split document verification into review, extraction, and verification events. This lets one file support identity, residency, income, employment, liability, or funding-source facts without hard-coding one entity field.
6. Completely rework CRM as an extension layer Remove Lead, remove lead-owned IDs/statuses, and rebuild CRM around workflow, task, activity, stage, label, and custom-field records attached to V2.1 core subjects. CRM should help operators manage work around core entities. It should not define identity, application, property preference, lender, document, or customer lifecycle as a parallel aggregate.
7. Normalize enums and reference data Convert stored enum values to lower snake case and move countries/nationalities/industries into backend reference data where static Prisma enums are too rigid. This avoids country/provider-specific schema names and prevents migrations for ordinary reference-data changes.
8. Validate and generate Run Prisma validation, generated client checks, API extraction/generation, typecheck, tests, and format checks. The rewrite changes model names, relations, enums, routes, and SDK contracts. Validation must cover both database shape and generated API/client drift.

Implementation Rules

  • Use the V2.1 entities tab as the target source of truth; use this tab only to guide the full rewrite from the current Sooner Prisma schema.
  • Keep core.prisma and crm.prisma as source inputs, but rewrite their contents to the target model instead of carrying paused V2 names forward.
  • Remove ai.prisma from this pass and remove all assistant relations before validating Prisma.
  • Remove/recreate the current root schema.prisma config. If a root file remains, it should be config-only and should not include the removed ai schema.
  • Do not build rename/copy/backfill migrations for paused V2 data. Data loss is acceptable for this rewrite.
  • Completely rework crm.prisma as a CRM extension over core V2.1 entities. Do not keep Lead or any lead-owned aggregate as the center of CRM.
  • Use metadata JSON only for owner-specific derived/display-only or optional UI payloads, with lower snake case JSON keys.
  • Prefer typed relations for active finance data. Use generic subject references only for audit, immutable event payloads, or carefully bounded operational workflow records.

Verification Checklist

Check What must be true
No stale AI schema No Assistant* models, no AI enums, no @@schema("ai"), and no ai entry in root datasource schemas for the V2.1 pass.
No stale lead ownership Finance entities, messages, tasks, activities, documents, and extracted facts do not require lead_id as their durable owner.
No single-applicant application application owns many application_party rows, and party roles come through application_party_role.
No market-specific scalar identity There is no person.emirates_id or residency_status.uae_resident style modeling in generic person tables/enums.
No polymorphic document ownership for active finance records Documents use explicit document_link rows to say which fact or requirement the document supports.
Enum storage is normalized Stored enum values are lower snake case across the migrated Prisma schema.
Field catalog still maps cleanly Every Gabriel field catalog item lands on a target entity, an owner metadata JSON column, or a documented deferred scope.

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 keep Lead as a V2.1 center. A person can become user, applicant, and customer through roles and application state; CRM workflow should attach to core subjects.
  • 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 if needed and Application for the financing journey. Do not keep Lead as the CRM or finance aggregate.
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 residency for application-time legal/immigration residency and 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, or a sourced property option/listing? Use application_property_preference.metadata.specific_property for intake-only text. Use property, property_listing, and application_property_match when the row is a sourced option or migrated listing candidate.
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 plus an application_party_role(role = co_applicant) row as durable state. Keep the boolean as intake/UI gate only.
has_specific_property, listing_url, exact_price, freehold_status Is this search intent, optional typed intake detail, or an actual sourced option? Use application_property_preference.property_search_status for the search state. Use property_listing plus application_property_match for actual sourced options; use preference metadata only for intake text that is not yet a sourced option.
property_options, locked_option_id, property_options_complete Is this transient UI option state or real candidate listings? Use application_property_match for real candidates. Treat purely transient form comparison state as application.metadata.forms values with lower snake case keys.
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 or listing, is it a sourced option/candidate? If yes, use PropertyListing plus ApplicationPropertyMatch. If it is only typed intake text, keep it under the owner metadata with lower snake case keys. Still avoid valuation, ownership, title, security-property, and lender-property models in this pass.
  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 Stable human profile, platform/person lifecycle roles, contact-only/person promotion state. Application-specific income, co-applicant relationship, document slots, submitted identity facts, nationality, residency, tax residency, 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, identity profile, nationality/residency/tax-residency disclosures 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. Sourced property options or applicant-specific candidate workflow; use Property, PropertyListing, and ApplicationPropertyMatch for those. Still avoid ownership, valuation, title, security-property, lender-property, 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.”
  • ApplicationPartyRole: “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-specific facts 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 propertyPreference.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, institution, 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, ApplicationPartyRole 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 and assign co_applicant through ApplicationPartyRole. 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 property modeling simple. Use ApplicationPropertyPreference for target amount, selected locations, property type choices, bedroom/bathroom count choices, purchase timeline, intended use, and property-search status. Use Property as a lightweight property/location anchor, PropertyListing as the sourced option/listing with unit-like facts merged into the listing, and ApplicationPropertyMatch for applicant-specific candidate workflow. Do not add PropertyUnit, valuation, ownership, security-property, lender-property, or recommendation-engine tables in this pass unless a concrete product flow requires them.

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 property ownership, valuation, title, security-property, lender-property, or recommendation-engine models from Gabriel's current forms. For sourced property options or migrated listing candidates, keep the model simple: Property, PropertyListing, and ApplicationPropertyMatch. For intake-only typed detail, use metadata on ApplicationPropertyPreference with lower_snake_case keys.

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.

Keep the first-pass identity model simple. Use Person for the stable human and ApplicationParty for that human's participation in one application. Store submitted identity facts in IdentityProfile, Nationality, Residency, and TaxResidency records scoped by applicationPartyId. Do not add person-level identity profile tables until Sooner needs cross-application prefill or long-lived verification profiles. 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, ApplicationPartyRole, 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 plus ApplicationPartyRole(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 repeatable Nationality, Residency, and TaxResidency records scoped by applicationPartyId.
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.