Skip to content

locus-web (bank) ​

Next.js 15 App Router · React 19 · TypeScript · Bun tests · port 3000.

Responsibilities ​

  • Institutional UI (/workspace/*) — shell, sections, copilot sheet.
  • BFF /api/locus/* → SoR with Authorization: Bearer <sessionId> + Zod validation + RBAC guard.
  • Auth cookie mint (/api/auth), document upload to R2, OCR orchestration, copilot, exports proxy.
  • Next.js never spawns OS subprocesses (exec()); heavy work goes through SoR jobs → workers.
text
Browser ──cookie locus_session──► Edge middleware (ticket HMAC + expiry)
                                     │
                                     ▼
                        App Router / BFF route handlers
                        requireBffSession ─► enforceRbacGuard ─► Zod
                                     │ Bearer <sessionId> · X-Organization-Id
                                     ▼
                              locus-backend :5000

Stack ​

ConcernChoice
FrameworkNext.js 15 App Router, React 19, TypeScript
StylingTailwind + institutional design system (globals.css, ui/ components)
Client dataTanStack Query for deal refresh
TestsBun (locus-web/tests/)
Object storage@aws-sdk/client-s3 + presigner against Cloudflare R2
ValidationZod (src/lib/api/validation.ts, schemas.ts, sor-write-schemas.ts)
QueueBullMQ when REDIS_URL is set; in-memory Map otherwise (local/tests)

Code layout ​

ZonePathRole
App routessrc/app/workspace/*, src/app/api/*Pages + BFF
Workbenchsrc/components/workbench/WorkbenchView, workspace-routes.ts, WorkspaceSection.tsx
Domainssrc/domains/{portfolio,market,fixed_income,regulatory,auth,editions,dataroom,queue,jobs,…}TS business logic
Regulatory twinsrc/regulatory_engine/ (+ domains/regulatory/)analyzeDeal, analyzeDealWithVault, catalog, versioning, applicability
Copilotsrc/copilot/engine, agent, async, retrieval, llm, financials, tools/*
Storagesrc/lib/storage/{r2,document-upload,ocr-ingest,ocr-worker-client,vault-crypto}.tsVault + ingest
SoR clientsrc/lib/api/locus-server.ts, locus-bff.ts, locus-browser.ts, legal-vault.tsSession → guard → Bearer SoR
Authsrc/middleware.ts, src/lib/auth/{session-ticket,session-cookie}.ts, src/domains/auth/Edge gate, RBAC matrix, enforceRbacGuard
Local enginesengine/ratio_calculator.ts, credit_lab/, risk_engine/, dataroom_completeness/Client-side deterministic math

Workspace routes ​

Declared in workspace-routes.ts (WORKSPACE_SECTIONS, 28 sections) and rendered by WorkspaceSection.tsx. URL shape: /workspace/{section}?project={dealId}.

ZoneSections
Workoverview, deals, new-dossier, deal-room, dataroom-completeness, deal-qa, deal-graph, diligence-runner, issuers, instruments, portfolio
Analysisfinancial-tables, ratio-cockpit, credit-lab, review-grids, contradiction-radar, risk-register, regulatory, cosumaf-auditor, sovereign-curves, fixed-income-terminal
Monitoringmarket-radar
Knowledgeresearch, institutional-memory, memo-drafter
Adminprofile, org-members, org-settings
  • sovereign-curves and fixed-income-terminal both render FixedIncomeTerminalView; market-radar and research both render MarketIntelligenceView (see Workspace route map).
  • platformOpsRedirectPath() maps legacy access-requests → /access-requests and regulatory-admin → /regulatory on locus-ops (absolute URL via NEXT_PUBLIC_OPS_APP_URL).

Institutional identity (UI) ​

ElementSource
SidebarLogo + legal name of the organization (orgProfile.logoDataUrl / companyName) — not the user name
NavbarProfile avatar (avatarDataUrl) through UserAccountMenu
Admin/workspace/org-settings (logo, name, jurisdiction), /workspace/org-members (SMTP invites, roles, module overrides)
COSUMAF attestationLocus product logo (/logo.svg) + arranger = authenticated organization
Fresh tenantWorkspace is empty until a dossier is created; SoR unreachable → empty institution shell, never a branded demo firm

Auth gate (middleware) ​

src/middleware.ts (matcher: everything except _next/static, _next/image, favicon.ico):

Path classBehaviour
Static assets (/_next, .png, .svg, .css, …)pass
Public pages/, /landing, /login, /register, /auth/login, /auth/forgot-password, /auth/otp, /auth/reset-password, /pricing, /request-access, /request-access/verify, /onboarding, /auth/accept-invite, /legal + /legal/{mentions-legales,confidentialite,conditions,securite}
Public API prefixes/api/onboarding, /api/regulatory/search, /api/auth, /api/locus/members/accept-invite, /api/locus/access-requests
Other /api/** without valid ticket401 { error: "UNAUTHENTICATED" }
/workspace/** without valid ticketredirect /login?redirect=…

The ticket is read from cookie locus_session (or __Secure-locus-session) or Authorization: Bearer; only signed v1. tickets are accepted — legacy unsigned sess_* cookies are always rejected. There is no demo bypass. Workspace layout.tsx additionally resolves the session through the SoR (fetchSorSession) and redirects if it is gone. Ticket format and lifecycle: Auth & sessions.

BFF → SoR proxy (/api/locus/*) ​

Every handler runs requireBffSession(req, '<permission>') → enforceRbacGuard, then forwards Authorization: Bearer <sessionId> + X-Organization-Id derived from the session (admin headers are never trusted in production).

BFF routeUpstream SoR
/api/locus/dealsGET/POST /api/deals
/api/locus/deals/[id]GET /api/deals/{id}
/api/locus/deals/[id]/statusPUT /api/deals/{id}/status
/api/locus/deals/[id]/archive · unarchivePOST /api/deals/{id}/archive · unarchive
/api/locus/deals/[id]/reconciliationGET /api/deals/{id}/reconciliation (fallback reconciliationService.evaluateReconciliation)
/api/locus/deals/[id]/cells/[cellId]/overridePOST /api/deals/{id}/cells/{cellId}/override (Zod CellOverrideSchema)
/api/locus/deals/[id]/analysis · jobsPOST /api/deals/{id}/analysis · GET /api/deals/{id}/jobs
/api/locus/analysisjobs/[jobId]GET /api/analysisjobs/{jobId}
/api/locus/deals/[id]/qa · qa/[threadId]api/deals/{dealId}/qa
/api/locus/deals/[id]/regulatory-findingsGET/PUT /api/deals/{dealId}/regulatory-findings
/api/locus/documents/uploadR2 put + POST /api/documents + OCR persist
/api/locus/documents/[id]/contentAuthenticated decrypt proxy (AES-256-GCM objects)
/api/locus/documents/[id]/urlPresign R2 GET (legacy plaintext objects)
/api/locus/documents/[id]/verify-citationPOST /api/documents/{id}/verify-citation (±5 px)
/api/locus/legal-vault/resolve · artifacts/[id]/contentGET /api/legal-vault/resolve · artifacts/{id}/content
/api/locus/members*invites, roles, audit, resend, accept-invite
/api/locus/organizationProfile (logo / avatar / name)
/api/locus/exports/[...type]POST /api/exports/* (PDF / Excel)
/api/locus/search · blockers · portfolio · market · precedents · alertsapi/search, api/blockers, api/portfolio/book, api/market/overview, api/precedents, api/alerts
/api/locus/market/ingestPOST /api/market/ingest
/api/locus/access-requests · verify · [id]/review · [id]/provisionsubmit (public) / list (system:manage) / verify (public) / review / provision
/api/locus/storage/healthR2 ping

Other non-proxy routes: /api/auth (login / OTP / logout / cookie), /api/regulatory/* (audit, ingest, search), /api/jobs/*, /api/pipeline, /api/copilot/polish-stream.

Zod validation ​

Every /api/locus/* handler imports validation.ts; mutating routes use parseJsonBody / validateRequestBody against schemas.ts (CreateDealSchema, ArchiveDealSchema, MemberInviteSchema, AccessRequestReviewSchema, DocumentUploadFormSchema, VerifyCitationSchema, CellOverrideSchema, search/blockers query schemas, …). Invalid input returns a structured 400 VALIDATION_ERROR with field paths — never stack traces. Market ingest uses MarketIngestSchema (sor-write-schemas.ts, max 1000 rows per collection).

Other BFF guards: sliding-window rate limiter (60 req/min → 429 + Retry-After), 401 UNAUTHENTICATED / 401 SESSION_EXPIRED, 403 TENANT_FORBIDDEN.

Security headers & secrets ​

2-year preloaded HSTS (max-age=63072000; includeSubDomains; preload), X-Frame-Options: DENY, strict CSP. SecretsManager (src/lib/security/secrets.ts) reads process environment only and fails closed in production on forbidden placeholders. Cookie options: httpOnly, secure in production, sameSite=lax, path=/.

Environment (locus-web/.env.example) ​

VariableRole
LOCUS_API_BASE_URLC# SoR (default http://localhost:5000)
NEXT_PUBLIC_APP_URLPublic URL of the bank app (build-time)
SESSION_SIGNING_KEYHMAC key for locus_session tickets — required in production (openssl rand -hex 32)
DOCUMENT_ENCRYPTION_KEY_HEXAES-256-GCM at rest for R2 objects — required when NODE_ENV=production
LOCUS_ALLOW_PLAINTEXT_DOCUMENTSEmergency only; never in prod
R2_SIGNING_KEY, REGULATORY_ATTESTATION_HMAC_SECRETPresign / attestation signing
R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET, R2_ENDPOINTCloudflare R2 (default endpoint https://{R2_ACCOUNT_ID}.r2.cloudflarestorage.com)
REDIS_URLBullMQ; required in production
LOCUS_ALLOW_INMEMORY_QUEUEEmergency only
LOCUS_WORKER_URL (alias PYTHON_WORKER_URL)Python worker for Tesseract OCR (default http://127.0.0.1:8000)
COPILOT_LLM_PROVIDERoff | openai | anthropic
OPENAI_API_KEY / ANTHROPIC_API_KEYProse polish only
COPILOT_OPENAI_MODEL / COPILOT_ANTHROPIC_MODELOptional model overrides
LOCUS_ORG_ID, LOCUS_DEV_USER_ID, LOCUS_DEV_USER_ROLEDev-only header mode — never production
NEXT_PUBLIC_OPS_APP_URLRedirect target for legacy ops routes

Coolify: set runtime secrets as available in container, not during build; NEXT_PUBLIC_* need a redeploy. Never set LOCUS_ALLOW_* in production. See Deploy & env.

Tests ​

bun test in locus-web/tests/ (e.g. auth_gate_and_api_rbac, session_ticket_middleware, auth_otp_and_register, bff_locus_validation, bff_analysis_jobs_contract, dossier_persistence_rbac, market_portfolio_search_rbac, document_encryption_at_rest, fail_closed_integration). Web Python tests live alongside locus-workers/tests/.

UI tours ​

Workspace route map · Shared shell · Full guide.

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