superapp
Backend

Roles

Map roles to permissions and define role hierarchies for access control.

Roles group permissions into named sets. Each user gets a role, and the engine resolves which permissions apply based on that role.

Example

An orders dashboard with three roles — viewer, editor, and admin:

import { createEngine } from '@superapp/backend'

const engine = createEngine({
  connections: {
    main: { type: 'postgres', url: process.env.PG_URL! },
  },
  permissions: {
    view_orders: { /* ... */ },
    edit_orders: { /* ... */ },
    delete_orders: { /* ... */ },
  },
  roles: {
    viewer: ['view_orders'],
    editor: ['view_orders', 'edit_orders'],
    admin: ['view_orders', 'edit_orders', 'delete_orders'],
  },
})

Each role is an array of permission slugs. Higher roles include lower-role permissions — admin can do everything editor can, plus delete orders.

How It Works

  1. User authenticates and resolveSession returns their role
  2. Engine looks up the role in the roles config
  3. All permissions in that role's array are activated for the request

Role Resolution

The role comes from the user's session. Set it up in resolveSession:

const auth = betterAuthProvider({
  secret: process.env.AUTH_SECRET!,
  userTable: {
    table: 'main.users',
    matchOn: { column: 'id', jwtField: 'id' },
  },
  resolveSession: async (user, db) => {
    const membership = await db
      .selectFrom('main.members')
      .select(['organization_id', 'role'])
      .where('user_id', '=', user.id)
      .where('status', '=', 'active')
      .executeTakeFirst()

    return {
      ...user,
      role: membership?.role ?? 'viewer',
      current_org_id: membership?.organization_id ?? null,
    }
  },
})

Unknown Roles

If a user's role is not in the roles config, they get zero permissions — all data requests return empty results or 403 Forbidden.

On this page