Neela — Data Map Overview
This is the canonical reference for where every exchange field lives. Each row maps field → source hub → storage → sensitivity → consuming hubs. The hard rule: NPI (SSN, DOB, full street, bank account) lives only in profiles_sensitive under user-only RLS or in encrypted closing_info — never in localStorage. Demo identity is Matt Belcher · 456 Sunset Blvd, Los Angeles CA · $850,000 (synthetic only).
Onboarding & KYC
Identity captured once at account creation; pre-fills every hub. NPI is isolated in profiles_sensitive.
| Field | Source | Storage | Sensitivity | Consuming hub(s) |
|---|---|---|---|---|
| User input | Supabase Auth profiles.email | PII | All hubs | |
| password | User input | Auth bcrypt | NPI | Auth only |
| first_name · last_name | User input | profiles.first_name / last_name | PII | All hubs (autofill) |
| phone | User input | profiles.phone | PII | Sales Purchase |
| address_city · address_state | User input | profiles.address_city / address_state | PII | Sales (W-9) |
| date_of_birth | KYC (Persona) | SENS profiles_sensitive.date_of_birth | NPI | KYC verify only |
| ssn_last_four · ssn_hash · ssn_encrypted | User input + KYC | SENS profiles_sensitive | NPI | IRS filing |
| address_street · address_zip | User input / KYC | SENS profiles_sensitive.address_street / address_zip | NPI | Sales (W-9) IRS |
| tin_type · ein | User input | SENS profiles_sensitive.tin_type / ein | NPI | IRS form |
| documentFront · documentBack · selfie | User upload | OBJ storage/kyc/ | NPI | KYC verify |
| is_identity_verified · is_kyc_complete | Persona webhook | profiles.is_identity_verified / is_kyc_complete | PUB | All hubs (gating) |
Sales Hub (Phase 1)
Densest collection stage. Property + price + exchanger + sale-side documents. Seeds the 45/180-day deadline clock.
| Field | Source | Storage | Sensitivity | Consuming hub(s) |
|---|---|---|---|---|
| exchange_type | User radio | JSONB wizard_state.taxpayer_info.exchangeType | TX | All hubs (flow gate) |
| sellingPrice / salesPrice | User / Neela extract | DB exchanges.relinquished_value | TX | Escrow IRS line 14 |
| originalPurchasePrice · mortgageBalance | User input | JSONB calculator_results.input | TX | IRS line 13 / Part III |
| salesPurchaseAgreement · preliminaryTitleReport (files) | User upload | OBJ storage/docs/ + documents row | TX | Identification (extraction) |
| exchangerPhaseOne.{firstName,middleName,lastName} | Autofill from profiles | JSONB wizard_state.taxpayer_info | PII | ID Purchase Disburse IRS |
| exchangerPhaseOne.taxIdNumber (SSN/EIN) | Server-side, filing time | SENS profiles_sensitive.ssn_encrypted | NPI | W-9 only |
| buyer.names · closingCompanySale.* | User / extracted | JSONB wizard_state.relinquished_property | PII | Notice of Assignment |
| relinquished.addressOrLegalDescription | User / extracted | JSONB wizard_state.relinquished_property | TX | ID (auto) IRS line 1 |
| relinquished.closingDate | User input | DB exchanges.sale_close_date | TX | All hubs (deadline clock) |
| identification_deadline · exchange_deadline | Postgres trigger (+45d / +180d) | DB exchanges.identification_deadline / exchange_deadline | TX | ID Purchase Disburse |
| exchange_agreement_signed · w9_signed (+ signature) | User signature (DocuSign) | DB exchange_hub_status.document_signatures + documents envelope | NPI | Audit IRS file |
| sales_hub_submitted (flag) | System on submit | DB exchange_hub_status.hub_submissions.sales | PUB | Hub-access gating |
Escrow & Identification Hubs
Funds land in the segregated qualified-escrow account; the 45-day ID list is built and locked.
Escrow Identification
| Field | Source | Storage | Sensitivity | Consuming hub(s) |
|---|---|---|---|---|
| syncedBalance / extractedBalance | Bank sync / Neela extract | DB exchanges.exchange_funds_held | TX | Disburse (balance) IRS line 14 |
| fundedAt | System (bank webhook) | DB exchanges.exchange_funds_received_at | TX | Audit IRS |
| Harborline account · routing | Server constant | SENS | NPI | Escrow display |
| identificationForm.idRule | User selection | JSONB wizard_state.identified_properties | TX | IRS Validation |
| identifiedProperties[].{address,estimatedFMV,allocationAmount,type} | DST picker / like-kind form | JSONB wizard_state.identified_properties[] | TX | Purchase (autofill) IRS line 12 |
| hudOneUploaded (HUD-1 file) · identFormSigned | User upload / signature | OBJ storage/docs/ + exchange_hub_status.document_signatures | NPI | IRS Audit |
| identification_hub_submitted (+ date) | System on submit | DB exchange_hub_status.hub_submissions.identification | PUB | IRS line 5 Gating |
Purchase & Disbursement Hubs
Replacement property closes; the QI authorizes the outbound wire. The most security-critical signature in the system.
Purchase Disbursement
| Field | Source | Storage | Sensitivity | Consuming hub(s) |
|---|---|---|---|---|
| seller.names · closingCompanyPurchase.* | User / extracted | JSONB wizard_state.replacement_property | PII | Disburse (wire default) |
| replacement.addressOrLegalDescription | User / extracted | JSONB wizard_state.replacement_property | TX | IRS line 2 Disburse |
| replacement.closingDate | User input | DB exchanges.replacement_value + JSONB ref | TX | IRS line 6 Deadline check |
| Purchase Agreement · HUD-1 (files) · Assignment signed | User upload / signature | OBJ storage/docs/ + documents | NPI | IRS Audit |
| disbursementForm.amount | User input | JSONB wizard_state.closing_info | TX | Wire IRS |
| wireRecipientBankABA · wireRecipientBankAccount | User input | JSONB wizard_state.closing_info | NPI | Wire |
| disbursement_auth_signed (+ signature) | User signature | DB exchange_hub_status.document_signatures.disbursement_authorization | NPI | Money movement Audit |
| wire_confirmation_verified (file) | Upload after wire executes | OBJ storage/docs/ + exchange_hub_status.document_signatures.wire_confirmation | TX | Audit |
IRS Reporting — Form 8824 assembly
No dedicated hub; assembled at preview time via mapToForm8824(), drawing read-only from every hub. SSN read server-side at filing time only.
| Line | Source field | Sensitivity | Originating hub |
|---|---|---|---|
| 8824 · 1 | relinquished.addressOrLegalDescription | TX | Sales |
| 8824 · 2 | replacement.addressOrLegalDescription | TX | Purchase |
| 8824 · 4 | relinquished.closingDate | TX | Sales |
| 8824 · 5 | identification_hub_submitted_date | PUB | Identification |
| 8824 · 6 | replacement.closingDate | TX | Purchase |
| 8824 · 12–19 | calculator_results.results.{boot,recognizedGain,deferredGain,…} | TX | Sales (calculator) |
| filer SSN/EIN | profiles_sensitive.ssn_encrypted | NPI | KYC |
Target storage layout
Five physical stores. Tenant isolation is enforced by Supabase RLS scoped via exchange_participants; NPI is further isolated in profiles_sensitive with user-only RLS the admin client cannot read. Every write produces an audit_log row forwarded to Vectr's log-api Durable Object.
Supabase Postgres
Tenant-scoped RLS · source of truth
profiles name · phone · city/state (PII)
exchanges price · deadlines · funds_held (TX)
exchange_participants cross-tenant access
exchange_hub_status submissions · signatures
documents file metadata
audit_log append-only (7-day cache)
profiles_sensitive
NPI · user-only RLS · AES at rest
ssn_last_four display
ssn_hash match
ssn_encrypted AES full
date_of_birth NPI
address_street · address_zip full street = NPI
tin_type · ein entity exchangers
wizard_state (JSONB)
Scoped per (exchange, scope)
taxpayer_info identifiers (NOT SSN)
relinquished_property + closing-co · buyer
replacement_property + closing-co · seller
identified_properties[] 45-day list
closing_info wire · account ENCRYPTED
Supabase Storage
Object store · signed-URL access
kyc/ ID front/back · selfie
docs/ sale + purchase agreements · HUD-1
signatures/ DocuSign signed PDFs
irs/ Form 8824 PDFs
External (via Vectr)
No direct calls from Pages Functions
Persona KYC parsed ID
DocuSign 4 envelopes
Plaid bank verification
Zillow / RentSpree FMV enrichment
Google Places address autocomplete
localStorage
Transient cache only — target state
hub_status_${exchangeId} read-through cache of exchange_hub_status
Hydrated by useExchangeHubStatus; reconciled from the API. No business data writes.
Data classification rule
NPI
profiles_sensitive (user-only RLS) or storage/ with signed URL. AES at rest. Audit on every read. NEVER in localStorage; NEVER in audit_log details.
PII
profiles or wizard_state JSONB. Encrypted at rest. Audit on write.
TX
exchanges + wizard_state. At rest. Audit on write.
PUB
Anywhere; cacheable. Audit on write only.
ADR-009 — DB as source of truth
The app today carries 43 distinct localStorage keys holding business state. The migration target is one key — hub_status_${exchangeId} — and it's a read-through cache only. Every other key becomes a DB column, a wizard_state JSONB scope, or a Storage object. Tier-1 rows (full SSN, signatures in localStorage) are GLBA / SOC 2 exposures and migrate first.
Legacy keys today
43
business state in localStorage
Target keys
1
read-through cache only
Tier-1 (NPI exposure)
3
SSN · signatures · KYC files
Source of truth
Postgres
+ RLS + audit_log
Migration tracks — localStorage shape → DB-authoritative target
Ordered by compliance tier. Each track lists the legacy keys it deletes.
Full SSN / TIN out of localStorage
Tier 1 · NPI
profiles_sensitive.ssn_encrypted only. UI shows last-4 + masked. W-9 PDF reads the full value server-side at generation time. wizard_state.taxpayer_info keeps tin_type + last_four — never the full string.
sale_wizard_v1_*_phase1.taxIdNumber
Signatures & signer data out of localStorage
Tier 1
exchange_hub_status.document_signatures + DocuSign envelope ID in documents. Signature images live in storage/signatures/.
exchange_agreement_signed +_signature/_signed_date/_signed_by
w9_signed +_signature
disbursement_auth_signed +_signature
identification_form_signed
KYC ID images get a persistence path
Tier 1
storage/kyc/ via the Persona Vectr workflow. Today the ID front/back + selfie live only in component state until the onComplete callback.
documentFront · documentBack · selfie (component state)
Hub-submission flags consolidate
Tier 2
exchange_hub_status.hub_submissions (DB, source of truth) + ephemeral hub_status_${exchangeId} cache. Drop the per-hub mirrors and the acquisition_hub_submitted alias.
sales_hub_submitted +_date
identification_hub_submitted +_date
purchase_hub_submitted +_date
acquisition_hub_submitted (alias)
disbursement_hub_submitted +_date
escrow_hub_submitted +_date
Escrow balance · sale price · close date → DB columns
Tier 2
exchanges.exchange_funds_held, exchanges.relinquished_value, exchanges.sale_close_date are canonical. The 45/180-day deadlines derive from sale_close_date via the calculate_exchange_deadlines() trigger — never a duplicate.
escrow_data_${userId} { principalAmount, fundedAt }
calculator_results
property_sale_date
wire_confirmation_verified +_date
identification_hud1_uploaded
Wizard scopes & edit tickets go server-side
Tier 2
Each sale_wizard_v1_* scope maps to a wizard_state JSONB row keyed by (exchange, scope). Edit requests move to POST /exchanges/:id/edit-tickets.
sale_wizard_v1_*_phase1
sale_wizard_v1_*_exchange
sale_wizard_v1_*_acquisition
sale_wizard_v1_*_disbursement
sale_wizard_v1_*_initial
exchange_wizard_v1_*_initial
edit_tickets
Demo flags scoped to demo tenant
Tier 3
accounts.settings on the demo tenant only — never global localStorage. After this track lands, exactly one cache key remains.
demo_unlock_all
demo_identity_verified
demo_activities_logged
hub_status_${exchangeId} (KEEP — read-through cache)
Standing rule (AGENTS.md): any new localStorage write of business data is an automatic Compliance + Code Quality rejection. localStorage is permitted only as a read-through cache reconciled from the API, or ephemeral UI state that does not survive a reload. After ADR-009, the read-through cache is the single surviving key.