Appearance
locus-ops (platform)
Next.js 15 (App Router) · React 19 · Bun · port 3001 (next dev --port 3001) · cookie locus_ops_session.
Responsibilities
- Platform-only UI for
PLATFORM_OPERATOR— separated from the bank workspace so tenant users never see platform surfaces. - Regulatory admin (sources, vault, review, applicability, logs).
- Access-request approve / provision.
- Market ingest on behalf of a tenant org.
- Legal vault PDF upload (R2 or
LEGAL_VAULT_LOCAL_DIR).
text
PLATFORM_OPERATOR browser
│ cookie locus_ops_session (HMAC v1 ticket, NOT shared with bank)
▼
locus-ops middleware ─► (ops)/ pages ─► /api/* BFF (requireOpsSession)
│ │ Bearer <sessionId>
▼ ▼
PUBLIC: /login, /login/otp, /api/auth locus-backend :5000
/api/admin/regulatory/* (system:manage)
/api/access-requests/* (system:manage)
/api/market/ingest (org override)Stack
| Concern | Choice |
|---|---|
| Framework | Next.js ^15.1.7, React ^19 |
| Styling | Tailwind 3 + shared institutional look (AdminShell, OpsFrame) |
| Storage | @aws-sdk/client-s3 for legal vault objects |
| Tests | bun test |
Identity: PLATFORM_OPS vs bank
| Aspect | locus-ops | locus-web |
|---|---|---|
| Role allowed | PLATFORM_OPERATOR only | in-tenant roles (never PLATFORM_OPERATOR) |
| Cookie | locus_ops_session | locus_session (+ __Secure-locus-session) |
| Login | POST /api/auth/platform-login (SoR) | POST /api/auth/login (SoR) |
| Mismatch response | NOT_PLATFORM_OPERATOR before OTP is issued | USE_PLATFORM_LOGIN if an operator tries the bank form |
| Public paths | /login, /login/otp, /api/auth | landing, register, request-access, legal, … |
| Unauthenticated | pages → redirect /login?redirect=…; /api/** → 401 { error: "UNAUTHENTICATED", message: "Ops session ticket required." } | same pattern |
Cookies are separate from the bank. The signing key may be the same SESSION_SIGNING_KEY (recommended if rotating tickets across apps), but the cookie names differ so a bank session can never authenticate in ops and vice versa. Ops ticket and bank ticket share the v1.<payload>.<sig> format — see Auth & sessions.
The ops /api/auth route (src/app/api/auth/route.ts) supports:
| Method / action | Behaviour |
|---|---|
GET | Resolve cookie → SoR session; 401 unless role is PLATFORM_OPERATOR |
POST { action: "platform_login", email, password, rememberDevice } | SoR platform-login; returns mfaRequired: true (+ otpCode outside production) |
POST { action: "otp", email, code } | SoR OTP verify; 403 NOT_PLATFORM_OPERATOR if the session role differs; mints ticket and sets cookie |
POST { action: "logout" } | Revokes SoR session and deletes cookie |
Key routes
| Route | Page | Purpose |
|---|---|---|
/login, /login/otp | public | Platform password + OTP |
/ | (ops)/page.tsx | "Console opérateur" — tiles to Regulatory Admin / Market ingest / Demandes d'accès |
/regulatory | RegulatoryAdminView | Source Registry · Legal Vault · Review · Applicability · Logs |
/access-requests | AccessRequestsAdminView | Onboarding queue (review / provision) |
/market | MarketIngestAdminView | Tenant-targeted market ingest JSON |
BFF routes (src/app/api/)
| Group | Routes |
|---|---|
| Auth | /api/auth |
| Access requests | /api/access-requests, [id]/review, [id]/provision |
| Market | /api/market/ingest |
| Regulatory | overview, sources, sources/[id] (+ check, fetch-documents), sources/check-all, instruments, instruments/[id], editions/[editionId]/{artifact,provisions,amendments}, artifacts/[id]/{content,extract}, amendments, applicability (+ [id]), review (+ [id]/decide), logs |
Each regulatory route proxies 1:1 to api/admin/regulatory/* on the SoR (regulatory-proxy.ts), which enforces system:manage.
Legal vault object storage
src/lib/storage/legal-vault.ts computes SHA-256 of the uploaded PDF and stores it at legal-vault/{sha256}/{safe-filename}:
| Backend | When | storageKey |
|---|---|---|
| R2 | R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET all set | legal-vault/{sha256}/{file} |
| Local | otherwise | local:<abs path under LEGAL_VAULT_LOCAL_DIR/{sha256}/{file}> |
The BFF then registers metadata with POST /api/admin/regulatory/editions/{editionId}/artifact (SoR stores sha256, storageKey, filename, size; duplicate SHA reuses the artifact row). See Legal vault & Watchtower.
Env (locus-ops/.env.example)
| Variable | Role |
|---|---|
LOCUS_API_BASE_URL | SoR (default http://localhost:5000) |
SESSION_SIGNING_KEY | HMAC key for locus_ops_session; required in production (openssl rand -hex 32) |
NEXT_PUBLIC_APP_URL | Optional public URL of the ops app (the bank app uses NEXT_PUBLIC_OPS_APP_URL to redirect) |
R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET, R2_ENDPOINT | Legal vault object storage |
LEGAL_VAULT_LOCAL_DIR | Fallback dir (default ./data/legal-vault, gitignored) when R2 is not configured |
Seeded operator (dev only)
When the SoR seeder runs (Seed:RunOnStartup=true): ops@locus.platform / Locus@2026!Seed. Off in production by default (Seed__RunOnStartup=false). Do not use this credential outside a throwaway environment.
Tests
locus-ops/tests/ (bun test): ops_app_scaffold, legal_vault_watchtower, document_fetcher_and_vault_resolve, legal_vault_amendments_applicability_fixtures, vault_publisher_extract_findings, market_ingest_and_change_detector.
UI tours
Ops overview · Regulatory admin · Access requests · Market ingest UI.