Modern Agent Engineering

OpenFGA para permisos granulares en aplicaciones modernas, MCP y flujos de trabajo con agentes

Una guía práctica para usar OpenFGA en la autorización granular de aplicaciones SaaS, servidores MCP, agentes de workflows y orquestadores de agentes.

36 min de lectura Actualizado el 8 may 2026

TL;DR

  • OpenFGA es un motor de autorización de código abierto inspirado en el paper Zanzibar de Google. Responde una pregunta pequeña pero poderosa: «¿Puede este usuario, servicio o agente ejercer esta relación sobre este objeto?».
  • En lugar de ocultar los permisos dentro del código de la aplicación, OpenFGA te permite modelar usuarios, equipos, organizaciones, recursos, roles, herencia, uso compartido y delegación como relaciones.
  • En las aplicaciones web modernas, OpenFGA funciona mejor como una capa de autorización del lado del servidor, detrás de las rutas de tu API, loaders, acciones de servidor, resolvers de GraphQL y trabajos en segundo plano.
  • Para servidores MCP y herramientas de agentes, OpenFGA es especialmente útil porque el acceso a una herramienta rara vez se reduce a «¿inició sesión?». Por lo general, la pregunta es «¿puede este usuario usar esta herramienta en este workspace, proyecto, repositorio, ticket, cliente o documento?».
  • Para agentes de workflows y orquestadores, OpenFGA puede separar a la persona que inició el trabajo, la identidad del agente que lo realiza, la tarea que se ejecuta y el recurso sobre el que se actúa.
  • No uses OpenFGA como reemplazo de la autenticación, la seguridad a nivel de fila de la base de datos, los registros de auditoría o la aprobación humana. Úsalo como el cerebro de políticas que consultan esas capas.

La autorización se complica justo cuando tu producto empieza a ser útil.

Al principio, «admin» y «member» parecen suficientes. Luego los clientes piden workspaces, equipos, invitados, roles por proyecto, documentos compartidos, usuarios con acceso exclusivo a facturación, suplantación de usuarios por parte de soporte, API keys de solo lectura, identidades de automatización, herramientas MCP y agentes de IA que puedan actuar en nombre de personas.

De pronto, la pregunta aparentemente inocente «¿puede esta persona hacer clic en este botón?» se convierte en:

Can Luis approve this deployment
inside Acme's production project
through this workflow agent
after the ticket has been approved
but without letting the agent read billing data?

Ese es el mundo para el que se creó OpenFGA.

Qué aprenderás aquí

  • El modelo mental detrás de OpenFGA y el control de acceso basado en relaciones
  • Cómo diseñar tu primer modelo de autorización
  • Cómo usar OpenFGA desde una aplicación web moderna
  • Dónde encaja OpenFGA en un stack con OAuth, bases de datos, MCP y herramientas de agentes
  • Cómo modelar permisos de herramientas MCP, acceso a documentos para RAG, agentes de workflows y delegación limitada a tareas
  • Los aspectos de producción que aún necesitas cubrir en torno a caché, auditoría, migraciones, pruebas y aprobaciones

OpenFGA en una frase

OpenFGA almacena relaciones, evalúa un modelo de autorización y le dice a tu aplicación si un sujeto tiene un permiso sobre un objeto.

Suena sencillo. No lo es.

El objeto puede ser un documento, proyecto, workspace, repositorio, cliente, factura, herramienta, tarea, workflow, entorno o registro de memoria.

El sujeto puede ser un usuario humano, un equipo, una organización, una cuenta de servicio, una API key, un cliente MCP o un agente.

El permiso puede ser directo, heredado, calculado, delegado o condicional.

Subject              Relation              Object
-------              --------              ------
user:luis     --->   owner          --->   organization:acme
team:eng      --->   member         --->   organization:acme
user:ana      --->   viewer         --->   document:roadmap
agent:triage  --->   can_read       --->   ticket:123
tool:refund   --->   available_in   --->   workspace:acme

La aplicación pregunta:

Can user:luis view document:roadmap?
Can agent:triage summarize ticket:123?
Can user:ana invoke tool:refund in workspace:acme?

OpenFGA responde allowed: true o allowed: false.

Por qué importa ahora

La autorización granular solía ser un «problema de las grandes empresas SaaS». Ahora aparece mucho antes porque las aplicaciones son más colaborativas y los agentes interactúan con sistemas reales.

Un producto moderno puede tener:

  • dashboards para usuarios
  • API keys
  • webhooks
  • trabajos en segundo plano
  • consolas de administración
  • copilotos integrados
  • servidores MCP
  • agentes de workflows
  • conectores de datos
  • RAG sobre documentos de clientes
  • pasos de aprobación humana

Cada superficie necesita autorización, pero no siempre del mismo tipo.

La autenticación responde:

Who are you?

Los scopes de OAuth suelen responder:

What broad category of access did you request?

OpenFGA responde:

Given this exact user, resource, relation, and context, is this action allowed?

En esa última pregunta es donde reside la mayor parte de la complejidad del producto.

Los conceptos fundamentales

OpenFGA tiene cuatro conceptos que necesitas interiorizar.

ConceptoSignificado sencilloEjemplo
TipoUna clase de entidad en tu sistemauser, organization, project, document
RelaciónUna relación o permiso con nombreowner, member, viewer, can_edit
TuplaUn hecho almacenado sobre una relaciónuser:luis es owner de organization:acme
CheckUna pregunta sobre permisos¿Puede user:luis ejercer can_edit sobre document:roadmap?

El modelo de autorización define qué relaciones existen y cómo se combinan.

Las tuplas de relaciones son los datos.

Tu aplicación realiza checks en tiempo de ejecución.

Application code
   |
   | "Can user:123 can_edit document:456?"
   v
OpenFGA
   |
   | evaluates model + tuples + optional context
   v
allowed true/false

Un primer modelo: organizaciones, proyectos y documentos

Modelemos una aplicación SaaS común:

  • Las organizaciones contienen proyectos.
  • Los proyectos contienen documentos.
  • Los administradores de la organización pueden gestionar todo lo que pertenece a ella.
  • Los editores de proyectos pueden editar los documentos del proyecto.
  • Los usuarios con acceso de lectura al proyecto pueden ver sus documentos.
  • Un documento también se puede compartir directamente con un usuario.

Un modelo de OpenFGA puede verse así:

model
  schema 1.1

type user

type organization
  relations
    define admin: [user]
    define member: [user]

type project
  relations
    define parent: [organization]
    define owner: [user] or admin from parent
    define editor: [user] or owner
    define viewer: [user] or editor or member from parent
    define can_manage: owner
    define can_edit: editor
    define can_view: viewer

type document
  relations
    define parent: [project]
    define direct_viewer: [user]
    define direct_editor: [user]
    define can_edit: direct_editor or can_edit from parent
    define can_view: direct_viewer or direct_editor or can_view from parent

Ese modelo compacto incorpora muchas reglas de comportamiento del producto:

  • los administradores de la organización heredan la propiedad de los proyectos
  • los propietarios de proyectos pueden editar
  • los editores de proyectos pueden ver
  • los miembros de la organización pueden ver los proyectos
  • los documentos heredan permisos del proyecto
  • los documentos también se pueden compartir directamente

Ahora agreguemos tuplas de relaciones:

organization:acme#admin@user:luis
organization:acme#member@user:ana
project:agent-platform#parent@organization:acme
project:agent-platform#editor@user:maya
document:roadmap#parent@project:agent-platform
document:roadmap#direct_viewer@user:guest-42

Lo interesante es lo que no tienes que almacenar.

No necesitas almacenar user:luis can_edit document:roadmap. Eso se infiere a partir de:

Luis is admin of organization:acme
project:agent-platform belongs to organization:acme
document:roadmap belongs to project:agent-platform
project admins can edit project documents

Esta es la ventaja principal: almacenas hechos duraderos sobre relaciones y el motor de autorización deriva los permisos a partir del modelo.

Cómo encaja esto en una aplicación web

OpenFGA debe estar detrás de la frontera del servidor.

No permitas que el navegador consulte OpenFGA directamente. El navegador no es confiable. Tu API, acción de servidor, manejador de ruta o servicio de backend debe autenticar al usuario, asociar la acción de la aplicación con una relación de OpenFGA, ejecutar el check y solo entonces realizar la acción.

Browser
   |
   | request with session/JWT
   v
Web app server
   |
   | authenticate user
   | map route to relation
   | check OpenFGA
   v
Business logic
   |
   | query/update database
   v
Response

Una pequeña función auxiliar en TypeScript podría verse así:

import { OpenFgaClient } from "@openfga/sdk";

const fga = new OpenFgaClient({
  apiUrl: process.env.FGA_API_URL!,
  storeId: process.env.FGA_STORE_ID!,
  authorizationModelId: process.env.FGA_MODEL_ID!,
});

type CheckInput = {
  userId: string;
  relation: string;
  object: string;
};

export async function assertAllowed(input: CheckInput) {
  const result = await fga.check({
    user: `user:${input.userId}`,
    relation: input.relation,
    object: input.object,
  });

  if (!result.allowed) {
    throw new Response("Forbidden", { status: 403 });
  }
}

Úsala en la frontera del servidor:

export async function updateDocumentTitle(request: Request, documentId: string) {
  const session = await requireSession(request);

  await assertAllowed({
    userId: session.user.id,
    relation: "can_edit",
    object: `document:${documentId}`,
  });

  return db.document.update({
    where: { id: documentId },
    data: { title: await readTitle(request) },
  });
}

La interfaz de usuario puede usar checks de permisos para ocultar o deshabilitar botones, pero eso solo mejora la experiencia de uso. El check del servidor es la frontera de seguridad.

Check, Batch Check y List Objects

La mayor parte del trabajo de integración con OpenFGA se reduce a tres tipos de consulta.

Check

Usa Check cuando una solicitud necesita una decisión.

Can user:luis can_view document:roadmap?

Es útil para:

  • abrir un documento
  • editar una configuración de proyecto
  • invocar una herramienta MCP
  • aprobar un paso de un workflow

Batch Check

Usa batch checks cuando una pantalla necesita muchas decisiones.

Can user:luis can_edit document:1?
Can user:luis can_delete document:1?
Can user:luis can_invite project:alpha?
Can user:luis can_export project:alpha?

Es útil para:

  • renderizar menús de acciones
  • determinar los controles de funcionalidades
  • verificar varios permisos de herramientas a la vez

List Objects

Usa list objects cuando necesites preguntar:

Which documents can user:luis view?

Es útil para:

  • selectores de recursos
  • filtros de búsqueda
  • listas de documentos
  • filtros de recuperación para agentes

Ten cuidado con los endpoints de listado en tenants grandes. Según los requisitos de escala y latencia, seguirás necesitando filtros de base de datos, paginación, índices de búsqueda y, en ocasiones, índices de permisos desnormalizados.

Diseño concreto: una mini aplicación de Google Docs

Hagamos esto menos abstracto.

Imagina una aplicación sencilla de documentos colaborativos. No un clon completo de Google Docs, pero sí lo bastante cercana para mostrar las capas reales de permisos:

  • los usuarios pertenecen a organizaciones
  • los usuarios crean documentos
  • los propietarios de documentos pueden compartirlos con lectores, comentaristas y editores
  • los administradores de la organización pueden gestionar los documentos de la organización
  • las API keys pueden leer o editar documentos seleccionados
  • la UI debe ocultar las acciones que el usuario no puede realizar
  • la API debe hacer cumplir los permisos incluso si alguien la llama directamente
  • la base de datos sigue siendo la fuente del contenido de los documentos
  • OpenFGA sigue siendo la fuente de los permisos basados en relaciones

La aplicación tiene cuatro fuentes de datos importantes:

Fuente de datosQué se almacena allí
Proveedor de autenticaciónidentidad del usuario, sesiones, JWTs
Postgresorganizaciones, usuarios, documentos, revisiones de documentos, comentarios, API keys, eventos de auditoría
OpenFGArelaciones: propietario, editor, comentarista, lector, administrador de la organización, acceso de API keys
Almacenamiento de objetos o índice de búsquedaexportaciones opcionales, archivos adjuntos, búsqueda de texto completo, embeddings

El recorrido de la solicitud se ve así:

Browser / API client
   |
   | session cookie or API key
   v
App API
   |
   | authenticate principal
   | user:luis or api_key:docs-importer
   v
Authorization middleware
   |
   | OpenFGA check
   | principal can_edit document:doc_123
   v
Business logic
   |
   | read/write Postgres
   | write audit event
   v
Response

La separación importante:

Postgres stores document state.
OpenFGA stores who can do what.
The API is the only place where both are joined.

Permisos del producto

Para una aplicación de documentos, una matriz práctica de permisos podría verse así:

Rol o relaciónVerComentarEditarCompartirEliminar
Administrador de la organizaciónsísísísísí
Propietario del documentosísísísísí
Editor del documentosísísínono
Comentarista del documentosísínonono
Lector del documentosínononono
API key de lecturasínononono
API key de ediciónsínosínono

Esto ya es más granular que «admin/member». También establece una decisión útil de producto: las API keys pueden leer y editar documentos, pero no compartirlos ni eliminarlos.

Ese límite importa. Las API keys suelen durar mucho tiempo, se copian en sistemas de CI y las usan scripts. No deberían heredar todas las facultades de un propietario humano.

Modelo de OpenFGA

Aquí tienes un modelo compacto para la aplicación:

model
  schema 1.1

type user

type api_key

type organization
  relations
    define admin: [user]
    define member: [user]

type document
  relations
    define parent: [organization]
    define owner: [user]
    define editor: [user, api_key]
    define commenter: [user]
    define viewer: [user, api_key]

    define can_view: viewer or commenter or editor or owner or admin from parent
    define can_comment: commenter or editor or owner or admin from parent
    define can_edit: editor or owner or admin from parent
    define can_share: owner or admin from parent
    define can_delete: owner or admin from parent

Ahora carguemos las relaciones iniciales:

organization:acme#admin@user:luis
organization:acme#member@user:ana
organization:acme#member@user:maya

document:roadmap#parent@organization:acme
document:roadmap#owner@user:luis
document:roadmap#editor@user:maya
document:roadmap#commenter@user:ana
document:roadmap#viewer@api_key:docs-importer

document:release-notes#parent@organization:acme
document:release-notes#owner@user:maya
document:release-notes#editor@api_key:release-bot

Esas tuplas producen estos resultados:

user:luis can_share document:roadmap        true
user:maya can_edit document:roadmap         true
user:ana can_comment document:roadmap       true
user:ana can_edit document:roadmap          false
api_key:docs-importer can_view roadmap      true
api_key:docs-importer can_delete roadmap    false
api_key:release-bot can_edit release-notes  true
api_key:release-bot can_share release-notes false

Esa es la estructura que buscas: personas, API keys y permisos derivados usan el mismo lenguaje de autorización.

Esquema de Postgres

OpenFGA no almacena el contenido de tus documentos. Mantenlo en la base de datos de tu aplicación.

create table organizations (
  id text primary key,
  name text not null,
  created_at timestamptz not null default now()
);

create table users (
  id text primary key,
  email text not null unique,
  name text not null,
  created_at timestamptz not null default now()
);

create table documents (
  id text primary key,
  organization_id text not null references organizations(id),
  title text not null,
  body jsonb not null default '{}'::jsonb,
  created_by text not null references users(id),
  updated_at timestamptz not null default now(),
  created_at timestamptz not null default now()
);

create table document_revisions (
  id bigserial primary key,
  document_id text not null references documents(id),
  actor_type text not null check (actor_type in ('user', 'api_key')),
  actor_id text not null,
  body jsonb not null,
  created_at timestamptz not null default now()
);

create table comments (
  id text primary key,
  document_id text not null references documents(id),
  author_id text not null references users(id),
  body text not null,
  created_at timestamptz not null default now()
);

create table api_keys (
  id text primary key,
  organization_id text not null references organizations(id),
  name text not null,
  token_hash text not null,
  created_by text not null references users(id),
  revoked_at timestamptz
);

create table audit_events (
  id bigserial primary key,
  actor_type text not null,
  actor_id text not null,
  action text not null,
  resource_type text not null,
  resource_id text not null,
  allowed boolean not null,
  metadata jsonb not null default '{}'::jsonb,
  created_at timestamptz not null default now()
);

Vale la pena destacar dos tablas.

document_revisions registra el historial de contenido. Eso es estado de la aplicación.

audit_events registra acciones sensibles desde el punto de vista de los permisos. Eso permite rendir cuentas.

OpenFGA sigue siendo el motor de permisos, pero tu producto debería poder responder más adelante:

Who edited this document?
Was it a user or API key?
Which permission check allowed it?
Which request or workflow caused it?

Mapa de permisos a nivel de API

Antes de programar el middleware, escribe el mapa de la API.

Ruta de APIPrincipalCheck de OpenFGA
GET /api/documents/:idusuario o API keycan_view sobre document:id
PATCH /api/documents/:idusuario o API keycan_edit sobre document:id
POST /api/documents/:id/commentssolo usuariocan_comment sobre document:id
POST /api/documents/:id/sharesolo usuariocan_share sobre document:id
DELETE /api/documents/:idsolo usuariocan_delete sobre document:id
GET /api/documentsusuario o API keylist_objects para documentos visibles, más un filtro de base de datos

Este mapa es aburrido en el mejor sentido posible. Convierte la autorización en un contrato explícito en lugar de lógica dispersa entre rutas.

Middleware de API en TypeScript

Aquí tienes una implementación mínima del lado del servidor con una API al estilo de Express. El mismo patrón se aplica a los manejadores de rutas de Next.js, loaders/actions de Remix, endpoints de servidor de Astro, Fastify, Hono o NestJS.

import crypto from "node:crypto";
import { OpenFgaClient } from "@openfga/sdk";
import type { Request, Response, NextFunction } from "express";

const fga = new OpenFgaClient({
  apiUrl: process.env.FGA_API_URL!,
  storeId: process.env.FGA_STORE_ID!,
  authorizationModelId: process.env.FGA_MODEL_ID!,
});

type Principal =
  | { type: "user"; id: string; fgaUser: `user:${string}` }
  | { type: "api_key"; id: string; fgaUser: `api_key:${string}` };

declare global {
  namespace Express {
    interface Request {
      principal?: Principal;
    }
  }
}

async function authenticate(req: Request, _res: Response, next: NextFunction) {
  const apiKey = req.header("x-api-key");

  if (apiKey) {
    const key = await findApiKeyByHash(hashToken(apiKey));
    if (!key || key.revoked_at) throw forbidden("Invalid API key");

    req.principal = {
      type: "api_key",
      id: key.id,
      fgaUser: `api_key:${key.id}`,
    };
    return next();
  }

  const session = await requireSession(req);
  req.principal = {
    type: "user",
    id: session.user.id,
    fgaUser: `user:${session.user.id}`,
  };
  return next();
}

function requirePrincipal(req: Request): Principal {
  if (!req.principal) throw forbidden("Missing principal");
  return req.principal;
}

async function assertDocumentAccess(
  req: Request,
  documentId: string,
  relation: "can_view" | "can_comment" | "can_edit" | "can_share" | "can_delete",
) {
  const principal = requirePrincipal(req);

  const result = await fga.check({
    user: principal.fgaUser,
    relation,
    object: `document:${documentId}`,
  });

  await insertAuditEvent({
    actorType: principal.type,
    actorId: principal.id,
    action: relation,
    resourceType: "document",
    resourceId: documentId,
    allowed: Boolean(result.allowed),
  });

  if (!result.allowed) throw forbidden("Forbidden");
}

function hashToken(token: string) {
  return crypto.createHash("sha256").update(token).digest("hex");
}

function forbidden(message: string) {
  return Object.assign(new Error(message), { status: 403 });
}

Este es el patrón principal de la aplicación:

authenticate once
represent principal as OpenFGA subject
map route to relation
check OpenFGA before touching protected data
write audit event
execute business logic

APIs de documentos

Ahora los manejadores de rutas quedan claros.

app.get("/api/documents/:id", authenticate, async (req, res) => {
  const documentId = req.params.id;

  await assertDocumentAccess(req, documentId, "can_view");

  const document = await db.document.findUniqueOrThrow({
    where: { id: documentId },
  });

  res.json(document);
});

app.patch("/api/documents/:id", authenticate, async (req, res) => {
  const documentId = req.params.id;

  await assertDocumentAccess(req, documentId, "can_edit");

  const updated = await db.$transaction(async (tx) => {
    const document = await tx.document.update({
      where: { id: documentId },
      data: {
        title: req.body.title,
        body: req.body.body,
        updated_at: new Date(),
      },
    });

    await tx.documentRevision.create({
      data: {
        document_id: documentId,
        actor_type: req.principal!.type,
        actor_id: req.principal!.id,
        body: req.body.body,
      },
    });

    return document;
  });

  res.json(updated);
});

app.post("/api/documents/:id/comments", authenticate, async (req, res) => {
  if (req.principal?.type !== "user") {
    throw forbidden("API keys cannot comment");
  }

  const documentId = req.params.id;

  await assertDocumentAccess(req, documentId, "can_comment");

  const comment = await db.comment.create({
    data: {
      id: crypto.randomUUID(),
      document_id: documentId,
      author_id: req.principal.id,
      body: req.body.body,
    },
  });

  res.status(201).json(comment);
});

La regla de las API keys es intencional. Aunque una API key tuviera can_comment de alguna manera, la ruta la rechaza porque la política del producto establece que los comentarios deben ser escritos por personas.

Esto es normal: OpenFGA responde sobre la autorización basada en relaciones, mientras que el código de la aplicación sigue aplicando las reglas específicas del producto sobre cómo se realiza una acción.

API para compartir

Al compartir es cuando se escriben relaciones.

Solo las personas con can_share deberían crear tuplas de relaciones para otras personas o API keys.

app.post("/api/documents/:id/share", authenticate, async (req, res) => {
  if (req.principal?.type !== "user") {
    throw forbidden("API keys cannot share documents");
  }

  const documentId = req.params.id;

  await assertDocumentAccess(req, documentId, "can_share");

  const { targetType, targetId, role } = req.body as {
    targetType: "user" | "api_key";
    targetId: string;
    role: "viewer" | "commenter" | "editor";
  };

  if (targetType === "api_key" && role === "commenter") {
    throw forbidden("API keys cannot receive commenter access");
  }

  await fga.write({
    writes: [
      {
        user: `${targetType}:${targetId}`,
        relation: role,
        object: `document:${documentId}`,
      },
    ],
  });

  await insertAuditEvent({
    actorType: req.principal.type,
    actorId: req.principal.id,
    action: `share:${role}`,
    resourceType: "document",
    resourceId: documentId,
    allowed: true,
    metadata: { targetType, targetId },
  });

  res.status(204).send();
});

Este es un buen lugar para agregar medidas de protección adicionales:

  • permitir compartir solo dentro de la misma organización
  • requerir confirmación antes de compartir con API keys
  • limitar el acceso de edición de las API keys a documentos que pertenecen a la integración
  • notificar a los propietarios cuando un documento se comparte externamente
  • registrar un evento de auditoría por cada cambio de tupla

Listado de documentos

El listado de documentos es donde los equipos suelen encontrarse con dificultades.

Para tenants pequeños, puedes preguntarle a OpenFGA qué documentos puede ver un principal y luego obtener esos documentos de Postgres.

app.get("/api/documents", authenticate, async (req, res) => {
  const principal = requirePrincipal(req);

  const visible = await fga.listObjects({
    user: principal.fgaUser,
    relation: "can_view",
    type: "document",
  });

  const documentIds = visible.objects.map((object) =>
    object.replace("document:", ""),
  );

  const documents = await db.document.findMany({
    where: { id: { in: documentIds } },
    orderBy: { updated_at: "desc" },
    take: 50,
  });

  res.json(documents);
});

Para tenants grandes, evita convertir cada carga de página en una consulta de autorización enorme. Algunas opciones comunes son:

  • consultar primero por organización y luego ejecutar batch checks sobre los candidatos
  • mantener un índice de búsqueda con IDs de documentos e IDs de tenants
  • desnormalizar permisos seleccionados para obtener listas rápidas, manteniendo OpenFGA como fuente de verdad
  • almacenar en caché los checks de bajo riesgo durante períodos cortos
  • usar trabajos en segundo plano para actualizar índices después de cambios en las tuplas

La regla de seguridad sigue siendo la misma: nunca devuelvas un documento a menos que el principal actual pueda verlo.

Checks de permisos en la UI

La UI no debería adivinar. Debería consultar a la API qué acciones están permitidas.

app.get("/api/documents/:id/capabilities", authenticate, async (req, res) => {
  const principal = requirePrincipal(req);
  const documentId = req.params.id;

  const checks = await Promise.all(
    ["can_view", "can_comment", "can_edit", "can_share", "can_delete"].map(
      async (relation) => {
        const result = await fga.check({
          user: principal.fgaUser,
          relation,
          object: `document:${documentId}`,
        });

        return [relation, Boolean(result.allowed)] as const;
      },
    ),
  );

  res.json(Object.fromEntries(checks));
});

El frontend puede renderizar:

export function DocumentToolbar({ capabilities }: { capabilities: Capabilities }) {
  return (
    <div className="toolbar">
      <button disabled={!capabilities.can_edit}>Save</button>
      <button disabled={!capabilities.can_comment}>Comment</button>
      <button disabled={!capabilities.can_share}>Share</button>
      <button disabled={!capabilities.can_delete}>Delete</button>
    </div>
  );
}

Esto mejora la UX, pero los checks de la API siguen siendo obligatorios. Un botón deshabilitado no es seguridad.

Estructura de despliegue

Para un despliegue real, mantén las capas separadas:

CDN / edge
   |
   v
Web app container
   |
   | sessions/JWTs
   v
Auth provider

Web app container
   |
   | SQL
   v
Postgres

Web app container
   |
   | authorization checks + tuple writes
   v
OpenFGA
   |
   | stores OpenFGA state
   v
OpenFGA datastore

Worker container
   |
   | background indexing, exports, notifications
   v
Postgres + OpenFGA + object storage

Una configuración local de Docker Compose puede verse así:

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://app:app@postgres:5432/docs
      FGA_API_URL: http://openfga:8080
      FGA_STORE_ID: ${FGA_STORE_ID}
      FGA_MODEL_ID: ${FGA_MODEL_ID}
    depends_on:
      - postgres
      - openfga

  postgres:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: app
      POSTGRES_DB: docs
    ports:
      - "5432:5432"
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d docs"]
      interval: 5s
      timeout: 5s
      retries: 10

  openfga-migrate:
    image: openfga/openfga:latest
    command: migrate
    environment:
      OPENFGA_DATASTORE_ENGINE: postgres
      OPENFGA_DATASTORE_URI: postgres://app:app@postgres:5432/docs?sslmode=disable
    depends_on:
      postgres:
        condition: service_healthy

  openfga:
    image: openfga/openfga:latest
    command: run
    environment:
      OPENFGA_DATASTORE_ENGINE: postgres
      OPENFGA_DATASTORE_URI: postgres://app:app@postgres:5432/docs?sslmode=disable
    ports:
      - "8080:8080"
      - "8081:8081"
    depends_on:
      postgres:
        condition: service_healthy
      openfga-migrate:
        condition: service_completed_successfully

volumes:
  postgres-data:

En producción, normalmente separaría la base de datos de la aplicación y el almacén de datos de OpenFGA, incluso si en desarrollo local comparten Postgres por comodidad. Eso simplifica los respaldos, el escalado, las migraciones y el control de acceso.

Lista de verificación de despliegue para la mini aplicación de documentos

Para desplegar esto de forma segura, necesitas más que contenedores.

CapaRequisito de producción
Autenticaciónsesiones o JWTs verificados, IDs de usuario estables, sincronización de membresía en organizaciones
API de la aplicaciónmapa de rutas a permisos, middleware de autorización, errores estructurados
Postgresmigraciones, respaldos, filtros por tenant, RLS opcional, historial de revisiones
OpenFGAproceso de migración del modelo, servicio de escritura de tuplas, checks de salud
Secretoshash de API keys, rotación de secretos, ningún token en texto plano en los logs
Auditoríaescritura de tuplas, checks denegados, escritura de documentos, documentos compartidos y eliminaciones
UIendpoint de capacidades, acciones deshabilitadas, estados de uso compartido claros
Workerslas mismas funciones auxiliares de autorización que las rutas de API, sin atajos privilegiados
Observabilidadlatencia de checks, tasa de denegación, fallas al escribir tuplas, cobertura de auditoría

La versión simple se puede programar en un fin de semana. La versión de producción requiere disciplina en los puntos donde se encuentran la identidad, los permisos y los datos.

El patrón vale la pena porque el mismo diseño crece de forma natural:

documents today
folders tomorrow
workspaces next month
MCP tools after that
workflow agents later

No necesitas descartar el modelo cuando el producto se vuelve más colaborativo.

Las preguntas para modelar el producto

La parte más difícil de OpenFGA no es el SDK. Es el modelo.

Haz estas preguntas antes de escribir código:

  1. ¿Cuáles son los objetos sobre los que actúan los usuarios?
  2. ¿Qué relaciones son hechos duraderos?
  3. ¿Qué permisos se derivan de otras relaciones?
  4. ¿Qué permisos son globales, por tenant, por proyecto o por recurso?
  5. ¿Se pueden delegar permisos a equipos, grupos, cuentas de servicio o agentes?
  6. ¿Alguna decisión requiere contexto de la solicitud, como hora, IP, entorno, riesgo o estado de aprobación?
  7. ¿Qué checks deben tener una latencia lo suficientemente baja para renderizar páginas?
  8. ¿Qué checks deben ser auditables porque afectan dinero, exportaciones de datos, producción o clientes?

Esto normalmente produce mejores modelos que empezar por los roles.

Los roles siguen siendo útiles, pero deberían describir relaciones con objetos:

user:luis is owner of organization:acme
user:maya is editor of project:agent-platform
team:support is viewer of workspace:acme
agent:deploy-bot is operator of environment:staging

Eso es más flexible que:

user:luis has role ADMIN

Porque la siguiente pregunta es obvia: ¿administrador de qué?

Dónde termina OpenFGA

OpenFGA no es todo tu sistema de seguridad.

No reemplaza:

  • el inicio de sesión y la gestión de sesiones
  • los flujos de autorización de OAuth
  • la validación de tokens
  • los permisos de base de datos y la seguridad a nivel de fila
  • la validación de entradas
  • el filtrado de salidas
  • los límites de tasa
  • los registros de auditoría
  • la aprobación humana para operaciones riesgosas
  • las revisiones de seguridad de herramientas y agentes

Piensa en OpenFGA como el motor que toma decisiones de autorización. Otros sistemas deben consultarlo sin dejar de cumplir sus propias funciones.

OAuth / Authn          proves identity
OpenFGA               decides relationship permission
Database/RLS          enforces data boundary
Tool gateway          limits action shape
Audit logs            preserve accountability
Approval workflow     handles high-risk action consent

Esta separación es muy importante para MCP y los agentes de IA.

OpenFGA para servidores MCP

Los servidores MCP exponen herramientas, recursos y prompts a clientes de IA. Eso hace que la autorización sea más concreta que en una página web normal.

Necesitas responder preguntas como:

Can this user list Jira tickets for this project?
Can this user invoke the refund tool for this customer?
Can this agent read this repository through MCP?
Can this tool operate in production, or only staging?

La especificación de autorización de MCP se centra en flujos de OAuth, metadatos de recursos protegidos, scopes, indicadores de recursos, validación de tokens y desafíos 401 o 403. Todo eso es necesario, pero los scopes suelen ser demasiado generales para la autorización del producto.

Un scope como este:

tools:call

no te dice si el usuario puede llamar a refund_customer para customer:acme.

OpenFGA puede aportar la decisión que falta a nivel de recurso.

MCP client
   |
   | Authorization: Bearer <token>
   v
MCP server
   |
   | validate token, audience, issuer, scopes
   v
Tool router
   |
   | OpenFGA check:
   | user:ana can_call tool:refund_customer
   | user:ana can_refund customer:acme
   v
Tool handler

Un modelo pequeño para herramientas MCP:

model
  schema 1.1

type user

type workspace
  relations
    define admin: [user]
    define member: [user]

type customer
  relations
    define parent: [workspace]
    define support_agent: [user] or admin from parent
    define billing_manager: [user] or admin from parent
    define can_view: support_agent or billing_manager
    define can_refund: billing_manager

type tool
  relations
    define parent: [workspace]
    define allowed_user: [user]
    define allowed_member: member from parent
    define can_call: allowed_user or allowed_member

Luego, el manejador de la herramienta MCP verifica tanto la herramienta como el recurso de destino:

async function refundCustomerTool(ctx: McpContext, input: { customerId: string }) {
  const user = await requireMcpUser(ctx);

  await assertAllowed({
    userId: user.id,
    relation: "can_call",
    object: "tool:refund_customer",
  });

  await assertAllowed({
    userId: user.id,
    relation: "can_refund",
    object: `customer:${input.customerId}`,
  });

  return createRefundDraft(input.customerId);
}

Observa la última línea: createRefundDraft, no issueRefundImmediately.

Para acciones sensibles, la autorización no debería significar automáticamente ejecución autónoma. Puede significar «este usuario puede pedirle al agente que prepare la acción y luego una persona la aprueba».

OpenFGA para RAG y memoria de agentes

Los sistemas RAG suelen fallar en la autorización porque primero recuperan información y después la filtran.

Eso es riesgoso. Si el modelo ve contexto no autorizado, el daño ya está hecho.

Un flujo de RAG más seguro es:

User question
   |
   v
Resolve user identity and tenant
   |
   v
Find candidate documents
   |
   v
Filter by OpenFGA permissions
   |
   v
Fetch allowed chunks only
   |
   v
Generate answer

En la práctica, puedes combinar índices de búsqueda y OpenFGA de varias maneras.

Para conjuntos pequeños de candidatos, recupéralos y ejecuta batch checks:

async function authorizedSearch(userId: string, query: string) {
  const candidates = await vectorSearch(query, { limit: 40 });

  const checks = await Promise.all(
    candidates.map(async (doc) => {
      const result = await fga.check({
        user: `user:${userId}`,
        relation: "can_view",
        object: `document:${doc.documentId}`,
      });

      return result.allowed ? doc : null;
    }),
  );

  return checks.filter(Boolean);
}

Para sistemas grandes, usa OpenFGA para limitar el espacio de búsqueda antes de la recuperación, cuando sea posible:

List objects user can view
   |
   v
Use allowed document IDs as search filter
   |
   v
Retrieve chunks only from allowed documents

La implementación exacta depende de tu motor de búsqueda, el tamaño del tenant, el presupuesto de latencia y el diseño del índice. El principio no cambia:

El modelo solo debe recibir contexto que el usuario esté autorizado a ver.

Esto también se aplica a la memoria de los agentes. La memoria no es solo «datos». Son datos recordados cuyo uso futuro no está claro. Si las memorias pueden contener información de clientes, secretos, estrategias, conversaciones privadas o datos de tenants, también necesitan fronteras de autorización.

OpenFGA para agentes de workflows

Los agentes de workflows introducen un problema sutil de autorización:

La persona que solicitó el trabajo no siempre tiene la misma identidad que el proceso que lo realiza.

Human user
   |
   | "Deploy the staging build"
   v
Workflow orchestrator
   |
   v
Agent worker
   |
   v
GitHub / CI / Cloud / Database

Necesitas conservar al menos cuatro identidades:

IdentidadPregunta
Usuario humano¿Quién solicitó esto?
Agente¿Qué agente está actuando?
Tarea¿Qué trabajo específico se delegó?
Recurso¿Sobre qué se está actuando?

Un modelo útil consiste en representar las tareas como objetos.

model
  schema 1.1

type user

type agent
  relations
    define operator: [user]

type environment
  relations
    define deployer: [user, agent]
    define approver: [user]
    define can_deploy: deployer
    define can_approve: approver

type workflow_task
  relations
    define requester: [user]
    define assignee: [agent]
    define target_environment: [environment]
    define can_execute: assignee
    define can_approve: can_approve from target_environment

Luego, un orquestador puede verificar:

async function executeDeploymentTask(task: WorkflowTask) {
  await assertSubjectAllowed({
    subject: `agent:${task.agentId}`,
    relation: "can_execute",
    object: `workflow_task:${task.id}`,
  });

  await assertSubjectAllowed({
    subject: `agent:${task.agentId}`,
    relation: "can_deploy",
    object: `environment:${task.environmentId}`,
  });

  if (task.environment === "production") {
    await requireHumanApproval(task.id);
  }

  return startDeployment(task);
}

Esto hace explícita la autoridad del agente.

El agente no tiene poder porque posee un secreto en una variable de entorno. Tiene permiso porque el modelo de autorización establece que este agente específico puede ejecutar esta clase específica de tarea sobre este recurso específico.

Delegación limitada a tareas

La delegación limitada a tareas es el patrón para agentes que espero que cobre más importancia con el tiempo.

En lugar de otorgarle a un agente acceso amplio y permanente:

agent:researcher can_read all_documents in workspace:acme

Otórgale autoridad limitada y temporal:

agent:researcher can_execute workflow_task:task-789
workflow_task:task-789 can_read document:pricing-notes
workflow_task:task-789 expires at 2026-05-08T18:00:00Z

La estructura es:

Human grants task
   |
   v
Task gets limited resource relations
   |
   v
Agent acts through task
   |
   v
Task expires or is revoked

El modelo de relaciones de OpenFGA encaja bien con la parte duradera de este patrón. Si necesitas considerar la hora, el riesgo, la dirección IP, el estado de aprobación u otros hechos dinámicos, usa condiciones contextuales o combina los checks de OpenFGA con tu propia capa de políticas.

Por ejemplo:

await fga.check({
  user: `agent:${agentId}`,
  relation: "can_execute",
  object: `workflow_task:${taskId}`,
  contextualTuples: [
    {
      user: `workflow_task:${taskId}`,
      relation: "can_read",
      object: `document:${documentId}`,
    },
  ],
});

Las tuplas contextuales son útiles cuando una relación existe solo para esta solicitud o se puede derivar del estado actual del workflow. Ten cuidado de no ocultar hechos duraderos del producto dentro de código efímero de una solicitud. Si una relación debe ser auditable y reutilizable, almacénala.

OpenFGA y las bases de datos

OpenFGA decide si una acción debe permitirse. Tu base de datos debe seguir controlando sobre qué datos se puede actuar.

Por ejemplo, un flujo de edición de documentos puede combinar ambos:

API route
   |
   | OpenFGA: user can_edit document
   v
Database query
   |
   | tenant_id filter / RLS / narrow DB role
   v
Mutation

No le des a un agente de IA una credencial de propietario de la base de datos con la esperanza de que los checks de OpenFGA se ejecuten en todas partes. La base de datos debe usar roles limitados, seguridad a nivel de fila, vistas, procedimientos almacenados, tiempos de espera para sentencias y registros de auditoría cuando corresponda.

Para sistemas sensibles, trata a OpenFGA y a la base de datos como dos cerraduras independientes.

OpenFGA says: this subject may attempt the action.
Database says: this query may only touch these rows and columns.

Eso es defensa en profundidad, y es especialmente importante cuando los agentes pueden verse influidos por prompts, documentos recuperados, salidas de herramientas o solicitudes humanas ambiguas.

Errores comunes de modelado

Estos son errores que vigilaría en la primera implementación.

Error 1: modelar cada verbo como una relación almacenada

No almacenes todo como tuplas.

Prefiere:

define can_edit: editor or owner

En lugar de almacenar:

document:roadmap#can_edit@user:luis
document:roadmap#can_view@user:luis
document:roadmap#can_comment@user:luis

Almacena hechos estables. Deriva los permisos.

Error 2: olvidar la frontera del objeto

«Admin» es ambiguo.

¿Administrador de qué?

organization admin
workspace admin
project admin
billing admin
environment admin
tool admin

Los roles globales son fáciles de crear y difíciles de eliminar de forma segura.

Error 3: usar checks de la UI como mecanismo de seguridad

Ocultar un botón no es autorización.

Cada modificación y cada lectura sensible necesita un check del lado del servidor. Las herramientas de agentes necesitan la misma disciplina.

Error 4: darles a los agentes permisos del tamaño de los de una persona

Un agente que actúa en nombre de Luis no debería obtener automáticamente todos los permisos que tiene Luis.

Dale al agente el conjunto mínimo de permisos necesario para la tarea. Para operaciones de alto riesgo, exige aprobación incluso cuando el usuario podría realizar la acción manualmente.

Error 5: omitir las pruebas del modelo

Los errores de autorización son errores de producto y de seguridad al mismo tiempo.

Escribe pruebas para tu modelo:

Given Luis is org admin
When checking can_edit on a document in that org
Then allowed is true

Given guest-42 is direct_viewer
When checking can_edit on the document
Then allowed is false

Given triage-agent is assigned to task-789
When checking can_read on unrelated customer invoice
Then allowed is false

Estas pruebas se convierten en tu especificación ejecutable de permisos.

Lista de verificación para producción

Antes de usar OpenFGA en producción, me aseguraría de cubrir esta lista.

ÁreaQué decidir
Ciclo de vida del modeloCómo se revisan, versionan, migran y revierten los modelos
Escritura de tuplasQué servicio tiene permiso para crear o eliminar relaciones
LatenciaQué rutas necesitan caché, procesamiento por lotes o índices de permisos desnormalizados
ConsistenciaQué acciones requieren lecturas correctas inmediatamente después de una escritura
AuditoríaQué checks, cambios de tuplas, llamadas a herramientas y aprobaciones se registran
PruebasQué recorridos de usuario y casos de denegación están cubiertos
Herramientas de administraciónCómo investigan los equipos de soporte «¿por qué tiene acceso este usuario?»
Acceso de emergenciaCómo se concede, limita, registra y revoca el acceso de emergencia
Delegación a agentesSi los agentes reciben permisos permanentes o limitados a tareas
Aprobación humanaQué acciones pueden preparar los agentes, pero no ejecutar de forma autónoma

La funcionalidad visible del producto son los «permisos». El sistema de ingeniería comprende la gobernanza del modelo, el ciclo de vida de las tuplas, los checks, la observabilidad y la disciplina operativa.

Una ruta práctica de adopción

No necesitas modelar toda la empresa desde el primer día.

Empieza con una frontera de alto valor.

Buenos proyectos iniciales:

  • uso compartido de documentos
  • membresía en proyectos
  • acceso de soporte al cliente
  • acciones administrativas internas
  • acceso a herramientas MCP
  • filtrado de documentos para RAG
  • delegación de tareas a agentes

Una ruta de adopción puede verse así:

Step 1: Model one resource type
Step 2: Add server-side checks to one API surface
Step 3: Write allow and deny tests
Step 4: Add batch checks for UI controls
Step 5: Add audit logs for sensitive decisions
Step 6: Extend model to teams, projects, and agents
Step 7: Add operational tooling for support and debugging

El objetivo no es hacer que la autorización sea sofisticada. Es hacerla explícita, comprobable y reutilizable.

La decisión experta: separar la autoridad de la ejecución

La idea de diseño más importante es esta:

La autorización indica si un sujeto puede intentar una acción. La ejecución sigue necesitando controles de riesgo.

Para una aplicación web normal, eso significa que un usuario que puede editar un documento debe seguir pasando por validación, límites de tasa, checks de tenant y restricciones de base de datos.

Para un servidor MCP, significa que un usuario que puede llamar a una herramienta debe seguir limitado a argumentos seguros y recursos aprobados.

Para un workflow de agentes, significa que un agente que puede preparar un reembolso, un despliegue, una exportación de datos o un cambio de permisos aún puede necesitar aprobación humana antes de ejecutarlo.

Esta distinción mantiene tu sistema bajo control.

Permission check       Can this subject attempt this?
Input validation       Is the request shaped correctly?
Risk policy            Is this action sensitive?
Approval workflow      Does a human need to confirm?
Execution layer        Can the downstream system enforce limits?
Audit log              Can we explain what happened later?

OpenFGA es poderoso porque te da una forma consistente de responder la primera pregunta en todas esas superficies.

Modelo mental final

Si recuerdas una sola cosa, que sea esta:

Authentication identifies the actor.
OpenFGA evaluates the actor's relationship to the resource.
Your app executes only the allowed action.
Your database and tools enforce the blast radius.
Your audit trail explains the decision later.

Eso es útil para el SaaS clásico.

Es aún más útil para los agentes.

Porque una vez que el software puede planear, llamar a herramientas, recuperar contexto privado y actuar entre sistemas, los permisos deben convertirse en una funcionalidad fundamental del producto en lugar de sentencias if dispersas.

OpenFGA no resolverá todos los problemas de seguridad de las aplicaciones modernas, los servidores MCP, los agentes de workflows o los orquestadores. Pero te da una base clara para la autorización: relaciones explícitas, decisiones comprobables y un vocabulario que puede crecer desde «puede editar este documento» hasta «¿puede este agente ejecutar esta tarea sobre este cliente mediante esta herramienta en este momento?».

Ese es el salto de los roles a la autorización real.

Lista de fuentes