ADR-023: Identity and access
Estado
Accepted — 2026-08-23
Amendment 2026-08-24: dual IAM in factory (Organization User + PlatformUser). Permission catalog + custom roles. /platform in V1.
Amendment 2026-09-08: Universo de producto = Subscriber (B2C), no Organization finance con OWNER/AR_SPECIALIST. IAM de staff en /platform se conserva. Filename kept for stable links.
Contexto
Hay que saber: quién es el trader (Subscriber), qué Plan/entitlements tiene, y quién opera IVPrior (PlatformUser).
La fábrica traía un panel “finance” con roles de cobranzas (OWNER, FINANCE_ADMIN, AR_SPECIALIST). Eso no aplica. IVPrior no vende asientos a un equipo de AR; vende acceso al scanner a una persona.
OAuth de Gmail/M365 (ADR-020) es leftover de mailbox Cashlane, no login de producto IVPrior.
Identidad self-hosted, sin Auth0/Clerk en V1.
Decisión
NestJS + email/password + JWT access + refresh
universe: subscriber → Subscriber (cuenta B2C)
universe: platform → PlatformUser (staff)
Universo Subscriber (app del producto)
- Identidad self-hosted (password hash argon2). Un signup = un Subscriber (ADR-048, ADR-018).
- JWT de acceso corto + refresh rotativo. Claims:
universe: subscriber, id de cuenta, entitlements de Plan (no matriz RBAC de “empleados del tenant”). - V1 sin roles de equipo del trader (
OWNER/FINANCE_ADMIN/VIEWERde fábrica). El Subscriber es dueño de su cuenta. - Email unique global en el universo Subscriber (un humano, una cuenta). No
(organizationId, email). - Guards Nest: auth → aislamiento de cuenta (ADR-018) → entitlement de Plan. El id del token gana sobre el body.
- Sin SSO/SAML/Google login de producto en V1.
- Sesiones revocables. Logout invalida refresh.
- Seats / invitaciones a “compañeros del trader” = fuera de V1.
Universo Platform (/platform)
- PlatformUser es staff IVPrior: tabla aparte, JWT
universe: platform, sin id de Subscriber en el token de staff. - IAM de staff sí se conserva: PlatformPermission / PlatformRole / overrides. Semillas
PLATFORM_ADMIN,PLATFORM_SUPPORT,PLATFORM_OPERATIONS. Branding, social, planes, usuarios internos viven aquí. - Login de plataforma es superficie distinta (no el mismo login que el Subscriber). El primer admin nace por seed, no por signup público.
- Impersonation (slice posterior) nunca usa un id de cuenta mágico: sesión explícita + audit.
Alternativas
| Opción | Nota |
|---|---|
| Self-hosted JWT + dos universos | Elegida — control, costo, fábrica |
| RBAC de Organization B2B | Rechazada para V1 — no hay staff del subscriber |
| Clerk / Auth0 | Vendor + precio; se reevalúa si un piloto lo exige |
| Google / SSO como login de producto | Fuera de V1 |
Consecuencias
/platform/access/*(permisos, roles, usuarios) es IAM de staff, no de tenants restaurante. El nav se oculta si el catálogoplatform_permissionsno está seedeado — eso es un bug de seed, no un recorte de producto.- Signup público del Subscriber: flag
signup.open(ADR-048). - Código de fábrica con
universe: organizationse lee como Subscriber hasta el remap. - Mailbox OAuth (ADR-020) no es identidad de IVPrior; no implementar “conectar Gmail” como onboarding.