O OpenFGA no Node.js permite implementar autorização baseada em relações, também conhecida como ReBAC. Em vez de espalhar condicionais como “o usuário é administrador?” ou “o dono do documento é o mesmo da sessão?” por controllers e services, a aplicação descreve relações entre usuários, grupos, organizações e recursos em um modelo central.
Esse tipo de autorização é útil em sistemas colaborativos, SaaS multi-tenant, plataformas de arquivos, gestão de projetos e aplicações em que o acesso depende de vínculos. Um usuário pode editar um documento porque é proprietário, porque pertence a uma equipe editora ou porque recebeu acesso por meio de uma pasta pai.
Neste guia, você aprenderá a executar OpenFGA, instalar o SDK JavaScript, criar uma store, definir um modelo, gravar relationship tuples, verificar permissões, listar objetos acessíveis, integrar com Express ou Fastify e evitar falhas como confiar em IDs enviados pelo cliente.
O que é OpenFGA?
A documentação oficial de Getting Started do OpenFGA apresenta um fluxo composto por store, authorization model, relationship tuples e checks. O projeto é inspirado no sistema Google Zanzibar, criado para autorização em grande escala.
OpenFGA não autentica usuários. Ele responde perguntas de autorização, como:
O usuário user:ana pode viewer o document:relatorio?A aplicação continua responsável por autenticação, sessão, JWT, validação de tenant e contexto da requisição.
Conceitos principais
- Store: espaço isolado que contém modelos e relações.
- Authorization model: tipos, relações e regras.
- Relationship tuple: fato como “ana é editora do projeto 42”.
- Check: pergunta se uma relação é válida.
- ListObjects: lista recursos que o usuário pode acessar.
- ListUsers: lista usuários relacionados a um recurso.
Executando localmente
Uma forma simples é usar Docker:
docker run --rm \
--name openfga \
-p 8080:8080 \
-p 3000:3000 \
openfga/openfga runFixe uma versão específica em ambientes reais. Imagens com latest podem mudar sem revisão. A porta 8080 expõe a API HTTP e a interface de playground pode variar conforme a configuração.
Instalando o SDK
npm install @openfga/sdkCrie um cliente administrativo temporário para preparar a store:
import { OpenFgaApi } from '@openfga/sdk';
const adminClient = new OpenFgaApi({
apiUrl: process.env.OPENFGA_API_URL ?? 'http://localhost:8080'
});Em produção, configure autenticação do servidor OpenFGA e credenciais com escopo mínimo.
Criando uma store
const { id: storeId } = await adminClient.createStore({
name: 'codigo-facil-saas'
});
console.log({ storeId });Normalmente a store é criada por infraestrutura ou pipeline, não durante cada inicialização da API. Guarde o ID em configuração segura.
Primeiro modelo de autorização
Um SaaS com organizações, projetos e documentos pode usar:
model
schema 1.1
type user
type organization
relations
define member: [user]
define admin: [user]
type project
relations
define organization: [organization]
define owner: [user]
define editor: [user, organization#admin]
define viewer: [user, organization#member] or editor or owner
type document
relations
define parent: [project]
define owner: [user]
define editor: [user] or editor from parent or owner
define viewer: [user] or viewer from parent or editorO modelo expressa herança: quem pode editar o projeto também pode editar documentos relacionados, conforme a regra.
Publicando o modelo
O SDK aceita o modelo transformado para JSON. Em projetos reais, mantenha o arquivo versionado e use ferramentas oficiais para validar e transformar.
const response = await adminClient.writeAuthorizationModel({
storeId,
typeDefinitions: authorizationModel.typeDefinitions,
schemaVersion: '1.1'
});
const authorizationModelId = response.authorizationModelId;Modelos são imutáveis. Uma mudança cria um novo ID, o que facilita rollout e rollback.
Configurando o cliente da aplicação
const fga = new OpenFgaApi({
apiUrl: process.env.OPENFGA_API_URL,
storeId: process.env.OPENFGA_STORE_ID,
authorizationModelId: process.env.OPENFGA_MODEL_ID
});Não busque automaticamente “o último modelo” em cada requisição. Fixe o model ID implantado junto com a aplicação.
Relationship tuples
Uma tuple tem usuário, relação e objeto:
{
"user": "user:ana",
"relation": "member",
"object": "organization:acme"
}Outro exemplo:
{
"user": "organization:acme",
"relation": "organization",
"object": "project:payments"
}Gravando relações
await fga.write({
writes: [
{
user: 'user:ana',
relation: 'member',
object: 'organization:acme'
},
{
user: 'user:bruno',
relation: 'owner',
object: 'project:payments'
},
{
user: 'project:payments',
relation: 'parent',
object: 'document:architecture'
}
]
});Use IDs canônicos e previsíveis, mas não inclua dados pessoais diretamente no identificador.
Removendo relações
await fga.write({
deletes: [
{
user: 'user:ana',
relation: 'member',
object: 'organization:acme'
}
]
});Remoções devem acompanhar a transação de negócio. Quando o banco e OpenFGA não compartilham uma transação, use Outbox Pattern e reconciliação.
Realizando um check
const result = await fga.check({
user: 'user:ana',
relation: 'viewer',
object: 'document:architecture'
});
if (!result.allowed) {
throw new ForbiddenError();
}O backend deriva user:ana da sessão autenticada. Nunca aceite o user ID do corpo da requisição para decidir acesso.
Integração com Express
function requireRelation(relation, objectFromRequest) {
return async function authorizationMiddleware(req, res, next) {
try {
const object = objectFromRequest(req);
const user = `user:${req.auth.userId}`;
const { allowed } = await fga.check({
user,
relation,
object
});
if (!allowed) {
return res.status(403).json({
type: 'https://api.example.com/problems/forbidden',
title: 'Acesso negado',
status: 403
});
}
next();
} catch (error) {
next(error);
}
};
}
app.get(
'/documents/:id',
requireRelation('viewer', req => `document:${req.params.id}`),
getDocument
);Para respostas padronizadas, veja Problem Details no Node.js.
Autorização não substitui filtro de dados
Mesmo depois do check, carregue o documento dentro do tenant correto. OpenFGA responde sobre relação; o banco continua aplicando isolamento e integridade.
Consulte Multi-Tenancy no Node.js e Row-Level Security no Node.js.
ListObjects
Para listar projetos que Ana pode visualizar:
const response = await fga.listObjects({
user: 'user:ana',
relation: 'viewer',
type: 'project'
});
const projectIds = response.objects.map(object =>
object.replace('project:', '')
);Depois consulte o banco pelos IDs e aplique paginação, tenant e filtros. Não retorne recursos apenas porque apareceram no resultado sem validar o contexto atual.
ListUsers
Para descobrir quem pode visualizar um documento:
const response = await fga.listUsers({
object: {
type: 'document',
id: 'architecture'
},
relation: 'viewer',
userFilters: [{ type: 'user' }]
});Esse recurso é útil para telas de compartilhamento, auditoria e administração.
Contextual tuples
Relações temporárias podem ser enviadas apenas no check:
await fga.check({
user: 'user:ana',
relation: 'viewer',
object: 'document:preview',
contextualTuples: {
tupleKeys: [{
user: 'user:ana',
relation: 'viewer',
object: 'document:preview'
}]
}
});Use para contexto efêmero e controlado. Não permita que o cliente invente tuples que concedam acesso.
RBAC, ABAC e ReBAC
RBAC usa papéis, como admin e editor. ABAC usa atributos, como departamento, horário ou classificação. ReBAC usa relações entre entidades.
OpenFGA pode representar papéis por relações e combinar com conditions quando a versão e o modelo suportarem. Consulte RBAC no Node.js e ABAC no Node.js.
Consistência
Após escrever uma tuple, um check imediatamente seguinte precisa de uma estratégia de consistência adequada. O SDK e a API oferecem preferências de consistência conforme a versão. Use maior consistência apenas nos fluxos que realmente precisam, pois pode aumentar latência.
Cache de decisões
Checks repetidos podem ser cacheados por poucos segundos, mas a chave deve incluir:
- user;
- relation;
- object;
- authorization model ID;
- tenant ou contexto relevante.
Revogações exigem invalidação. Para permissões críticas, prefira check direto.
Timeout e falha fechada
const result = await Promise.race([
fga.check(request),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('OpenFGA timeout')), 1000)
)
]);Quando o serviço de autorização está indisponível, operações sensíveis devem falhar fechadas. Para conteúdo público, uma política de fallback pode ser explícita.
Sincronização com o banco
Ao criar um projeto:
- grave o projeto e uma outbox na mesma transação;
- o worker envia tuples ao OpenFGA;
- registre sucesso;
- retries são idempotentes;
- um job de reconciliação compara estados.
Veja Outbox Pattern no Node.js.
Testes do modelo
Mantenha casos permitidos e negados:
test('membro visualiza projeto da organização', async () => {
const { allowed } = await fga.check({
user: 'user:ana',
relation: 'viewer',
object: 'project:payments'
});
assert.equal(allowed, true);
});
test('usuário externo não edita documento', async () => {
const { allowed } = await fga.check({
user: 'user:carlos',
relation: 'editor',
object: 'document:architecture'
});
assert.equal(allowed, false);
});Model tests
Use ferramentas oficiais para testar o modelo sem depender apenas de testes da aplicação. Cada mudança precisa cobrir concessões, negações e regressões de privilégio.
Observabilidade
Monitore:
- latência dos checks;
- taxa de allowed e denied;
- timeouts;
- falhas de escrita;
- tuples pendentes na outbox;
- model ID em uso;
- ListObjects lentos;
- reconciliações divergentes.
Não use user IDs como labels de métricas.
Auditoria
Registre alterações de relações com ator, origem, recurso e motivo. Não registre tokens ou dados sensíveis. Consulte Logs de Auditoria no Node.js.
Erros comuns
- Confiar no user enviado: permite verificar outra identidade.
- Modelo sem testes: uma mudança amplia acesso.
- Buscar último model automaticamente: rollout fica imprevisível.
- OpenFGA como autenticação: sessão continua sem validação.
- Sem outbox: banco e tuples divergem.
- Cache longo: revogação demora.
- Fail-open: indisponibilidade concede acesso.
- ListObjects sem filtros: tenant ou status são ignorados.
Conclusão
O OpenFGA no Node.js centraliza autorização baseada em relações e torna regras complexas explícitas. Stores, modelos, tuples e checks permitem representar ownership, grupos, herança e compartilhamento sem condicionais espalhadas.
Derive a identidade da sessão, fixe o model ID, sincronize tuples com Outbox e teste permissões positivas e negativas. Combine OpenFGA com isolamento no banco, logs de auditoria e falha fechada. Assim, autorização evolui como um contrato versionado, não como uma coleção de exceções no código.


