Skip to main content

Fine-Grained Authorization (FGA)

note

Fine-Grained Authorization is part of the Mastra Enterprise Edition. Production deployments require a valid EE license. Contact sales for more information.

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,
mapUserToResourceId: user => user.teamId,
}),
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',
},
}),
storedResources: {
scope: true,
},
},
});

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.

When server.fga is configured, Mastra enforces FGA on protected actions. If a protected action has no authenticated user, Mastra denies it. If server.fga is not configured, these FGA checks are skipped and Mastra keeps the previous behavior.

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
  • metadata — provider-specific metadata for the attempted action

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.

Use validatePermissions() to validate the full set of permissions Mastra may emit at startup. Use this when a provider requires every Mastra permission to have an explicit provider permission slug.

Stored resource scoping
Direct link to Stored resource scoping

FGA authorizes access to a resource. It does not automatically filter stored records that live in shared storage. Enable stored resource scoping when the built-in stored resource APIs are used in a multi-tenant app.

const mastra = new Mastra({
server: {
auth: new MastraAuthWorkos({
/* ... */
mapUserToResourceId: user => user.teamId,
}),
storedResources: {
scope: true,
},
},
});

With scope: true, Mastra reads MASTRA_RESOURCE_ID_KEY from the request context. mapUserToResourceId() sets this value after authentication. Stored resource handlers persist the scope in record metadata and filter list, read, update, publish, and delete operations by that scope.

Use an object when the scope needs custom request logic:

storedResources: {
scope: {
metadataKey: 'teamId',
resolve: ({ user }) => user.teamId,
requireScope: true,
},
},

If requireScope is true or omitted, scoped stored resource routes fail when no scope can be resolved.

Route policy coverage
Direct link to Route policy coverage

Mastra includes route-level FGA metadata for built-in resource routes, including agents, workflows, tools, MCP tools, memory threads, responses, conversations, and stored resources. Stored resource route coverage includes /stored/agents, /stored/mcp-clients, /stored/prompt-blocks, /stored/scorers, /stored/skills, and /stored/workspaces. A route is checked when it has route-level fga metadata, when Mastra can derive built-in metadata for that route, or when the provider supplies metadata with resolveRouteFGA().

To deny protected routes that do not resolve FGA metadata, configure route policy coverage on the FGA provider:

const fga = new MastraFGAWorkos({
resourceMapping: {
project: { fgaResourceType: 'project' },
},
permissionMapping: {
'projects:read': 'read',
},
requireForProtectedRoutes: true,
auditProtectedRoutes: 'warn',
validatePermissions: async permissions => {
// Throw if a Mastra permission is missing from permissionMapping.
},
});

Set auditProtectedRoutes: 'error' to fail startup when protected routes are missing built-in FGA metadata. If requireForProtectedRoutes is enabled, Mastra logs this audit as a warning by default.

For custom routes, prefer route-level fga metadata. This keeps authorization policy next to the route:

import { createRoute } from '@mastra/server/server-adapter';

export const getProjectRoute = createRoute({
method: 'GET',
path: '/projects/:projectId',
responseType: 'json',
requiresAuth: true,
fga: {
resourceType: 'project',
resourceIdParam: 'projectId',
permission: 'projects:read',
},
handler: async () => {
return { project: null };
},
});

Use resolveRouteFGA() only when route metadata must be derived centrally from route, params, or request context. A route map scales better than string-prefix checks:

import type { FGARouteConfig, FGARouteResolver } from '@mastra/core/auth/ee';

const routeFGA = {
'GET /billing/:accountId': {
resourceType: 'account',
resourceIdParam: 'accountId',
permission: 'billing:read',
},
} satisfies Record<string, FGARouteConfig>;

const resolveRouteFGA: FGARouteResolver = ({ route }) => routeFGA[`${route.method} ${route.path}`];

const fga = new MastraFGAWorkos({
/* ... */
resolveRouteFGA,
});

Enforcement points
Direct link to Enforcement points

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

Lifecycle pointPermission checkedResource typeResource ID
Agent execution (generate, stream)agents:executeagentagentId
Built-in workflow HTTP execution routes and Workflow.execute()workflows:executeworkflowworkflowId
Standalone tool executiontools:executetooltoolName
Agent tool executiontools:executetool${agentId}:${toolName}
MCP tool executiontools:executetoolJSON.stringify([serverName, toolName])
Thread and memory accessmemory:read, memory:write, memory:deletethreadthreadId
Stored resource routesStored resource permission for the route actionStored resource typeRoute record ID, or the stored-resource scope for collection routes
HTTP resource routesConfigured per routeConfigured per routeConfigured per route

For OAuth-protected MCP servers, HTTP MCP transports pass authenticated data as extra.authInfo. If an MCPServer is registered on an FGA-enabled Mastra instance, configure mapAuthInfoToUser so Mastra can set requestContext.get('user') before checking tools/list and tools/call. See MCPServer authentication context.

Direct SDK calls to createRun().start(), resume(), or restart() are not independently checked by core FGA in this release. Make those calls from a protected route or guard them in application code. Pass a requestContext with an authenticated user when invoking protected entry points directly.

Core agent, internal workflow, tool, and memory checks also pass requestContext and action metadata to the FGA provider. Route checks pass requestContext. Thread checks pass the owning resourceId when available.

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
}
}