> ## Documentation Index
> Fetch the complete documentation index at: https://synthetiq.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Add access control

> Define scopes, configure row-level security, and enforce permissions declaratively

The App Framework implements role-based access control (RBAC) through `scopes.json`, a single configuration file that defines permissions, data access policies, and service access requirements. Enforcement is declarative — access control is never implemented as conditional logic in procedure handlers.

## scopes.json

For the task tracker, define scopes that let admins view and edit all tasks, and restrict regular users to only see and edit their own:

```json theme={null}
{
  "scopes": [
    { "name": "tasks:viewAll", "description": "View all users' tasks" },
    { "name": "tasks:editAll", "description": "Edit any user's tasks" }
  ],
  "tables": {
    "Task": {
      "ownerColumn": "userId",
      "bypassScopes": {
        "read": "tasks:viewAll",
        "write": "tasks:editAll"
      }
    }
  },
  "services": {}
}
```

The file has three sections:

* **`scopes`** — the available scopes for the app. These can be used to control access to procedures and pages.
* **`tables`** — defines row-level security on each table and the scopes required to bypass those restrictions.
* **`services`** — which services and tools the built-in AI agent can access on behalf of the user, and the user scopes required to do so.

## How it works

We've set up the `getMyTasks` procedure to only return the authenticated user's tasks:

```typescript theme={null}
getMyTasks: scopedProcedure([])
  .meta({ description: "Get current user's tasks" })
  .query(async ({ ctx }) => {
    return ctx.db.task.findMany({
      where: { userId: ctx.userId },
      orderBy: { createdAt: 'desc' },
    });
  }),
```

And scoped the `Task` table so that only rows where `userId` matches the authenticated user are returned:

```json theme={null}
"Task": {
  "ownerColumn": "userId",
  "bypassScopes": {
    "read": "tasks:viewAll",
    "write": "tasks:editAll"
  }
}
```

This means even if there was a bug in the procedure filtering, each user only has access to their own tasks at the data level.

However, we want admins to be able to access and edit all tasks. We can create a new procedure that requires the `tasks:viewAll` scope to do just that:

```typescript theme={null}
getAllTasks: scopedProcedure(['tasks:viewAll'])
  .meta({ description: 'Get all tasks across all users (admin)' })
  .query(async ({ ctx }) => {
    return ctx.db.task.findMany();
  }),
```

Now by default users will only be able to see their own tasks, and admins will be able to see all tasks.

## Defense in depth

All four enforcement layers derive from `scopes.json`:

| Layer                   | Mechanism                                 |
| ----------------------- | ----------------------------------------- |
| Frontend routes         | `ProtectedRoute` with `requiredScopes`    |
| Backend procedures      | `scopedProcedure(['scope'])`              |
| Database                | RLS policies generated from `scopes.json` |
| AI agent service access | `services` section of `scopes.json`       |

For additional RLS patterns (organization-based, group membership, inherited access, scope-gated), scope naming conventions, and build-time validation details, see the [Access control reference](/docs/platform-docs/app-framework/reference/access-control) and [Row-level security reference](/docs/platform-docs/app-framework/reference/row-level-security).
