Apariencia
0049. Alta delegada de tenants: reusar /auth/onboarding, no reimplementarlo
Estado
Aceptada
Contexto
platform-onboarding-tenants (2026-07-21) le dio al superadmin una vía para dar de alta un tenant desde camaroneras_platform: crea la organización (ya nombrada) e invita al primer admin por email. Al planificar su retiro del signup público (deprecar-signup-publico, ver backlog) apareció un caso que esa tarea no cubría: el superadmin no siempre sabe el nombre legal de la empresa del cliente, y forzarlo a inventarlo (o a pedírselo por otro canal antes de dar de alta) es peor experiencia que dejar que el propio cliente lo escriba, como hacía el signup público que se está por retirar.
La pregunta de diseño: ¿cómo nombra su empresa un cliente que llega por invitación, sin que exista auto-registro público?
Dos caminos:
- Reimplementar la creación de organización del lado del cliente — un endpoint nuevo (
PATCHoPOSTsobre la organización) que el admin invitado llama tras aceptar. - Reusar
/auth/onboarding, el endpoint que ya hace exactamente esto — pero dándole una segunda vía para llegar apending_onboardingademás del signup.
Decisión
Se reusa /auth/onboarding tal cual. No se le cambia la lógica (sigue creando Organization + sembrando roles/curvas + vinculando OrganizationUser + activando al usuario, vía el mismo helper createOrganizationWithUniqueSlug que ya comparte con el signup) — solo se corrige el mensaje de su único chequeo de precondición (dbUser.status !== 'pending_onboarding'), que asumía por texto que el único camino previo era la verificación de email.
Lo que cambia es quién puede dejar a un usuario en pending_onboarding:
POST /platform/organizationsgana un modo delegado: si se omitename, crea solo elUserinvitado (sin org, sin roles, sin curvas, sin membresía) — el alta completa queda pendiente de que el cliente la complete.POST /auth/accept-invitationbifurca: si el invitado que acepta no tiene membresía (llegó por el modo delegado), en vez de sesión completa le emite unonboardingToken— el mismo token de scopeonboardingque hoy emitesignup— y lo deja enpending_onboarding. Antes de esta tarea, ese caso ("invitación sin organización") era imposible por diseño: cualquier invitación (UsersService.invite, o el alta directa de plataforma) siempre creaba la membresía junto con el usuario, y el código lo trataba como un400inalcanzable ("La invitación no tiene organización asociada"). Ese branch pasó a ser el camino feliz del modo delegado.
Toda la maquinaria del scope onboarding (issueOnboardingToken, OnboardingJwtGuard, onboarding.strategy.ts, JWT_ONBOARDING_SECRET) queda intacta y compartida entre las dos vías — no hay dos tokens ni dos guards paralelos.
Por qué no el camino 1 (endpoint nuevo)
- Habría duplicado la secuencia de siembra (org + roles + curvas + membresía) que ya vive en un solo lugar compartido entre signup y el alta directa de plataforma — exactamente el tipo de divergencia que ese helper compartido (
createOrganizationWithUniqueSlug, extraído enplatform-onboarding-tenants) se creó para evitar. - El endpoint nuevo habría necesitado su propio guard, su propia validación de estado, su propio DTO — todo ya resuelto por
/auth/onboardingy su guard existente. - El costo del camino 2 es solo un
ifde bifurcación en dos lugares (createyaccept-invitation), contra reimplementar un flujo completo.
Efecto colateral en deprecar-signup-publico
Esta decisión corrige el relevamiento original de esa tarea (hecho antes de esta): el token de onboarding y pending_onboarding NO quedan huérfanos al retirar el signup — el modo delegado los mantiene vivos. Solo pending_verification queda sin productor tras ese retiro (ver backlog, entrada actualizada).
Listado mixto en GET /platform/organizations: sin paginar las filas delegadas
Corolario de diseño: una invitación delegada pendiente no tiene organización, así que no puede vivir en la tabla Organization ni en su paginación. Se expone como una fila más en el mismo listado (kind: 'pending_invitation', id = el del usuario invitado) para que el superadmin no tenga que mirar dos pantallas — pero esas filas se devuelven siempre completas y al principio, fuera de la paginación (meta describe solo organizaciones reales). Es una inconsistencia deliberada y acotada: el volumen esperado de invitaciones delegadas pendientes es bajo y transitorio (se resuelven o expiran en días), así que no justifica un UNION paginado sobre dos tablas de forma distinta. Documentado explícitamente en plataforma/superadmin-flow.md para que ningún consumidor futuro asuma data.length <= meta.pageSize.
Consecuencias
POST /platform/organizations:namepasa a opcional, cambia la forma de la respuesta ({ organization: {...} | null, adminEmail }, antes{ id, name, slug, adminEmail }) — breaking, pero su único consumidor (camaroneras_platform) se actualizó en la misma tarea.POST /auth/accept-invitationgana un discriminadorneedsOnboardingen la respuesta — mismo patrón polimórfico (oneOfen el spec) que ya usabaPOST /auth/signin.POST /platform/invitations/:userId/resend, endpoint nuevo: el reenvío existente (POST /platform/organizations/:id/resend-invitation) está scopeado por organización y no sirve para una invitación delegada, que todavía no tiene una.- Un usuario
pending_onboardingsin membresía es un estado nuevo en el sistema (antes, todo usuario que llegaba a ese estado ya tenía una organización esperándolo desde el signup). No requirió cambios adicionales: el access token de scopefullexigeorganizationIden su payload (AccessTokenPayload, no opcional) y solo lo emiteissueSession, que nunca se llama para este caso — no hay ningún guard que necesite un chequeo nuevo para no asumirorganizationIden un usuario sin membresía. camaroneras_admin'sAcceptInvitationPagebifurca igual que el backend: conneedsOnboarding: trueguarda elonboardingTokeny redirige a/onboardingen vez de abrir sesión. El caso límite "el usuario recarga la página y pierde el token en memoria" no necesitó código nuevo:SignInPageya redirige unpending_onboardinga/onboardingcon un token fresco al iniciar sesión (mecanismo preexistente del signup, reusado sin tocar).
Referencias
- Tarea
platform-onboarding-delegado(2026-07-21). - [ADR anterior de la cadena: alta directa]
platform-onboarding-tenants(2026-07-21, sin ADR propio — extensión aditiva sobre RBAC de plataforma, ver ADR-0048). camaroneras_backend/src/modules/auth/auth.service.ts(acceptInvitation,onboarding),src/modules/platform/platform-organizations.service.ts(create,resendDelegatedInvitation),src/modules/platform/platform.service.ts(listOrganizations).camaroneras_docs/plataforma/superadmin-flow.md,plataforma/rbac-flow.md,plataforma/auth-flow.md(contratos actualizados)._planning/_backlog/backlog.md→deprecar-signup-publico(relevamiento corregido por esta decisión).