Quickback Docs

Access - Role & Condition-Based Access Control

Define who can perform CRUD operations and under what conditions. Configure role-based and condition-based access control for your API endpoints.

Define who can perform CRUD operations and under what conditions.

Basic Usage

// features/rooms/rooms.ts
import { sqliteTable, text } from 'drizzle-orm/sqlite-core';
import { defineTable } from '@quickback/compiler';

export const rooms = sqliteTable('rooms', {
  id: text('id').primaryKey(),
  name: text('name').notNull(),
  organizationId: text('organization_id').notNull(),
});

export default defineTable(rooms, {
  firewall: { organization: {} },
  guards: { createable: ["name"], updatable: ["name"] },
  crud: {
    list: { access: { roles: ["owner", "admin", "member"] } },
    get: { access: { roles: ["owner", "admin", "member"] } },
    create: { access: { roles: ["owner", "admin"] } },
    update: { access: { roles: ["owner", "admin"] } },
    delete: { access: { roles: ["owner", "admin"] } },
  },
});

Configuration Options

interface Access {
  // Required roles (OR logic - user needs at least one)
  roles?: string[];

  // Record-level conditions
  record?: {
    [field: string]: FieldCondition;
  };

  // Combinators
  or?: Access[];
  and?: Access[];
}

// Field conditions - value can be string | number | boolean
type FieldCondition =
  | { equals: value | '$ctx.userId' | '$ctx.activeOrgId' }
  | { notEquals: value }
  | { in: value[] }
  | { notIn: value[] }
  | { lessThan: number }
  | { greaterThan: number }
  | { lessThanOrEqual: number }
  | { greaterThanOrEqual: number };

CRUD Configuration

crud: {
  // LIST - GET /resource
  list: {
    access: { roles: ["owner", "admin", "member"] },
    pageSize: 25,        // Default page size
    maxPageSize: 100,    // Client can't exceed this
    fields: ['id', 'name', 'status'],  // Selective field returns (optional)
  },

  // GET - GET /resource/:id
  get: {
    access: { roles: ["owner", "admin", "member"] },
    fields: ['id', 'name', 'status', 'details'],  // Optional field selection
  },

  // CREATE - POST /resource
  create: {
    access: { roles: ["owner", "admin"] },
    defaults: {           // Default values for new records
      status: 'pending',
      isActive: true,
    },
  },

  // UPDATE - PATCH /resource/:id
  update: {
    access: {
      or: [
        { roles: ["owner", "admin"] },
        { roles: ["member"], record: { status: { equals: "draft" } } }
      ]
    },
  },

  // DELETE - DELETE /resource/:id
  delete: {
    access: { roles: ["owner", "admin"] },
    mode: "soft",  // 'soft' (default) or 'hard'
  },

  // PUT - PUT /resource/:id (only when generateId: false + guards: false)
  put: {
    access: { roles: ["admin", "sync-service"] },
  },
}

List Filtering (Query Parameters)

The LIST endpoint automatically supports filtering via query params:

GET /rooms?status=active                    # Exact match
GET /rooms?capacity.gt=10                   # Greater than
GET /rooms?capacity.gte=10                  # Greater than or equal
GET /rooms?capacity.lt=50                   # Less than
GET /rooms?capacity.lte=50                  # Less than or equal
GET /rooms?status.ne=deleted                # Not equal
GET /rooms?name.like=Conference             # Pattern match (LIKE %value%)
GET /rooms?status.in=active,pending,review  # IN clause
OperatorQuery ParamSQL Equivalent
Equals?field=valueWHERE field = value
Not equals?field.ne=valueWHERE field != value
Greater than?field.gt=valueWHERE field > value
Greater or equal?field.gte=valueWHERE field >= value
Less than?field.lt=valueWHERE field < value
Less or equal?field.lte=valueWHERE field <= value
Pattern match?field.like=valueWHERE field LIKE '%value%'
In list?field.in=a,b,cWHERE field IN ('a','b','c')

Sorting & Pagination

GET /rooms?sort=createdAt&order=desc   # Sort by field
GET /rooms?limit=25&offset=50          # Pagination
  • Default limit: 50
  • Max limit: 100 (or maxPageSize if configured)
  • Default order: asc

Delete Modes

delete: {
  access: { roles: ["owner", "admin"] },
  mode: "soft",  // Sets deletedAt/deletedBy, record stays in DB
}

delete: {
  access: { roles: ["owner", "admin"] },
  mode: "hard",  // Permanent deletion from database
}

Context Variables

Use $ctx. prefix to reference context values in conditions:

// User can only view their own records
access: {
  record: { userId: { equals: "$ctx.userId" } }
}

// Nested path support for complex context objects
access: {
  record: { ownerId: { equals: "$ctx.user.id" } }
}

AppContext Reference

PropertyTypeDescription
$ctx.userIdstringCurrent authenticated user's ID
$ctx.activeOrgIdstringUser's active organization ID
$ctx.activeTeamIdstring | nullUser's active team ID (if applicable)
$ctx.rolesstring[]User's roles in current context
$ctx.isAnonymousbooleanWhether user is anonymous
$ctx.userobjectFull user object from auth provider
$ctx.user.idstringUser ID (nested path example)
$ctx.user.emailstringUser's email address
$ctx.{property}anyAny custom context property

Function-Based Access

For complex access logic that can't be expressed declaratively, use a function:

crud: {
  update: {
    access: async (ctx, record) => {
      // Custom logic - return true to allow, false to deny
      if (ctx.roles.includes('admin')) return true;
      if (record.ownerId === ctx.userId) return true;

      // Check custom business logic
      const membership = await checkTeamMembership(ctx.userId, record.teamId);
      return membership.canEdit;
    }
  }
}

Function access receives:

  • ctx: The full AppContext object
  • record: The record being accessed (for get/update/delete operations)

On this page