Appearance
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
| Store | Use |
|---|---|
| PostgreSQL | SoR: tenants, dossiers, OCR pages, cells, vault metadata, market, portfolio, audit, sessions, e-mail outbox, platform legal tables |
| Redis | BullMQ (web: DOCUMENT_OCR, CONTRADICTION_SCAN, MEMO_COMPILATION) + C# RedisAnalysisJobStore for analysis jobs; rate-limiter buckets when configured |
| Cloudflare R2 | Tenant deal-room PDFs (AES-256-GCM when DOCUMENT_ENCRYPTION_KEY_HEX is set); ops vault PDFs when R2 is configured |
| Local vault dir | Dev fallback: ops LEGAL_VAULT_LOCAL_DIR, SoR LEGAL_VAULT_STORAGE_PATH |
| In-memory maps | Local/tests only: queue without REDIS_URL, some domain mirrors. Production fails closed without Redis |
| Local engine corpus | locus-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
| Entity | Purpose |
|---|---|
Organization | Tenant: 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) |
User | Institutional e-mail, password hash, active flag (login always requires OTP) |
Membership | User ↔ organization role binding, status (Invited, Active, …), invite token hash + expiry, ModuleOverridesJson |
AuthSession | Postgres session: TokenHash = SHA-256(sessionId), role, org, expiry (8 h login / 72 h invite) |
AccessRequest | Onboarding record with the state machine in Access requests |
EmailOutbox | Queued verification, invite, and notification e-mails |
AuditEvent | Append-only, hash-chained ledger (PreviousHash, Hash per tenant) |
2.2 Dossier and evidence (tenant-scoped)
| Entity | Purpose |
|---|---|
Dossier | Mandate container: borrower, target debt, currency XAF/EUR/USD, JurisdictionProfileJson, WorkspacePayloadJson, LeadArrangerId, status ORIGINATION → IN_DILIGENCE → COMMITTEE_REVIEW → CLOSED / ARCHIVED |
Document | Storage metadata: S3/R2 key, SHA-256 (plaintext digest), page count, OcrStatus (PENDING, COMPLETED, FAILED) |
ExtractedPage | OCR / native page text persisted by POST /api/documents/{id}/ocr-result |
Citation | Source 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) |
FinancialStatement | Multi-year SYSCOHADA statements (Bilan Actif, Bilan Passif, SIG) |
ExtractedCell | Reported vs accepted value, SourceCitationId, override history (previous value, rationale, user, timestamp) |
RatioSnapshot | Deterministic KPIs (Net Debt, EBE, leverage, DSCR, ICR), formula id, covenant flags |
ContradictionRecord (Contradictions) | Pairwise discrepancies with severity, dual citations, waiver/resolution rationale |
ProspectusAudit | COSUMAF pre-visa checklist, score, fatal blockers, attestation hash |
BlockerItem (Blockers) | Open fatal blockers per dossier (e.g. tax quitus, BEAC art. 42 declaration) |
StressScenario | Credit-lab shocks and breach alerts |
QaThread | Q&A governance threads |
DossierRegulatoryFinding | Persisted analyzeDealWithVault findings incl. vault artifact ids, for audit replay |
2.3 Market and fixed income (tenant-scoped)
| Entity | Purpose |
|---|---|
MarketBulletin | Official BEAC/BVMAC publication metadata; de-duplicated on Sha256Hash per tenant |
MarketObservation | Series points (seriesKey, country, value, observedAt) |
AuctionLine | Treasury auction results (tenor, instrument type, marginal yield, payload) |
CurvePublication | Fitted curves: model (default Nelson–Siegel), parameters JSON, Rmse |
PricingScenario | Underwriting / pricing cockpit scenarios |
BlotterInstrument | Instruments 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)
| Entity | Purpose |
|---|---|
Precedent | Closed mandates promoted into institutional memory with spreads and covenants (dossier:read); proprietary tenant data is strictly segregated |
PostCloseOutcome | Append-only post-close ledger (coupons, waivers, technical defaults) that never rewrites committee minutes |
2.7 Platform legal vault (no OrganizationId)
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
| Group | Isolation |
|---|---|
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 tables | No 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
| Library | Key pattern | Notes |
|---|---|---|
| Tenant deal room | tenants/{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 vault | R2 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 withMigrateAsyncon start. Seeding runs only ifSeed:RunOnStartup=true(never in production). scripts/backup-and-migrations-verify.sh(CI jobrelease-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.