Domain · Architecture

Lakshmi Domain — Architecture

Lakshmi is the bounded context for personal and household financial intelligence.

12sections13 minread1diagrams

On this page

Personal and household finance for the Oshun monorepo: a deep, typed library of financial domain logic (account modelling, budgeting, tax, investments, retirement, estate, behavioral finance) sitting on a Drizzle/PostgreSQL data layer, fronted by a set of service applications. This page describes the real code under libs/lakshmi/* and apps/lakshmi/*, and is honest about which parts are fully implemented versus which are runtime skeletons not yet wired to the domain logic.


What Lakshmi Is#

Lakshmi is the bounded context for personal and household financial intelligence. Its purpose is to give a user a single, consent-driven view of every account they hold — checking, savings, credit cards, loans, brokerage and retirement accounts, crypto wallets, real estate, insurance policies, business equity — and to turn that aggregated picture into planning: budgets, debt payoff, tax optimization, retirement projection, estate readiness, and behavioral nudges.

Where Neith is the lowest layer of the stack, Lakshmi is a product domain near the top: it consumes shared infrastructure and adjacent domains' facts (Aje for on-chain assets, Cybele for property facts, Gaia for climate/energy inputs) and owns one thing exclusively — the personal-finance decision. It is the source of truth for what a user's net worth is, how their budget is tracking, and whether their retirement is on course. It is named after the Hindu goddess of wealth and prosperity.

The design principle that shapes every layer is consent-first: no financial data enters the system without explicit user consent, every connection is revocable, and every row is scoped to its owner at the database level.


Implementation Status — Read This First#

Earlier drafts of this page called Lakshmi a "fully-implemented platform" where "every feature exists in source." That is half-true and worth stating precisely, because the split matters for anyone working here:

  • The domain libraries are real and deep. The 24 packages under libs/lakshmi/* contain genuine, domain-specific financial logic — calculation engines (amortization, time-value-of-money, Monte Carlo, tax brackets, risk metrics, Social Security), a 32-variant account model, TF-IDF transaction categorization, portfolio performance math (TWR/MWR/XIRR), tax-loss harvesting, and more. These are pure input→output modules with their own types and tests.
  • The data layer is real. @lakshmi/db declares a full Drizzle schema, RLS policy generation, TimescaleDB hypertable plans, a pgvector similarity index, connection pooling, a migration registry, and seed data.
  • The service runtime is partly a skeleton. The six apps/lakshmi/* services provide real infrastructure — a Hono gateway with JWT/OAuth/RBAC middleware and OpenAPI generation, a BullMQ queue/DLQ system, a cron scheduler, typed Kafka event schemas. But the HTTP data plane is not yet wired to the domain libraries: the gateway's route handlers return placeholder empty payloads ({ accounts: [] }, taxProfile: null) with comments like "Domain service call will be implemented in @lakshmi/accounts library." The sync-engine and ai-agents services currently serve only health checks; the latter says so explicitly in its source.

So Lakshmi today is a strong domain core and data layer with a deliberately fail-honest service shell around it. The libraries can be imported and used directly; the end-to-end HTTP/sync/AI pipelines are scaffolding awaiting wiring. Items genuinely deferred are labelled (planned) below.


Workspace Shape#

The domain is 24 TypeScript libraries under libs/lakshmi/* and 6 service applications under apps/lakshmi/*.

text
apps/lakshmi/
  api-gateway/        Hono HTTP/WS gateway: auth, RBAC, OAuth scopes, OpenAPI, Kafka bus (:4200)
  sync-engine/        health/ready surface; open-banking polling (planned wiring) (:4201)
  ai-agents/          health/ready surface; financial agents (planned wiring) (:4202)
  worker/             BullMQ background worker: 5 queues + DLQs
  scheduler/          BullMQ repeatable-job scheduler (:3804)
  browser-extension/  MV3 extension: shopping-context capture (popup/content/service-worker)

libs/lakshmi/
  core/  db/                                   ← foundation
  accounts/  integrations/                     ← integration
  transactions/ budgeting/ behavioral/         ← intelligence
  investments/ tax/ debt/ credit/ retirement/
  insurance/ estate/ real-estate/ crypto/
  income/ goals/ household/ business/ ai-engine/
  security/                                    ← security
  reporting/ alerts/                           ← experience

Note: there is no libs/contracts/src/lakshmi or libs/openapi/src/specs/lakshmi package in source. Cross-domain contracts under @contracts/lakshmi are (planned); the OpenAPI document is generated at runtime inside the gateway (apps/lakshmi/api-gateway/src/public-rest-api.ts), not published as a shared spec package.


Layered Architecture#

The libraries are organized into five dependency layers. Higher layers depend on lower ones; the reverse is never allowed, which keeps core primitives stable while the experience layer evolves.

  • Core@lakshmi/core (money, account, user/household primitives, the calculation engine) and @lakshmi/db (the persistence layer). Foundation for everything above.
  • Integration@lakshmi/accounts (aggregation-provider adapters, sync, statement import, account management) and @lakshmi/integrations (open-banking, payroll, credit-bureau, crypto, real-estate, document gateways). Normalizes raw provider data into domain objects.
  • Intelligence — the bulk of the domain: @lakshmi/transactions, @lakshmi/budgeting, @lakshmi/behavioral, @lakshmi/investments, @lakshmi/tax, @lakshmi/debt, @lakshmi/credit, @lakshmi/retirement, @lakshmi/insurance, @lakshmi/estate, @lakshmi/real-estate, @lakshmi/crypto, @lakshmi/income, @lakshmi/goals, @lakshmi/household, @lakshmi/business, and @lakshmi/ai-engine. Derives meaning and recommendations from ingested data.
  • Security@lakshmi/security (encryption, key derivation, RBAC, MFA, GDPR/CCPA/SOC2 controls, privacy-preserving primitives). Enforces privacy across layers, complementing the database's row-level security.
  • Experience@lakshmi/reporting (statements, exports, shareable links) and @lakshmi/alerts (threshold notifications), surfaced through the apps/lakshmi/* services.

The Core Domain Model (@lakshmi/core)#

@lakshmi/core is the foundation every other library imports. Its public surface (libs/lakshmi/core/src/index.ts) re-exports caching, Prometheus metrics, financial primitives, domain types, and the calculation engine. The specifications page enumerates the full type system; the load-bearing pieces are:

  • Branded ID types (Brand<T, B> in types/account.ts and types/user.ts) — UserId, HouseholdId, AdvisorId, AccountId, ConnectionId, InstitutionId, ManualAssetId. These are compile-time unique so an AccountId cannot be passed where a UserId is expected.
  • FinancialAccount (libs/lakshmi/core/src/types/account.ts) — the central type: a discriminated union over a type field with 32 account variants across eight AccountCategory groups (depository, credit, loan, investment, crypto, real_estate, business, manual). Each carries an AccountSign (asset/liability) that drives its net-worth contribution. Using a discriminated union forces every code path to handle every variant.
  • Money (libs/lakshmi/core/src/primitives/money.ts) — an immutable value object holding integer minor units plus an ISO 4217 currency. Arithmetic preserves currency and rejects cross-currency operations; fromDecimal, divide, allocate, and split use banker's rounding (round-half-to-even) and conserve remainder pennies. Every monetary field in the domain is integer cents to avoid floating-point drift.
  • The calculation engine (libs/lakshmi/core/src/calculations/) — a deterministic suite kept separate from the domain types so it can be tested against known answers: amortization (fixed/ARM/interest-only/balloon, per-payment schedules, extra-payment modelling), tvm (PV/FV/NPV/IRR/XIRR), monte-carlo (retirement and goal simulation), tax-brackets (federal + 50-state, 2024), risk-metrics (Sharpe/Sortino/VaR/CVaR/beta/alpha), and social-security (PIA/COLA/claiming adjustments).

Transactions and goals are deliberately not core entities — they are owned by @lakshmi/transactions and @lakshmi/goals and persisted by @lakshmi/db.


Persistence and the Data Layer (@lakshmi/db)#

@lakshmi/db (libs/lakshmi/db/src/index.ts) is fully implemented and is the single owner of Lakshmi's PostgreSQL schema.

  • Namespaces and tables. src/schema/namespaces.ts declares 19 PostgreSQL schema namespaces (accounts, transactions, budgets, investments, tax, debt, credit, retirement, insurance, estate, real_estate, crypto, income, goals, behavioral, household, business, alerts, audit), mirrored by LAKSHMI_SCHEMA_NAMES in the migration registry. Across src/schema/*.ts there are roughly 80 Drizzle tables (82 …Schema.table(...) declarations), mapped to their namespaces by LAKSHMI_TABLE_SCHEMA_BY_NAME. (Earlier drafts said "20 namespaces / 78 tables"; the source has 19 and ~80.)
  • Row-level security. src/migrations.ts generates real RLS SQL. Helpers like buildUserScopedRlsPolicy, buildHouseholdSharedRlsPolicy, buildHouseholdOwnerRlsPolicy, and buildParentScopedRlsPolicy emit ENABLE ROW LEVEL SECURITY plus CREATE POLICY statements predicated on current_setting('app.current_user_id', true)::uuid. A query physically cannot return another user's rows even if an application check is bypassed — the database enforces the boundary.
  • Time-series. TIMESCALE_HYPERTABLE_CONFIGS + applyTimescaleHypertables convert the time-series tables (balance/net-worth/price/benchmark/credit-score history) into TimescaleDB hypertables via create_hypertable(...), and the routine skips gracefully if TimescaleDB is not installed rather than failing the migration.
  • Vector search. The same migration builds a pgvector IVFFlat index on transaction embeddings for similarity-based categorization. (The gateway's /v1/transactions/search endpoint anticipates this but is not yet wired.)
  • Connection pooling. src/connection.ts exposes three purpose-built pg pools — a primary pool, a high-throughput transaction-ingest pool (short statement timeout, larger max), and an analytics pool (120 s statement timeout for Monte Carlo / report queries) — each wrapped by Drizzle.
  • Seed and reference data. src/seed-data/ ships 2024 federal/state tax brackets, IRS contribution limits, RMD uniform lifetime table, Social Security benefit tables, market benchmarks, credit-score factor weightings, the Plaid transaction-category taxonomy, and insurance product templates.

Intelligence: Where Data Becomes Insight#

The intelligence libraries are the heart of the domain. Each owns one reasoning area, shares @lakshmi/core primitives, and is otherwise independent so it can be tested and evolved alone. A representative example shows the depth and the honesty caveat that applies to a few modules:

Transaction categorization (libs/lakshmi/transactions/src/categorization/engine.ts) is a real algorithm: text normalization → tokenization → TF-IDF scoring against a per-category keyword centroid → cosine similarity → confidence tiering (HIGH ≥ 0.85, MEDIUM 0.70–0.84, LOW 0.50–0.69, VERY_LOW < 0.50), with low-confidence results routed to a ManualReviewQueue and a PersonalizationEngine that learns from user corrections. (The module's docstring quotes accuracy percentages and a future pgvector/Qdrant ANN path; treat those as aspirational — the shipped logic is the TF-IDF cosine pipeline, and the README's "98%+ accuracy / 20,000+ institutions" are marketing claims, not measured metrics.)

The other intelligence modules follow the same pattern of concrete domain math: @lakshmi/budgeting has distinct ZeroBudgetEngine/CategoryBudgetEngine/ CashFlowBudgetEngine plus velocity tracking and forecasting; @lakshmi/investments computes TWR/MWR/XIRR, VaR/CVaR, factor exposure, and tax-aware rebalancing; @lakshmi/tax does wash-sale-aware tax-loss harvesting and Roth-ladder planning; @lakshmi/retirement runs Monte Carlo projections and Social Security optimization; @lakshmi/estate tracks beneficiaries, an encrypted document vault, and probate-avoidance analysis. The specifications page lists the engine classes per module.

@lakshmi/ai-engine is intended to sit above these modules and provide reasoning, recommendation, and explanation. The library exists; the service that would orchestrate it (apps/lakshmi/ai-agents) is a health-check skeleton today (see below).


Service Runtime — Honest State#

API Gateway (apps/lakshmi/api-gateway, port 4200)#

src/app.ts builds a real Hono application. Implemented: a global middleware stack (request logger, CORS with an allow-list, secure headers), /health and /ready probes (the latter pings Redis and checks Kafka), runtime OpenAPI generation (/openapi.json, /v1/meta, /.well-known/oauth-authorization-server), and a v1 router protected by authenticate() (JWT verification via jose, claims in src/middleware/auth.ts), rateLimitMiddleware (Redis-backed), per-route OAuth scope enforcement (requireOAuthScopes, driven by the endpoint catalog), and per-route RBAC (requirePermissions, driven by LAKSHMI_ROUTE_PERMISSION_POLICIES). The JWT model supports advisor-acting-on-behalf-of-client and household-role claims.

Not implemented: the route handlers themselves. GET /v1/accounts returns { accounts: [] }; /v1/tax/profile returns taxProfile: null; almost every handler returns an empty array or null with a comment that the domain-service call is still to be added. The gateway is a complete, secured HTTP front door that does not yet call the domain libraries or the database.

Sync Engine (apps/lakshmi/sync-engine, port 4201)#

The docstring describes open-banking polling across Plaid/Yodlee/MX/Finicity/Tink and Kafka publication, but src/index.ts currently implements only a node:http server answering /health and /ready (reporting the configured provider list, sync interval, and concurrency from env). The provider adapters live in @lakshmi/accounts/providers; the service does not yet invoke them. (Polling loop: planned.)

AI Agents (apps/lakshmi/ai-agents, port 4202)#

src/index.ts is explicit and fail-honest: "The financial agents themselves … are NOT yet implemented in this service — this process currently serves health checks only." Its health payload reports only the model providers whose credentials are actually present in the environment (resolveConfiguredModelProviders checks OPENAI_API_KEY/ANTHROPIC_API_KEY/Google keys) rather than asserting connections that may not exist. (Agent orchestration: planned.)

Worker and Scheduler#

apps/lakshmi/worker/src/queues.ts is fully implemented BullMQ infrastructure: five priority queues (account-sync, transaction-categorize, ai-recommendation, report-generation, data-export), per-queue retry/backoff policies, typed job payloads, enqueue helpers with deduplicated job IDs, a typed createWorker factory with structured logging, and an attachDlqForwarder that moves permanently-failed jobs to per-queue dead-letter queues. The job processors (the functions that would call domain logic) are injected by the caller and are not yet supplied.

apps/lakshmi/scheduler/src/schedules.ts defines the real recurring tasks (buildRecurringTasks): a 15-minute due-connection sync sweep, a 5-minute uncategorized-transaction sweep, a daily AI-recommendation cron (0 3 * * *), a daily report-rollup cron, and an hourly expired-export maintenance cron — each enqueued into the corresponding worker queue with a stable jobId.

Browser Extension (apps/lakshmi/browser-extension)#

A Manifest V3 extension (service worker, content script, popup) that captures shopping context (src/shared/shopping-context.js) to support behavioral/spend insight at the point of purchase. It is plain JS with its own build and validation scripts and Playwright/vitest tests.

Event Bus#

apps/lakshmi/api-gateway/src/events.ts defines versioned Zod schemas for all domain events (account-sync, new/categorized transactions, alerts, rebalance, tax harvest, goal milestone, AI recommendation) with explicit Kafka topic routing and a correlation ID propagated from the originating request — the contract by which the gateway, sync engine, worker, and downstream consumers would decouple.


Component and Data Flow#

The diagram shows the intended runtime topology. Solid arrows are wired today; dashed arrows are the planned handler/processor wiring that is not yet in source.

flowchart TD subgraph Clients UI[Web / Mobile / Advisor portal] EXT[Browser extension] end subgraph Services[apps/lakshmi] GW[api-gateway Hono\nJWT + OAuth + RBAC + OpenAPI] SYNC[sync-engine\nhealth only today] AI[ai-agents\nhealth only today] WORK[worker BullMQ\n5 queues + DLQs] SCHED[scheduler\nrepeatable crons] end subgraph Libs[libs/lakshmi domain logic] CORE[core: Money, accounts,\ncalculations] INTEL[intelligence: transactions,\nbudgeting, investments, tax, ...] SEC[security] DB[(@lakshmi/db\nDrizzle + RLS + Timescale + pgvector)] end subgraph Infra PG[(PostgreSQL\nrow-level security)] REDIS[(Redis\ncache + rate limit + BullMQ)] KAFKA[(Kafka\ndomain events)] S3[(MinIO S3\nreceipts/docs/exports)] end UI --> GW EXT --> GW GW -->|auth, rate-limit, OpenAPI| REDIS GW -->|publish events| KAFKA GW -.->|planned: call domain services| INTEL SCHED -->|enqueue| REDIS WORK -->|consume jobs| REDIS WORK -.->|planned: invoke| INTEL SYNC -.->|planned: poll providers, ingest| INTEL AI -.->|planned: orchestrate| INTEL INTEL --> CORE INTEL --> DB CORE --> DB SEC --> DB DB --> PG WORK --> S3 KAFKA --> WORK

Invariants, Failure Modes, and Extension Points#

Invariants the codebase enforces.

  • Money is integer minor units. Every monetary value is cents; arithmetic goes through Money (banker's rounding, penny-conserving allocation, currency-safe).
  • IDs are branded. Cross-entity reference errors are caught at compile time.
  • Rows are user-scoped at the database. RLS predicated on app.current_user_id is the last line of defence regardless of application bugs; household-shared, household-owner, and parent-scoped policies layer on top.
  • Accounts are exhaustive. The 32-variant discriminated union forces every consumer to handle every account type — adding a variant surfaces every place that must change.
  • Consent-first and revocable. An AccountConnection only refreshes when active; revocation moves it to revoked.

Failure modes the code already handles honestly.

  • Missing credentials fail loud, not fake. @lakshmi/db throws if LAKSHMI_DATABASE_URL is unset; ai-agents reports only configured model providers; readiness returns 503 when Redis or Kafka is down.
  • Degraded infrastructure degrades gracefully. TimescaleDB hypertable creation is skipped (with a warning) when the extension is absent.
  • Background jobs have a dead-letter path. Jobs that exhaust retries are forwarded to a DLQ with the original payload, failure reason, and attempt count.

Extension points. Add an account type by extending the FinancialAccount union and its helper switches in core/src/types/account.ts. Add an aggregation provider via the AggregationProvider enum and a @lakshmi/accounts/providers adapter. Add a background job by defining its payload + queue in worker/src/queues.ts and a sweep in scheduler/src/schedules.ts. Add an HTTP endpoint by extending the gateway route catalog (which auto-derives OAuth-scope enforcement and the OpenAPI document). The most valuable open work is wiring the existing gateway handlers and worker processors to the existing domain libraries.


Cross-Domain Boundaries#

Lakshmi's boundaries are drawn around ownership of the financial decision, not raw data. Each adjacent domain owns its own source of truth and exposes it through a defined contract:

  • Aje owns blockchain/Web3 infrastructure; @lakshmi/crypto consumes on-chain asset data from Aje rather than re-implementing chain access, and owns how those assets appear in net worth and tax.
  • Cybele owns property records, construction costs, and AVM data; @lakshmi/real-estate consumes those facts but owns affordability, buy-vs-rent, equity, and rental economics.
  • Maat owns enterprise/organizational finance; it may consume aggregate, consented business intelligence from Lakshmi but shares no database or model.
  • Themis supplies governance policy and immutable audit records used for legal-transfer events (the V2 estate-vault surface).
  • Gaia (Phase 175) supplies renewable-energy-potential and climate-risk products that Lakshmi consumes for household capacity planning, insurance context, property-risk, and financial-scenario modelling.

Two V2 adapter surfaces — @v2/lakshmi-responsible-play-spend-insight (composing behavioral/budgeting/transactions for responsible-play store confirmation) and @v2/per-account-vault-estate-bridge (bridging estate data to @oshun/identity and @themis/transparency) — live in the V2 services tree, not under libs/lakshmi. Both are off-rollback and must never feed deterministic match simulation or competitive outcomes. See features.md for their contracts.


Verification Expectations#

Every financial calculation, categorization, aggregation, and alert path is expected to carry deterministic tests (asserting computed values against known-correct answers), plus privacy, consent, audit, and contract tests. Integration tests use sandbox fixtures and never require live aggregation, bureau, or model-provider credentials in CI — which is why the service layer's external integrations are environment-gated and fail loud when unconfigured rather than fabricating connections. The honest gap to close is the data-plane wiring between the services and the already-built domain libraries.