superapp
Backend

Roles

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

Roles group permissions and actions into named sets. Each user gets a role, and the engine resolves which permissions and actions 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: { /* ... */ },
  },
  actions: {
    exportOrders: async ({ user, db }, input) => { /* ... */ },
    bulkUpdate: async ({ user, db }, input) => { /* ... */ },
  },
  roles: {
    viewer: ['view_orders'],
    editor: ['view_orders', 'edit_orders', 'action_exportOrders'],
    admin: ['view_orders', 'edit_orders', 'delete_orders', 'action_exportOrders', 'action_bulkUpdate'],
  },
})

Each role is an array of permission slugs and action slugs. Action slugs are prefixed with action_ to distinguish them from table permissions. Higher roles include lower-role capabilities — admin can do everything editor can, plus delete orders and run bulk updates.

How It Works

  1. User authenticates and resolveSession returns their role
  2. Engine looks up the role in the roles config
  3. For POST /data — all permission slugs in that role's array are activated
  4. For POST /actions/{name} — engine checks if the action slug is in the role's array

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, and all action calls return 403.

On this page