Skip to main content

Fine-Grained Authorization (FGA)

Fine-Grained Authorization (FGA) adds resource-level permission checks to your Mastra application. While RBAC answers "can this role do this action?", FGA answers "can this user do this action on this specific resource?"

When to use FGA
Direct link to When to use FGA

FGA is designed for multi-tenant B2B products where permissions are contextual:

  • A user might be an admin of Team A but only a member of Team B
  • Thread access should be limited to the user's own organization
  • Workflow execution should be scoped to a specific team or project
  • Tool access depends on the user's relationship to a resource

Configuration
Direct link to Configuration

Configure FGA in your Mastra server config alongside authentication and RBAC:

import { Mastra } from '@mastra/core/mastra';
import { MastraFGAPermissions } from '@mastra/core/auth/ee';
import { MastraAuthWorkos, MastraFGAWorkos } from '@mastra/auth-workos';

const mastra = new Mastra({
server: {
auth: new MastraAuthWorkos({
/* ... */
fetchMemberships: true,
}),
fga: new MastraFGAWorkos({
resourceMapping: {
agent: { fgaResourceType: 'team', deriveId: (ctx) => ctx.user.teamId },
workflow: { fgaResourceType: 'team', deriveId: (ctx) => ctx.user.teamId },
thread: { fgaResourceType: 'workspace-thread', deriveId: ({ resourceId }) => resourceId },
},
permissionMapping: {
[MastraFGAPermissions.AGENTS_EXECUTE]: 'manage-workflows',
[MastraFGAPermissions.WORKFLOWS_EXECUTE]: 'manage-workflows',
[MastraFGAPermissions.MEMORY_READ]: 'read',
[MastraFGAPermissions.MEMORY_WRITE]: 'update',
},
}),
},
});

When using MastraFGAWorkos, set fetchMemberships: true on MastraAuthWorkos. WorkOS FGA checks need the user's organization memberships to resolve the correct membership ID for authorization.

Use thread as the resource-mapping key for memory authorization. MastraFGAWorkos still accepts the legacy alias memory, but new configs should prefer thread.

Resource mapping
Direct link to Resource mapping

The resourceMapping tells Mastra how to resolve FGA resource types and IDs from request context. Keys are Mastra resource types, values define the FGA resource type and how to derive the ID:

resourceMapping: {
// When checking "can user execute agent X?", resolve the FGA resource
// as the user's team (type: 'team', id: user.teamId)
agent: {
fgaResourceType: 'team',
deriveId: (ctx) => ctx.user.teamId,
},
}

deriveId() receives:

  • user — the authenticated user
  • resourceId — the owning Mastra resource ID when available (for example, a thread's resourceId)
  • requestContext — the current request context for advanced tenant resolution

Return undefined from deriveId() to fall back to the original Mastra resource ID.

For thread and memory checks, Mastra still passes the raw threadId as the resource being checked, but it also forwards the thread's owning resourceId into deriveId(). This lets you map thread permissions to composite tenant IDs such as userId-teamId-orgId.

Permission mapping
Direct link to Permission mapping

The permissionMapping translates Mastra's internal permission strings to your FGA provider's permission slugs:

import { MastraFGAPermissions } from '@mastra/core/auth/ee';

permissionMapping: {
[MastraFGAPermissions.AGENTS_EXECUTE]: 'manage-workflows', // Mastra permission -> WorkOS permission slug
[MastraFGAPermissions.MEMORY_READ]: 'read',
}

If no mapping exists for a permission, the original string is passed through.

Enforcement points
Direct link to Enforcement points

When an FGA provider is configured, Mastra automatically checks authorization at these lifecycle points:

Lifecycle pointPermission checkedResource
Agent execution (generate, stream)agents:execute{ type: 'agent', id: agentId }
Workflow executionworkflows:execute{ type: 'workflow', id: workflowId }
Tool executiontools:execute{ type: 'tool', id: toolName }
Thread/memory accessmemory:read, memory:write, memory:delete{ type: 'thread', id: threadId }
MCP tool executiontools:execute{ type: 'tool', id: toolName }
HTTP routes (opt-in)Configured per routeConfigured per route

All checks are no-ops when FGA is not configured, maintaining backward compatibility.

Custom FGA provider
Direct link to Custom FGA provider

Implement IFGAProvider to use any FGA backend:

import { FGADeniedError } from '@mastra/core/auth/ee'
import type { FGACheckParams, IFGAProvider, MastraFGAPermissionInput } from '@mastra/core/auth/ee'

class MyFGAProvider implements IFGAProvider {
async check(user: any, params: FGACheckParams): Promise<boolean> {
// Your authorization logic
return true
}

async require(user: any, params: FGACheckParams): Promise<void> {
const allowed = await this.check(user, params)
if (!allowed) {
throw new FGADeniedError(user, params.resource, params.permission)
}
}

async filterAccessible<T extends { id: string }>(
user: any,
resources: T[],
resourceType: string,
permission: MastraFGAPermissionInput,
): Promise<T[]> {
// Filter resources the user can access
return resources
}
}