Work / Chitragupt / Wiki / Decisions
2026-07-11-user-identity-entry-model
Decisioncanonicalverified 2026-09-12
DECISION.2026-07-11.USER-IDENTITY-ENTRY-MODELUser → Identity → Document → Entry (12-bucket model)
Decision
Every parsed fact in chitragupt lives on a single chain:
User = Firebase auth account (one PAN via self identity, subscription, DPDP consent, regime pref)
└─ Identity = "self" (exactly one per user, holds this user's PAN + DOB + name)
OR an entity relevant to the user
~31 typed variants across 12 top-level buckets
└─ Document = uploaded file + parsed fields + identity_id + about_self
└─ Entry = one parsed fact (income line, TDS row, holding, txn, prescription)
Rename users/{uid}/accounts/ → users/{uid}/identities/. The typed discriminator becomes identity_type (~31 values). One reserved identity per user has type: "self" and holds the login-owner's PAN / DOB / full name (removed from users/{uid} root). Every document carries identity_id (the source entity) + bucket (denormalised for fast Inbox queries) + about_self. Every ledger entry carries identity_id + bucket + a strict-enum entry_type (no more free-form string).
The Inbox left rail is exactly 12 top-level buckets — Tax · Banks · Employer · Investments · Loans · Insurance · Health · Assets · Retirement · Business · Identity documents · Other. Every identity belongs to exactly one bucket. The Tax bucket is a virtual view (docs cross-listed by AY relevance).
Multi-Firebase-user family (Pro Family, up to 6 PANs) stays unchanged as the only multi-person model — dependents = their own login. No in-workspace person-identities.
Why
Today three ideas collide:
- Login owner carries their own PAN inline on
users/{uid}— one truth for "self". - Entities the user deals with live in the typed
accounts/table — another truth for "who is this doc from". identity_extractedsnapshot on every document — a third truth, never rolled up, used only for matcher scratchpad.
Every callable that reads PAN has two code paths (users/{uid}.pan vs accounts/{id}.employer_pan); the parser has one identity discipline; the Inbox folder tree has a third. This drift caused named bugs (FormType enumerated in 4 places, category never re-derived after parse, mapper entry_type free-form).
Prior ADR 2026-05-31-accounts-master-table established the typed-discriminator primitive — this decision keeps that primitive and extends it: one uniform identities/ table where self is just another row. Prior ADR 2026-05-31-owner-validation established the accept-grows-identity matcher — this decision keeps the flow and renames the module (owner-validation → identity-matcher), collapsing three "pending" statuses into one pending_identity_confirmation.
Chitragupt is a zero-customer MVP; there is no cost to a clean rewrite. The no-legacy rule mandates a single-PR cutover with every account_id / users/{uid}.pan / three-status-set reference deleted, not left behind under a banner.
Impact
- Schema (
packages/shared/src/schemas/):account.ts→identity.ts; newbuckets.ts,entry-types.ts;user-profile.tsshrinks (dropspan,dob_iso,display_name,residential_status);document.tsgainsidentity_id + bucket + about_self, collapses three "pending" statuses;ledger-entry.tsgainsidentity_id + bucketand enum-checksentry_type. - Storage (
firebase/firestore.{rules,indexes.json}): newusers/{uid}/identities/*rules; deleteusers/{uid}/accounts/**; CA share scope becomesidentity_ids; new composite indexes on(identity_id, uploaded_at),(bucket, uploaded_at),(identity_id, ay, entry_class),(bucket, ay). - Backend (
apps/functions/src/):parser/owner-validation.ts→parser/identity-matcher.ts;on-document-confirmed.ts+makeEntryextend signatures; all 34 mappers stamp identity_id + bucket;profile/save-profile.tsshrinks + newupsert-identity.ts; tax + CA-handoff PDF source PAN fromidentities/self; seed personas regenerated. - Website (
apps/website/src/):store/accounts.ts→store/identities.ts; InboxFolderTree+DocumentListrewritten around 12 buckets + 4-level tree; newsettings/identities/surface; onboarding step-1 writes toidentities/self;settings/accounts/*deleted. - Wiki: this ADR + new pages user-identity-entry, identity-buckets, identity-types, ledger-entry-types; inbox-folder-taxonomy superseded by identity-buckets; family-workspace clarified to state dependents = own Firebase login only.
- Wireframes: 12-bucket Inbox tree + new Settings/Identities page + updated onboarding step-1.
Locked-in scope decisions this ADR carries:
- Dependents ≠ in-workspace identities. Multi-user family (Pro Family) is the only multi-person model.
- No
household_rollups. Household aggregates for Expense/Portfolio computed at read time. - Regime preference stays per-user (dependents have their own Firebase login → their own regime).
- Health-pillar schema deferred —
health_conditionidentity type exists in the schema so docs land in the Health bucket, but a dedicated Health page ships later.
Affects inbox-document-types, pillar-inbox, pillar-tax, pillar-expense, pillar-portfolio, inbox-pillar, upload-only, no-manual-entry, family-workspace, inbox-folder-taxonomy, 2026-05-31-accounts-master-table, 2026-05-31-owner-validation.
Cleanup progress (2026-09-12)
The account_id line of the aggressive-cleanup checklist is now zero across
apps/, packages/, firebase/ and scripts/ (the only survivors are
Razorpay's own account_id on its webhook payload and linked_account_id,
which points at linked_broker_accounts — a real broker account, not an
identity). The rename also closed a live bug: firestore.rules scoped the CA
read on documents/{docId} using resource.data.account_id, a field
DocumentSchema had already dropped, so an identity-scoped share matched no
document at all. See ROADMAP.md Phase 8 for the itemised list and for what is
still outstanding — chiefly sub-phase 3, which still leaves
users/{uid}.pan / dob_iso / display_name / residential_status writable
alongside identities/self.
Status
Active. Supersedes 2026-05-31-accounts-master-table and 2026-05-31-owner-validation.
Sources
- .context/wiki/decisions/* § "User → Identity → Document → Entry (12-bucket model)"
packages/shared/src/schemas/identity.ts(post-rewrite)apps/functions/src/parser/identity-matcher.ts(post-rewrite)
Every project of mine is written down like this.
Read the résumé