Skip to content

Data stores ​

Status: slice 34 · October 2026.

What is stored where, which entities exist in the system of record, and how tenant isolation applies to each group. Sources: technical presentation §4.4, technical documentation §10.3 / §10.11 / §10.12, and the EF Core model at slice 34 (11 migrations, InitialSoR → LegalVaultApplicabilityAndAmendments).

1. Stores at a glance ​

StoreUse
PostgreSQLSoR: tenants, dossiers, OCR pages, cells, vault metadata, market, portfolio, audit, sessions, e-mail outbox, platform legal tables
RedisBullMQ (web: DOCUMENT_OCR, CONTRADICTION_SCAN, MEMO_COMPILATION) + C# RedisAnalysisJobStore for analysis jobs; rate-limiter buckets when configured
Cloudflare R2Tenant deal-room PDFs (AES-256-GCM when DOCUMENT_ENCRYPTION_KEY_HEX is set); ops vault PDFs when R2 is configured
Local vault dirDev fallback: ops LEGAL_VAULT_LOCAL_DIR, SoR LEGAL_VAULT_STORAGE_PATH
In-memory mapsLocal/tests only: queue without REDIS_URL, some domain mirrors. Production fails closed without Redis
Local engine corpuslocus-web/data/regulatory/ JSON + SQLite for the regulatory engine and retrieval (Regulatory fixtures) — not the vault

Rule: Next.js never opens Postgres for product writes. locus-web and locus-ops reach data only through locus-backend.

2. Entity inventory ​

Canonical model reference (all 46 entities, enums, RLS notes): locus-backend → Database models.

2.1 Identity and tenancy ​

EntityPurpose
OrganizationTenant: name, CorporateDomain, edition preset (Securities, Credit, AssetManagement, Dfi, Sovereign, Legal, Enterprise), licensed CEMAC jurisdictions, EnabledModulesJson (13 modules; regulatory cannot be disabled), ProfileJson (name, logo, avatar, team)
UserInstitutional e-mail, password hash, active flag (login always requires OTP)
MembershipUser ↔ organization role binding, status (Invited, Active, …), invite token hash + expiry, ModuleOverridesJson
AuthSessionPostgres session: TokenHash = SHA-256(sessionId), role, org, expiry (8 h login / 72 h invite)
AccessRequestOnboarding record with the state machine in Access requests
EmailOutboxQueued verification, invite, and notification e-mails
AuditEventAppend-only, hash-chained ledger (PreviousHash, Hash per tenant)

2.2 Dossier and evidence (tenant-scoped) ​

EntityPurpose
DossierMandate container: borrower, target debt, currency XAF/EUR/USD, JurisdictionProfileJson, WorkspacePayloadJson, LeadArrangerId, status ORIGINATION → IN_DILIGENCE → COMMITTEE_REVIEW → CLOSED / ARCHIVED
DocumentStorage metadata: S3/R2 key, SHA-256 (plaintext digest), page count, OcrStatus (PENDING, COMPLETED, FAILED)
ExtractedPageOCR / native page text persisted by POST /api/documents/{id}/ocr-result
CitationSource grounding: document, page, normalized bbox x0,y0,x1,y1, snippet, human-verified flag. Page cites cite_{docId}_p{n}; line cites on cells cite_{docId}_p{page}_{lineCode} (Citations)
FinancialStatementMulti-year SYSCOHADA statements (Bilan Actif, Bilan Passif, SIG)
ExtractedCellReported vs accepted value, SourceCitationId, override history (previous value, rationale, user, timestamp)
RatioSnapshotDeterministic KPIs (Net Debt, EBE, leverage, DSCR, ICR), formula id, covenant flags
ContradictionRecord (Contradictions)Pairwise discrepancies with severity, dual citations, waiver/resolution rationale
ProspectusAuditCOSUMAF pre-visa checklist, score, fatal blockers, attestation hash
BlockerItem (Blockers)Open fatal blockers per dossier (e.g. tax quitus, BEAC art. 42 declaration)
StressScenarioCredit-lab shocks and breach alerts
QaThreadQ&A governance threads
DossierRegulatoryFindingPersisted analyzeDealWithVault findings incl. vault artifact ids, for audit replay

2.3 Market and fixed income (tenant-scoped) ​

EntityPurpose
MarketBulletinOfficial BEAC/BVMAC publication metadata; de-duplicated on Sha256Hash per tenant
MarketObservationSeries points (seriesKey, country, value, observedAt)
AuctionLineTreasury auction results (tenor, instrument type, marginal yield, payload)
CurvePublicationFitted curves: model (default Nelson–Siegel), parameters JSON, Rmse
PricingScenarioUnderwriting / pricing cockpit scenarios
BlotterInstrumentInstruments blotter marks
AlertItem (AlertItems)Surveillance alerts: OPEN → ASSIGNED → ACKNOWLEDGED → ARCHIVED; created from critical contradictions, covenant breaches, fatal blockers, and market rows (MARCHÉ)

2.4 Portfolio (tenant-scoped) ​

PortfolioIssuer, PortfolioInstrument, PortfolioHolding, PortfolioLimit, PortfolioException. The bank reads the book through GET /api/locus/portfolio → GET /api/portfolio/book (financial:read, fail-closed tenant) and maps it to BondMath valuation, limits, attribution, and copilot tools. The instruments blotter shows provenance tags (SoR Book vs demo vs empty).

2.5 Search (tenant-scoped) ​

SearchDocument backs /api/locus/search (dossier:read). Migration PostgresRlsAndTsvectorSearch adds a generated French tsvector column (SearchVector, built from Title and Body) with a GIN index; the copilot's retrieveEvidence and the global ⌘K modal use it. Results are filtered to the dossiers the caller may see, and OCR text is indexed so hits resolve to Citation { documentId, page, bbox, snippet }.

2.6 Precedents and institutional memory (tenant-scoped) ​

EntityPurpose
PrecedentClosed mandates promoted into institutional memory with spreads and covenants (dossier:read); proprietary tenant data is strictly segregated
PostCloseOutcomeAppend-only post-close ledger (coupons, waivers, technical defaults) that never rewrites committee minutes

RegulatorySource, LegalInstrument, LegalEdition, LegalSourceArtifact, LegalProvision, LegalAmendment, LegalProvisionApplicability, LegalReviewItem, LegalIngestionLog. Written only by PLATFORM_OPERATOR. Edition state: legal_status (in_force, historical, under_review, imported) and verification_status (legal_reviewed, …). Details: Regulatory admin, Legal vault & Watchtower.

3. Tenant-scoped vs platform tables ​

GroupIsolation
Everything with OrganizationId (dossiers, memberships, audit, market, portfolio, precedents, search, alerts, outbox, sessions, access requests)EF HasQueryFilter + FORCE RLS
Dossier children (Documents, ExtractedPages, Citations, financials, contradictions, Q&A, blockers, findings)Filter via Dossier.OrganizationId; dossier-child RLS policies
Platform legal tablesNo tenant column; write gate system:manage; tenant read via resolve/open-statute only

4. Row-level security ​

Migration PostgresRlsAndTsvectorSearch: FORCE RLS with locus_tenant_ok(OrganizationId) and dossier-child policies. TenantRlsMiddleware sets the GUCs app.current_tenant_id and app.is_platform_operator; TenantRlsConnectionInterceptor applies them per connection. This supersedes older notes calling native RLS "optional hardening". The only deliberate filter bypass is the pre-claim AuthSessions lookup in InstitutionalAuthMiddleware.

5. Object storage layout ​

LibraryKey patternNotes
Tenant deal roomtenants/{tenantId}/dossiers/{dossier}/{doc}/…Private bucket; SoR checks the tenant prefix; AES-256-GCM (IV + tag in object metadata); reads via authenticated BFF proxy /api/locus/documents/{id}/content or short presigned GET (≤ 15 min) for legacy plaintext
Platform vaultR2 key or local:<abs path>LegalSourceArtifact carries SHA-256; open-statute verifies before streaming

6. Migrations and recovery ​

  • Migrations live in locus-backend/Data/Migrations/; the backend applies them with MigrateAsync on start. Seeding runs only if Seed:RunOnStartup=true (never in production).
  • scripts/backup-and-migrations-verify.sh (CI job release-and-dr) checks migration integrity against the model snapshot and exercises the backup/restore runbook.
  • Redis loss costs in-flight jobs, not durable state.

Related: locus-backend · Trust boundaries · Security · Glossary.

Source of trust for Locus. If this conflicts with code, code + IMPLEMENTATION_TRACKER.md win.