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| Operator | Query Param | SQL Equivalent |
|---|---|---|
| Equals | ?field=value | WHERE field = value |
| Not equals | ?field.ne=value | WHERE field != value |
| Greater than | ?field.gt=value | WHERE field > value |
| Greater or equal | ?field.gte=value | WHERE field >= value |
| Less than | ?field.lt=value | WHERE field < value |
| Less or equal | ?field.lte=value | WHERE field <= value |
| Pattern match | ?field.like=value | WHERE field LIKE '%value%' |
| In list | ?field.in=a,b,c | WHERE 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
maxPageSizeif 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
| Property | Type | Description |
|---|---|---|
$ctx.userId | string | Current authenticated user's ID |
$ctx.activeOrgId | string | User's active organization ID |
$ctx.activeTeamId | string | null | User's active team ID (if applicable) |
$ctx.roles | string[] | User's roles in current context |
$ctx.isAnonymous | boolean | Whether user is anonymous |
$ctx.user | object | Full user object from auth provider |
$ctx.user.id | string | User ID (nested path example) |
$ctx.user.email | string | User's email address |
$ctx.{property} | any | Any 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 objectrecord: The record being accessed (for get/update/delete operations)
Firewall - Data Isolation
Automatically isolate data by user, organization, or team with generated WHERE clauses. Prevent unauthorized data access at the database level.
Guards - Field Modification Rules
Control which fields can be modified during CREATE vs UPDATE operations. Protect sensitive fields and enforce data integrity rules.