Appearance
Access requests (ops)
Status: slice 34 · October 2026.
UI: locus-ops /access-requests (operator queue). Public submit and e-mail verification stay on the bank app; review, rejection, and provisioning are platform-operator actions on ops.
This page documents the onboarding state machine persisted in PostgreSQL by AccessRequestsController (/api/access-requests), the guards on submit, and what provisioning creates.
State machine
text
public (bank) operator (ops, system:manage)
┌───────────┐ verify link ┌────────────────────┐ review ┌──────────┐ provision ┌─────────────┐
│ SUBMITTED │ ─────────────▶ │ COMPLIANCE_REVIEW │ ────────▶ │ APPROVED │ ──────────▶ │ PROVISIONED │
└───────────┘ (≤ 24 h) │ (EMAIL_VERIFIED → │ └──────────┘ └─────────────┘
│ auto-advanced) │ review
└────────────────────┘ ────────▶ ┌──────────┐
│ REJECTED │
└──────────┘| State | Meaning | Entered by |
|---|---|---|
SUBMITTED | Form accepted; verification e-mail sent; token valid 24 h | POST /api/access-requests (public) |
EMAIL_VERIFIED | Requester proved control of the corporate mailbox | Transient: verify immediately auto-advances to COMPLIANCE_REVIEW; still accepted as input to review |
COMPLIANCE_REVIEW | Waiting in the operator queue | GET/POST /api/access-requests/verify |
APPROVED | Operator accepted; edition fixed; tenant not yet created | POST /{id}/review decision=APPROVE |
REJECTED | Operator declined; reason stored | POST /{id}/review decision=REJECT |
PROVISIONED | Tenant, edition modules, and invited admin exist | POST /{id}/provision |
In short: pending (SUBMITTED → EMAIL_VERIFIED → COMPLIANCE_REVIEW) → APPROVED / REJECTED, then PROVISIONED for approved requests. For duplicate detection, "pending" means SUBMITTED, EMAIL_VERIFIED, COMPLIANCE_REVIEW, or APPROVED (an approved but unprovisioned request still blocks a new one for the same domain).
Illegal transitions return 409: reviewing a request that is not EMAIL_VERIFIED/COMPLIANCE_REVIEW → INVALID_STATUS_TRANSITION; provisioning anything other than APPROVED → NOT_APPROVED.
Submit guards (public, no session)
POST /api/access-requests (bank BFF /api/locus/access-requests) applies these checks in order:
| Guard | Rule | Error |
|---|---|---|
| Valid e-mail | Must contain @ | 400 INVALID_EMAIL |
| Webmail rejection | Consumer domains are refused: gmail.com, yahoo.com, hotmail.com, outlook.com, icloud.com, protonmail.com, mail.com, aol.com, gmx.com, yandex.com | 400 CONSUMER_EMAIL_REJECTED |
| Terms | termsAccepted and advisoryDisclaimerAccepted both true | 400 TERMS_NOT_ACCEPTED |
| Rate limit | 5 submissions per 15 minutes per e-mail address (in-process sliding window) | 429 RATE_LIMIT_EXCEEDED |
| Active tenant on domain | An active Organization already has this CorporateDomain | 409 TENANT_ALREADY_PROVISIONED |
| Pending request on domain | Another request in a pending state for the same domain | 409 PENDING_REQUEST_EXISTS |
On success the SoR stores the request (ar_… id, raw body in PayloadJson, RequestedEdition), generates a one-time token, stores only its SHA-256 hash with a 24-hour expiry, and sends the verification e-mail through the outbox/SMTP (/request-access/verify?token=… on App__PublicWebBaseUrl). When SMTP is not configured the response carries verificationToken as a development sink only.
Verify (public)
/request-access/verify calls GET or POST /api/access-requests/verify with the token. The SoR hashes it, rejects unknown (INVALID_TOKEN) or expired (TOKEN_EXPIRED) tokens, then sets the request to COMPLIANCE_REVIEW, records VerifiedAt, and clears the token. Re-verifying an already verified request returns alreadyVerified.
Operator review (ops)
Every review and provision call requires system:manage (PLATFORM_OPERATOR). Ops BFF routes: GET /api/access-requests, POST /api/access-requests/{id}/review, POST /api/access-requests/{id}/provision.
- Open
/access-requests, select a request inCOMPLIANCE_REVIEW. - Check the institution against its COSUMAF/COBAC licence and national registry (the reviewer notes field is for this).
- Approve — optional
approvedEdition(defaults to the requested edition, normalized). Audit eventACCESS_REQUEST_APPROVE. - Reject —
rejectionReasonis stored; if blank the default isInstitutional criteria not met.. Audit eventACCESS_REQUEST_REJECT. - Reviewer id, timestamp, and notes are persisted on the row.
Provisioning
For an APPROVED request, Provisionner le tenant calls POST /{id}/provision, which:
- refuses if an active organization already exists for the domain (
409 TENANT_ALREADY_PROVISIONED); - creates the
Organization(org_…): name from the institution,CorporateDomain, edition preset,PrimaryCountry,LicensedJurisdictions,EnabledModulesJsonfromEditionModuleCatalogfor the approved edition, and aProfileJsoncontaining the requester as the first team member; - finds or creates the
Userfor the requester e-mail; - creates a
Membershipwith roleINSTITUTION_ADMINand status Invited, with an invite token (SHA-256 hashed, 72 h TTL); - sets the request to
PROVISIONEDand records the newOrganizationId; - e-mails the invite (
/auth/accept-invite?token=…) and writesACCESS_REQUEST_PROVISIONto the audit chain.
The new tenant starts with an empty workspace. The admin sets a password and OTP through accept-invite, then creates dossiers. Edition presets: Securities, Credit, Asset Management, DFI, Sovereign, Legal, Enterprise.
Where each piece lives
| Step | App | Route |
|---|---|---|
| Submit form | bank locus-web | /request-access → /api/locus/access-requests (public) |
| Verify link | bank locus-web | /request-access/verify → /api/locus/access-requests/verify (public) |
| Queue, approve/reject, provision | locus-ops | /access-requests |
| Legacy bank page | bank locus-web | /workspace/access-requests redirects to ops via NEXT_PUBLIC_OPS_APP_URL |
Tests: onboarding_lifecycle.test.ts, access_request_production_api.test.ts.
Related: Ops overview · Auth & sessions · Trust boundaries · Security.