> ## Documentation Index
> Fetch the complete documentation index at: https://internal.softcrum.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Organizaciones, usuarios, membresías e invitaciones

> Cuando esto se entregue, una empresa tiene una cuenta y su administrador puede invitar al equipo.

> Traducción. Autoritativo: [`fs-idn-0001-organizations-and-users.md`](/modules/identity/features/fs-idn-0001-organizations-and-users).

## 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.memberships` con las asignaciones de rol.
* `identity.invitations` con 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)*

1. 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.
2. 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.
3. El registro crea la organización y la primera membresía en la **misma transacción**.
4. 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.
5. 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.
6. Desactivar una membresía revoca las sesiones de ese usuario **solo para esa organización**.
7. Una membresía **nunca se borra en duro**. Desactivarla conserva el rastro, y quien vuelve recupera
   su membresía en vez de recrearla.
8. Toda mutación escribe en `core.audit_log` con actor tipado (ADR-017).
9. PROHIBIDO: una membresía sin organización · una invitación reutilizable tras aceptarse o expirar.

## Datos *(normativo)*

| Tabla                    | Invariantes clave                                                                                                                                   |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `identity.organizations` | `slug` único e inmutable tras crearse; `name`; `default_locale`, `timezone`; `is_active`; nunca se borra en duro                                    |
| `identity.users`         | `organization_id` NOT NULL; `email` citext, **único por (`organization_id`, `email`)**; `name`; `locale`; `is_active`; `last_seen_at`               |
| `identity.memberships`   | único (`user_id`, `organization_id`); `is_active`; `deactivated_at`; al menos un owner activo por organización, garantizado por constraint diferido |
| `identity.invitations`   | `token_hash` único; `email`, `organization_id`, `roles`, `invited_by`; `expires_at`, `accepted_at`; conservada tras aceptarse                       |

Ninguna tabla se particiona. El volumen lo acota la dotación de nuestros clientes, no la de los suyos.

## API *(normativo)*

| Endpoint                                        | Clase      | Permiso                                 | Presupuesto |
| ----------------------------------------------- | ---------- | --------------------------------------- | ----------- |
| `POST /v1/identity/organizations`               | Management | flujo de registro, público              | p95 \<1 s   |
| `GET/PATCH /v1/identity/organizations/current`  | Management | `identity.organizations.{read\|update}` | p95 \<1 s   |
| `GET /v1/identity/memberships`                  | Management | `identity.memberships.read`             | p95 \<1 s   |
| `POST /v1/identity/invitations`                 | Management | `identity.invitations.create`           | p95 \<1 s   |
| `POST /v1/identity/invitations/{token}/accept`  | Management | público, autenticado por token          | p95 \<1 s   |
| `POST /v1/identity/memberships/{id}/deactivate` | Management | `identity.memberships.deactivate`       | p95 \<1 s   |

## 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)*

1. El registro crea organización y membresía owner en una transacción; una falla inducida no deja
   ninguna de las dos.
2. 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.
3. Quitar o degradar al último owner se rechaza con error tipado.
4. Una invitación aceptada no se puede aceptar de nuevo; una expirada se rechaza con razón distinta.
5. Desactivar una membresía revoca sesiones de esa organización y deja válidas las de otras.
6. 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.
7. Test de denegación RLS entre organizaciones.
8. **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 por `messaging` consumiendo el evento
`user.invited` — este módulo nunca habla con un proveedor de correo directamente.

## Preguntas abiertas

| # | Pregunta                                                                                                   | Decide | Para                                                                          |
| - | ---------------------------------------------------------------------------------------------------------- | ------ | ----------------------------------------------------------------------------- |
| 1 | Expiración de invitación — ¿7 o 14 días?                                                                   | Daniel | antes de aprobar                                                              |
| 2 | ¿Un usuario puede borrar su propia cuenta, y qué pasa con sus membresías?                                  | Daniel | antes de aprobar                                                              |
| 3 | ¿Ofrecemos account linking opcional después, para que una agencia cambie entre cuentas sin reautenticarse? | Daniel | ahora no — registrado para que la respuesta sea deliberada cuando se pregunte |

## Changelog

| Versión | Fecha      | Cambio           | Por qué | Autor                  |
| ------- | ---------- | ---------------- | ------- | ---------------------- |
| 0.1.0   | 2026-08-17 | Borrador inicial | —       | daniel + claude-opus-5 |

## Registro de entrega

*Aún no implementado.*
