Skip to main content
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:
{
  "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:
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:
"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:
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:
LayerMechanism
Frontend routesProtectedRoute with requiredScopes
Backend proceduresscopedProcedure(['scope'])
DatabaseRLS policies generated from scopes.json
AI agent service accessservices 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 and Row-level security reference.