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
- User authenticates and
resolveSessionreturns their role - Engine looks up the role in the
rolesconfig - For
POST /data— all permission slugs in that role's array are activated - 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.