Traducción. Autoritativo: fs-idn-0001-organizations-and-users.md.
Contexto
Todo lo demás en la plataforma cuelga de saber a qué organización pertenece un request. La decisión que conviene enunciar temprano es que el email de un usuario es único por organización, no a nivel de base de datos (standards/data.md §2b). La misma persona puede tener
cuenta con dos de nuestros clientes, y esas cuentas no se relacionan: dos filas, dos credenciales,
dos sesiones, y ninguna organización puede descubrir que existe la otra.
La conveniencia que esto entrega es real y hay que nombrarla: una agencia que maneja tres clientes
mantiene tres logins en vez de uno con selector. Lo aceptamos. Un email único global convertiría
“invitar a esta dirección” en un oráculo que revela si esa persona ya trabaja con alguien más, y
crearía una credencial cuyo compromiso alcanza a varios clientes nuestros a la vez. El aislamiento
gana sobre la conveniencia; si el reclamo se vuelve fuerte, el account linking opcional se puede
agregar después sin migración, porque sería una tabla nueva y no un constraint cambiado.
La segunda es que una invitación es evidencia. Quién dejó entrar a quién a una cuenta, y cuándo,
es de las primeras preguntas tras un incidente. Las invitaciones aceptadas se conservan.
Alcance (normativo)
identity.organizations, una por tenant, con slug y configuración de locale.identity.users, email único por organización (UNIQUE (organization_id, email)).identity.membershipscon las asignaciones de rol.identity.invitationscon tokens de un solo uso que expiran.- Desactivación de una membresía sin borrar historia.
- Creación de la organización en el registro, y el primer usuario como su owner.
Fuera de alcance (normativo)
- La autenticación misma — FS-IDN-0003. Esto entrega quién existe, no cómo lo prueba.
- Roles y permisos — FS-IDN-0002.
- Facturación y configuración de plan. Los entitlements se leen acá, nunca se poseen.
- SCIM y sincronización de directorio — F2.
Comportamiento (normativo)
- El email de un usuario es único por organización. La misma dirección en dos organizaciones son dos usuarios independientes con credenciales independientes. PROHIBIDO: cualquier índice, búsqueda o flujo de vinculación global sobre el email.
- Una organización siempre tiene al menos una membresía activa con el rol owner. El último owner no se puede quitar ni degradar — la operación se rechaza, no se advierte.
- El registro crea la organización y la primera membresía en la misma transacción.
- Una invitación es un token de un solo uso con expiración. Aceptarla crea la membresía y la marca aceptada; la fila se conserva, nunca se borra.
- Una invitación a un email que ya tiene usuario en esta organización se adhiere a ese usuario. Una dirección que existe en otra organización se trata como nueva acá — y el flujo nunca debe revelar, por respuesta, tiempo ni redacción, que la dirección se conoce en otra parte.
- Desactivar una membresía revoca las sesiones de ese usuario solo para esa organización.
- Una membresía nunca se borra en duro. Desactivarla conserva el rastro, y quien vuelve recupera su membresía en vez de recrearla.
- Toda mutación escribe en
core.audit_logcon actor tipado (ADR-017). - PROHIBIDO: una membresía sin organización · una invitación reutilizable tras aceptarse o expirar.
Datos (normativo)
Ninguna tabla se particiona. El volumen lo acota la dotación de nuestros clientes, no la de los suyos.
API (normativo)
Eventos (normativo)
identity.user.invited, identity.user.joined y identity.user.deactivated. No se ofrecen como
webhooks salientes en F1: los cambios de personal de un tenant no son algo a lo que sus integraciones
deban suscribirse por defecto.
Criterios de aceptación (normativo)
- El registro crea organización y membresía owner en una transacción; una falla inducida no deja ninguna de las dos.
- El mismo email invitado a una segunda organización crea un usuario independiente ahí. La respuesta, su tiempo y su redacción son idénticos a invitar una dirección jamás usada.
- Quitar o degradar al último owner se rechaza con error tipado.
- Una invitación aceptada no se puede aceptar de nuevo; una expirada se rechaza con razón distinta.
- Desactivar una membresía revoca sesiones de esa organización y deja válidas las de otras.
- Una invitación aceptada sigue legible un año después, nombrando quién invitó a quién. 6b. Ningún endpoint, mensaje de error ni diferencia de tiempo revela que un email existe en otra organización.
- Test de denegación RLS entre organizaciones.
- Negativo: ningún endpoint borra en duro una membresía ni una organización.
Ejecución
Un solo slice, comando síncrono. Las invitaciones se entregan pormessaging consumiendo el evento
user.invited — este módulo nunca habla con un proveedor de correo directamente.