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/applications/applications.ts
import { sqliteTable, text } from 'drizzle-orm/sqlite-core';
import { defineTable } from '@quickback/compiler';
export const applications = sqliteTable('applications', {
id: text('id').primaryKey(),
candidateId: text('candidate_id').notNull(),
jobId: text('job_id').notNull(),
stage: text('stage').notNull(),
notes: text('notes'),
organizationId: text('organization_id').notNull(),
});
export default defineTable(applications, {
firewall: { organization: {} },
guards: { createable: ["candidateId", "jobId", "notes"], updatable: ["notes"] },
crud: {
list: { access: { roles: ["owner", "hiring-manager", "recruiter", "interviewer"] } },
get: { access: { roles: ["owner", "hiring-manager", "recruiter", "interviewer"] } },
create: { access: { roles: ["owner", "hiring-manager", "recruiter"] } },
update: { access: { roles: ["owner", "hiring-manager", "recruiter"] } },
delete: { access: { roles: ["owner", "hiring-manager"] } },
},
});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[];
}Special Role: PUBLIC
Use roles: ["PUBLIC"] to make an endpoint accessible without authentication. This is intended for public-facing endpoints like contact forms or webhooks.
access: { roles: ["PUBLIC"] }Important:
PUBLICskips authentication, organization, and role checks entirely- Every
PUBLICaction invocation is mandatory audit logged to the security audit table (IP address, input, result, timing) - The wildcard
"*"is not supported — using it will throw a compile-time error
// Contact form — public, no auth
access: { roles: ["PUBLIC"] }
// Any authenticated user with any org role
access: { roles: ["member", "admin", "owner"] }
// Specific roles only
access: { roles: ["admin"] }// 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", "hiring-manager", "recruiter", "interviewer"] },
pageSize: 25, // Default page size
maxPageSize: 100, // Client can't exceed this
fields: ['id', 'candidateId', 'jobId', 'stage'], // Selective field returns (optional)
},
// GET - GET /resource/:id
get: {
access: { roles: ["owner", "hiring-manager", "recruiter", "interviewer"] },
fields: ['id', 'candidateId', 'jobId', 'stage', 'notes'], // Optional field selection
},
// CREATE - POST /resource
create: {
access: { roles: ["owner", "hiring-manager", "recruiter"] },
defaults: { // Default values for new records
stage: 'applied',
},
},
// UPDATE - PATCH /resource/:id
update: {
access: {
or: [
{ roles: ["hiring-manager", "recruiter"] },
{ roles: ["interviewer"], record: { stage: { equals: "interview" } } }
]
},
},
// DELETE - DELETE /resource/:id
delete: {
access: { roles: ["owner", "hiring-manager"] },
mode: "soft", // 'soft' (default) or 'hard'
},
// PUT - PUT /resource/:id (only when generateId: false + guards: false)
put: {
access: { roles: ["hiring-manager", "sync-service"] },
},
}List Filtering (Query Parameters)
The LIST endpoint automatically supports filtering via query params:
GET /jobs?status=open # Exact match
GET /jobs?salaryMin.gt=50000 # Greater than
GET /jobs?salaryMin.gte=50000 # Greater than or equal
GET /jobs?salaryMax.lt=200000 # Less than
GET /jobs?salaryMax.lte=200000 # Less than or equal
GET /jobs?status.ne=closed # Not equal
GET /jobs?title.like=Engineer # Pattern match (LIKE %value%)
GET /jobs?status.in=open,draft # 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 /jobs?sort=createdAt&order=desc # Sort by field
GET /jobs?limit=25&offset=50 # Pagination- Default limit: 50
- Max limit: 100 (or
maxPageSizeif configured) - Default order:
asc
Delete Modes
delete: {
access: { roles: ["owner", "hiring-manager"] },
mode: "soft", // Sets deletedAt/deletedBy, record stays in DB
}
delete: {
access: { roles: ["owner", "hiring-manager"] },
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.