Fine-Grained Authorization (FGA)
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 FGADirect 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
ConfigurationDirect 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 isn't configured, these FGA checks are skipped and Mastra keeps the previous behavior.
Resource mappingDirect 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 userresourceId: the owning Mastra resource ID when available (for example, a thread'sresourceId)requestContext: the current request context for advanced tenant resolutionmetadata: 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 mappingDirect 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 scopingDirect link to Stored resource scoping
FGA authorizes access to a resource. It doesn't 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 coverageDirect 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 don't 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 pointsDirect link to Enforcement points
When an FGA provider is configured, Mastra automatically checks authorization at these lifecycle points:
| Lifecycle point | Permission checked | Resource type | Resource ID |
|---|---|---|---|
Agent execution (generate, stream) | agents:execute | agent | agentId |
Built-in workflow HTTP execution routes and Workflow.execute() | workflows:execute | workflow | workflowId |
| Standalone tool execution | tools:execute | tool | toolName |
| Agent tool execution | tools:execute | tool | ${agentId}:${toolName} |
| MCP tool execution | tools:execute | tool by default, or the server-level fga.resourceMapping override | JSON.stringify([serverName, toolName]) by default, or the server-level derived ID |
| Thread and memory access | memory:read, memory:write, memory:delete | thread | threadId |
| Stored resource routes | Stored resource permission for the route action | Stored resource type | Route record ID, or the stored-resource scope for collection routes |
| HTTP resource routes | Configured per route | Configured per route | Configured 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. Use the server-level fga option when MCP tool checks need different resource or permission mapping than internal agent and workflow tool checks. See MCPServer authentication context.
Direct SDK calls to createRun().start(), resume(), or restart() aren't 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 providerDirect 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
}
}
See the IFGAProvider reference for every method, parameter, and the ActorSignal type.
System actorsDirect link to System actors
Autonomous and scheduled agents run without an end user. Mark these calls with an actor signal so FGA can tell them apart from user requests:
trueor{ actorKind: 'system' }identifies an anonymous system actor.- The object form can also carry
agentId,permissions, andscopeto identify and constrain the acting agent.
By default, a trusted actor skips the user-centric require() check after a tenant-scope check. To enforce per-agent least privilege, implement the optional requireActor method on your provider. It receives the actor and the same FGACheckParams as require, and throws FGADeniedError to deny. When your provider doesn't implement requireActor, the trusted-actor bypass is preserved, so adding it's backward compatible.
import { FGADeniedError } from '@mastra/core/auth/ee'
import type { ActorSignal, FGACheckParams, IFGAProvider } from '@mastra/core/auth/ee'
class MyFGAProvider implements IFGAProvider {
// ...check, require, filterAccessible...
async requireActor(actor: ActorSignal, params: FGACheckParams): Promise<void> {
const agentId = actor === true ? undefined : actor.agentId
// Resolve the agent's real grants from a trusted source keyed by agentId.
const granted = await this.grantsForAgent(agentId)
const required = Array.isArray(params.permission) ? params.permission : [params.permission]
if (!required.some(permission => granted.includes(permission))) {
throw new FGADeniedError(null, params.resource, params.permission)
}
}
}
Trust requirementsDirect link to Trust requirements
The actor signal is trusted input, so construct it server-side:
- Treat
actoras a per-call signal. Durable workflows forward it when a run starts, but don't restore the initial actor on resume. Pass it explicitly on every trusted resume, or resolve a fresh actor through agentdefaultOptions. Without a current actor, user authorization applies and a missing user fails closed. - Mastra strips
actorfrom execution options handled by its built-in agent HTTP routes. Set it in server-side code, for example a scheduled job or a workflow, never from client input. - Establish tenant scope server-side. Built-in agent HTTP routes ignore a client-supplied
organizationIdin the request context, and the trusted-actor path requires anorganizationIdto be set. - Durable resume keeps its existing request-context recovery and merge behavior. This doesn't make a persisted actor trusted for a later workflow segment.
- The tenant-scope check confirms that a trusted
organizationIdexists. It doesn't verify thatactor.agentIdbelongs to that organization. When that relationship matters, verify it inrequireActorusing authoritative provider data. - Treat
actor.permissionsas an unverified claim. Resolve authoritative grants from a trusted source. A provider that enforces least privilege resolves the agent's authoritative permissions from a trusted source, for example a manifest or your FGA backend keyed byagentId, rather than trusting the inline values. - Once a provider implements
requireActor, errors from that method stop execution. Mastra doesn't fall back to organization-only authorization.