ADR-023 — Identity and access

Architecture decision record — status: Accepted.

Source: docs/adr/ADR-023-identity-access.md

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)

  1. Identidad self-hosted (password hash argon2). Un signup = un Subscriber (ADR-048, ADR-018).
  2. JWT de acceso corto + refresh rotativo. Claims: universe: subscriber, id de cuenta, entitlements de Plan (no matriz RBAC de “empleados del tenant”).
  3. V1 sin roles de equipo del trader (OWNER / FINANCE_ADMIN / VIEWER de fábrica). El Subscriber es dueño de su cuenta.
  4. Email unique global en el universo Subscriber (un humano, una cuenta). No (organizationId, email).
  5. Guards Nest: auth → aislamiento de cuenta (ADR-018) → entitlement de Plan. El id del token gana sobre el body.
  6. Sin SSO/SAML/Google login de producto en V1.
  7. Sesiones revocables. Logout invalida refresh.
  8. Seats / invitaciones a “compañeros del trader” = fuera de V1.

Universo Platform (/platform)

  1. PlatformUser es staff IVPrior: tabla aparte, JWT universe: platform, sin id de Subscriber en el token de staff.
  2. IAM de staff se conserva: PlatformPermission / PlatformRole / overrides. Semillas PLATFORM_ADMIN, PLATFORM_SUPPORT, PLATFORM_OPERATIONS. Branding, social, planes, usuarios internos viven aquí.
  3. Login de plataforma es superficie distinta (no el mismo login que el Subscriber). El primer admin nace por seed, no por signup público.
  4. Impersonation (slice posterior) nunca usa un id de cuenta mágico: sesión explícita + audit.

Alternativas

OpciónNota
Self-hosted JWT + dos universosElegida — control, costo, fábrica
RBAC de Organization B2BRechazada para V1 — no hay staff del subscriber
Clerk / Auth0Vendor + precio; se reevalúa si un piloto lo exige
Google / SSO como login de productoFuera de V1

Consecuencias

  • /platform/access/* (permisos, roles, usuarios) es IAM de staff, no de tenants restaurante. El nav se oculta si el catálogo platform_permissions no 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: organization se lee como Subscriber hasta el remap.
  • Mailbox OAuth (ADR-020) no es identidad de IVPrior; no implementar “conectar Gmail” como onboarding.