# 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

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

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

```typescript
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

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:

```typescript
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

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

```typescript
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

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.

```typescript
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:

```typescript
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

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:

```typescript
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:

```typescript
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:

```typescript
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

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`               | `JSON.stringify([serverName, toolName])`                            |
| 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                                                |

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

Implement `IFGAProvider` to use any FGA backend:

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

## Related

- [Authentication overview](https://mastra.ai/docs/server/auth)
- [WorkOS authentication](https://mastra.ai/docs/server/auth/workos)