Organization Tenants
Vínculo M:N entre cooperativa (organization) e tenant — base do escopo de leitura por tenant.
Define quais tenants alcançam cada cooperativa. É o que faz a trava de acesso funcionar: um usuário só enxerga as cooperativas (e os documentos delas) dos tenants dos quais faz parte. Enquanto uma cooperativa não tiver vínculo com nenhum tenant do usuário, ela permanece invisível para ele.
Desde 2026-09-03 esse vínculo também governa as chamadas app-to-app: um token de serviço só alcança as cooperativas vinculadas ao seu próprio tenant (ver Escopo por organização).
O mesmo vínculo é gerenciado por dois contextos, com regras diferentes:
- Administrativo (
/api/*) — o tenant vem da rota, então um administrador vincula qualquer cooperativa a qualquer tenant. Exclusivo da personaglobal. - App-to-app (
/service/*) — o tenant é sempre o do chamador (resolvido do token de serviço peloServiceAuthMiddleware) e nunca vem no corpo. Um serviço só vincula/desvincula cooperativas ao próprio tenant. É como um serviço adquire alcance, e por isso essas rotas consultam a base inteira.
| Método | Rota | Auth | Ação (role) | Sucesso | Idempotente |
|---|---|---|---|---|---|
POST | /api/tenants/:tenantId/organizations | usuário | organization-tenants:create | 201 | sim |
DELETE | /api/tenants/:tenantId/organizations/:organizationId | usuário | organization-tenants:delete | 204 | sim |
GET | /api/organizations/:organizationId/tenants | usuário | organization-tenants:read | 200 | — |
GET | /api/tenants/:tenantId/organizations | usuário | tenants:read | 200 | — |
POST | /service/organizations/:organizationId/tenants | serviço | — | 201 | sim |
DELETE | /service/organizations/:organizationId/tenants | serviço | — | 204 | sim |
Controle de acesso por persona. As rotas
/apideste módulo são exclusivas da personaglobal(decisão de produto de 2026-09-04): o vínculo define o alcance de um tenant, no mesmo nível de Tenants — quem administra um tenant não amplia o próprio alcance.tenant-adminestá emTENANT_ADMIN_EXCLUDED_MODULES, e nenhuma outra persona tem o módulo na matriz. Liberação pontual só por role de exceção (organization-tenants:create), emodulo:acao:denybloqueia. Token sem isso →403 NOT_ALLOWED. As rotas/service/*não passam por esse controle.A leitura pelo lado do tenant (
GET /api/tenants/:tenantId/organizations) é anterior a este módulo e continua sobtenants:read— ver Tenants.
Vincular cooperativa ao tenant (admin)
POST /api/tenants/:tenantId/organizations
Passa a dar ao tenant alcance sobre a cooperativa informada. Idempotente: se o vínculo já
existir, responde 201 do mesmo jeito, sem duplicar.
Params
| Param | Tipo | Regras |
|---|---|---|
tenantId | uuid | Tenant existente (404 TENANT_NOT_FOUND se não existir) |
Corpo
| Campo | Tipo | Obrigatório | Regras |
|---|---|---|---|
organizationId | uuid | sim | Cooperativa existente (404 ORGANIZATION_NOT_FOUND se não existir) |
curl -X POST \
http://localhost:3000/api/tenants/$TENANT_ID/organizations \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "organizationId": "'$ORGANIZATION_ID'" }'201 Created
{
"organizationId": "o1b2c3d4-e5f6-4789-a123-456789abcdef",
"tenantId": "t1b2c3d4-e5f6-4789-a123-456789abcdef"
}Erros
| Status | code | Quando |
|---|---|---|
400 | VALIDATION_ERROR | organizationId ausente ou não-uuid |
403 | NOT_ALLOWED | Persona sem organization-tenants:create |
404 | ORGANIZATION_NOT_FOUND | Cooperativa não encontrada |
404 | TENANT_NOT_FOUND | Tenant não encontrado |
Desvincular cooperativa do tenant (admin)
DELETE /api/tenants/:tenantId/organizations/:organizationId
O tenant deixa de alcançar a cooperativa. Idempotente: se não houver vínculo, ainda responde
204.
| Param | Tipo | Regras |
|---|---|---|
tenantId | uuid | Tenant existente (404 TENANT_NOT_FOUND se não existir) |
organizationId | uuid | Cooperativa existente (404 ORGANIZATION_NOT_FOUND se não existir) |
curl -X DELETE \
http://localhost:3000/api/tenants/$TENANT_ID/organizations/$ORGANIZATION_ID \
-H "Authorization: Bearer $ACCESS_TOKEN"204 No Content — vínculo removido (ou já inexistente).
Erros
| Status | code | Quando |
|---|---|---|
403 | NOT_ALLOWED | Persona sem organization-tenants:delete |
404 | ORGANIZATION_NOT_FOUND | Cooperativa não encontrada |
404 | TENANT_NOT_FOUND | Tenant não encontrado |
Desvincular derruba também os escopos por usuário daquele par: as linhas de
user_tenant_organizations que apontavam para esse vínculo são removidas em cascata pelo
banco (FK onDelete: Cascade). Um usuário RESTRICTED que só tinha essa cooperativa
concedida fica sem nenhuma no tenant — ver
User Tenant Organizations.
Listar tenants de uma cooperativa
GET /api/organizations/:organizationId/tenants
Leitura inversa de GET /api/tenants/:tenantId/organizations — quais tenants alcançam esta
cooperativa. Alimenta o painel de vínculos na tela de detalhe da cooperativa.
| Param | Tipo | Regras |
|---|---|---|
organizationId | uuid | Cooperativa existente (404 ORGANIZATION_NOT_FOUND se não existir) |
curl http://localhost:3000/api/organizations/$ORGANIZATION_ID/tenants \
-H "Authorization: Bearer $ACCESS_TOKEN"200 OK
{
"data": [
{
"id": "t1b2c3d4-e5f6-4789-a123-456789abcdef",
"customerId": "c1b2c3d4-e5f6-4789-a123-456789abcdef",
"appId": "a1b2c3d4-e5f6-4789-a123-456789abcdef",
"name": "Acme Tenant",
"description": null,
"active": true,
"metadata": null,
"keycloakClientId": "acme-tenant",
"allowedScopes": [],
"createdAt": "2026-09-04T12:00:00.000Z",
"updatedAt": null
}
]
}Erros
| Status | code | Quando |
|---|---|---|
403 | NOT_ALLOWED | Persona sem organization-tenants:read |
404 | ORGANIZATION_NOT_FOUND | Cooperativa não encontrada |
Listar cooperativas de um tenant
GET /api/tenants/:tenantId/organizations — retorna as cooperativas alcançadas pelo tenant.
Exige tenants:read (não organization-tenants:read): a rota é anterior a este módulo e
não foi migrada para não quebrar quem já a consome.
curl http://localhost:3000/api/tenants/$TENANT_ID/organizations \
-H "Authorization: Bearer $ACCESS_TOKEN"200 OK
{
"data": [
{
"id": "o1b2c3d4-e5f6-4789-a123-456789abcdef",
"name": "Cooperativa Recicla Já",
"alias": null,
"federalTaxId": "12345678000190",
"active": true,
"listed": true
}
]
}Erros
| Status | code | Quando |
|---|---|---|
404 | TENANT_NOT_FOUND | Tenant não existe |
App-to-app (/service)
O tenant é sempre o do chamador, resolvido do token de serviço — nunca vem na rota nem no corpo.
Vincular cooperativa ao tenant autenticado
POST /service/organizations/:organizationId/tenants
curl -X POST \
http://localhost:3000/service/organizations/$ORGANIZATION_ID/tenants \
-H "Authorization: Bearer $SERVICE_ACCESS_TOKEN"201 Created
{
"organizationId": "o1b2c3d4-e5f6-4789-a123-456789abcdef",
"tenantId": "t1b2c3d4-e5f6-4789-a123-456789abcdef"
}Erros
| Status | code | Quando |
|---|---|---|
404 | ORGANIZATION_NOT_FOUND | Cooperativa não encontrada |
Vincular enxerga qualquer cooperativa existente, inclusive uma que o tenant ainda não
alcança — é justamente para isso que a rota serve (@UnscopedService()). A cooperativa também
entra no escopo do usuário de X-Username, se o dele no tenant é restrito. O vínculo feito pelo
painel (POST /api/tenants/:tenantId/organizations) não concede nada a ninguém: lá quem vincula
é o administrador. Já desvincular
opera dentro do escopo: uma cooperativa que não é sua responde 404 ORGANIZATION_NOT_FOUND.
Desvincular cooperativa do tenant autenticado
DELETE /service/organizations/:organizationId/tenants
curl -X DELETE \
http://localhost:3000/service/organizations/$ORGANIZATION_ID/tenants \
-H "Authorization: Bearer $SERVICE_ACCESS_TOKEN"204 No Content — vínculo removido (ou já inexistente).
Erros
| Status | code | Quando |
|---|---|---|
404 | ORGANIZATION_NOT_FOUND | Cooperativa não encontrada |