ADR-018 — Isolation of Subscriber data

Architecture decision record — status: Accepted.

Source: docs/adr/ADR-018-multi-tenancy.md

ADR-018: Isolation of Subscriber data

Estado

Accepted — 2026-08-23
Amendment 2026-08-24: PlatformUser in factory; impersonation never uses a magic organizationId.
Amendment 2026-09-08: Product tenant ≠ Organization B2B. Isolation boundary = Subscriber. Filename kept (multi-tenancy) for stable links. Factory column organizationId is a leftover name until a remap slice.

Contexto

IVPrior es SaaS B2C: muchas cuentas de traders (Subscribers) en una sola app. El Subscriber A nunca ve watchlist, oportunidades, diary ni billing del Subscriber B.

La fábrica copiada (FoodHub/Cashlane) aislaba Organization (agencia / restaurante con staff). Eso no es el modelo de negocio (DOC-001, DOC-002). El patrón técnico (shared Postgres + columna de cuenta) sí se reutiliza.

Decisión

  1. Límite de aislamiento de producto = Subscriber (cuenta de un trader que compra un Plan). En docs y copy nueva: subscriberId. En schema Prisma de fábrica: la columna puede seguir llamándose organizationId hasta el slice de remap — un solo id de cuenta, no dos modelos.
  2. Shared Postgres (una base). Aislamiento lógico, no schema-per-tenant ni DB-per-tenant en V1.
  3. Toda tabla de negocio del Subscriber (excepto catálogos globales / platform) lleva el id de cuenta NOT NULL + índice.
  4. Todo use case de Subscriber recibe el id del actor autenticado; prohibido tomarlo solo del body si contradice el token.
  5. Queries de dominio del Subscriber siempre filtran por ese id. Tests de aislamiento son obligatorios en módulos de cuenta.
  6. RLS de Postgres: opcional como defensa en profundidad más adelante, no gate de MVP.
  7. Staff IVPrior = PlatformUser (ADR-023). Opera /platform con JWT universe: platform. Nunca un id de cuenta “mágico” para leer todos los Subscribers. Impersonation (si existe) es sesión explícita, TTL + audit.
  8. Catálogos globales (planes SaaS, branding default de plataforma, permisos de staff) no llevan id de Subscriber.
  9. V1 no modela Organization B2B, staff del subscriber, locations, ni “tenant restaurant”. Un humano = un Subscriber.

Alternativas

OpciónNota
Shared DB + id de cuentaElegida — costo, DX, fábrica
Organization B2B + empleadosRechazada — no es el comprador de IVPrior
Schema-per-tenantOperación pesada para retail B2C
DB-per-subscriberInviable en V1

Consecuencias

  • Tenant en copy de producto está prohibido; el listado /platform/tenants es leftover de UI hasta rename a Subscribers.
  • Customer de Cashlane (deudor) no es el Subscriber. No reutilizar esa entidad.
  • AuditLog puede tener id de cuenta null cuando el actor es PlatformUser.
  • Remap organizationIdsubscriberId + rename de nav es un Spec Kit posterior; este ADR ya fija el significado.