Views - Column Level Security
Views implement Column Level Security (CLS) - controlling which columns users can access. This complements Row Level Security (RLS) provided by the Firewall, which controls which rows users can access.
| Security Layer | Controls | Quickback Feature |
|---|---|---|
| Row Level Security | Which records | Firewall |
| Column Level Security | Which fields | Views |
Views provide named field projections with role-based access control. Use views to return different sets of columns to different users without duplicating CRUD endpoints.
When to Use Views
| Concept | Purpose | Example |
|---|---|---|
| CRUD list | Full records | Returns all columns |
| Masking | Hide values | SSN *****6789 |
| Views | Exclude columns | Only id, name, status |
- CRUD list returns all fields to authorized users
- Masking transforms sensitive values but still includes the column
- Views completely exclude columns from the response
Basic Usage
// features/customers/customers.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
import { defineTable } from '@quickback/compiler';
export const customers = sqliteTable('customers', {
id: text('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull(),
phone: text('phone'),
ssn: text('ssn'),
internalNotes: text('internal_notes'),
organizationId: text('organization_id').notNull(),
});
export default defineTable(customers, {
masking: {
ssn: { type: 'ssn', show: { roles: ['admin'] } },
},
crud: {
list: { access: { roles: ['owner', 'admin', 'member'] } },
get: { access: { roles: ['owner', 'admin', 'member'] } },
},
// Views: Named field projections
views: {
summary: {
fields: ['id', 'name', 'email'],
access: { roles: ['owner', 'admin', 'member'] },
},
full: {
fields: ['id', 'name', 'email', 'phone', 'ssn', 'internalNotes'],
access: { roles: ['admin'] },
},
report: {
fields: ['id', 'name', 'email', 'createdAt'],
access: { roles: ['finance', 'admin'] },
},
},
});Generated Endpoints
Views generate GET endpoints at /{resource}/views/{viewName}:
GET /api/v1/customers # CRUD list - all fields
GET /api/v1/customers/views/summary # View - only summary fields
GET /api/v1/customers/views/full # View - all fields (admin only)
GET /api/v1/customers/views/report # View - report fieldsQuery Parameters
Views support the same query parameters as the list endpoint:
Pagination
| Parameter | Description | Default | Max |
|---|---|---|---|
limit | Number of records to return | 50 | 100 |
offset | Number of records to skip | 0 | - |
# Get first 10 records
GET /api/v1/customers/views/summary?limit=10
# Get records 11-20
GET /api/v1/customers/views/summary?limit=10&offset=10Filtering
# Filter by exact value
GET /api/v1/customers/views/summary?status=active
# Filter with operators
GET /api/v1/customers/views/summary?createdAt.gt=2024-01-01
# Multiple filters (AND logic)
GET /api/v1/customers/views/summary?status=active&email.like=yourdomain.comSorting
| Parameter | Description | Default |
|---|---|---|
sort | Field to sort by | createdAt |
order | Sort direction (asc or desc) | desc |
GET /api/v1/customers/views/summary?sort=name&order=ascSecurity
All four security pillars apply to views:
| Pillar | Behavior |
|---|---|
| Firewall | WHERE clause applied (same as list) |
| Access | Per-view access control |
| Guards | N/A (read-only) |
| Masking | Applied to returned fields |
Firewall
Views automatically apply the same firewall conditions as the list endpoint. Users only see records within their organization scope.
Access Control
Each view has its own access configuration:
views: {
// Public view - available to all members
summary: {
fields: ['id', 'name', 'status'],
access: { roles: ['owner', 'admin', 'member'] },
},
// Restricted view - admin only
full: {
fields: ['id', 'name', 'status', 'ssn', 'internalNotes'],
access: { roles: ['admin'] },
},
}Masking
Masking rules are applied to the returned fields. If a view includes a masked field like ssn, the masking rules still apply:
masking: {
ssn: { type: 'ssn', show: { roles: ['admin'] } },
},
views: {
// Even if a member accesses the 'full' view, ssn will be masked
// because masking rules take precedence
full: {
fields: ['id', 'name', 'ssn'],
access: { roles: ['owner', 'admin', 'member'] },
},
}How Views and Masking Work Together
Views and masking are orthogonal concerns:
- Views control field selection (which columns appear)
- Masking controls field transformation (how values appear based on role)
Example configuration:
masking: {
ssn: { type: 'ssn', show: { roles: ['admin'] } },
},
views: {
summary: {
fields: ['id', 'name'], // ssn NOT included
access: { roles: ['owner', 'admin', 'member'] },
},
full: {
fields: ['id', 'name', 'ssn'], // ssn included
access: { roles: ['owner', 'admin', 'member'] },
},
}| Endpoint | Role | ssn in response? | ssn value |
|---|---|---|---|
/views/summary | member | No | N/A |
/views/summary | admin | No | N/A |
/views/full | member | Yes | *****6789 |
/views/full | admin | Yes | 123-45-6789 |
Response Format
View responses include metadata about the view:
{
"data": [
{ "id": "cust_123", "name": "John Doe", "email": "john@yourdomain.com" },
{ "id": "cust_456", "name": "Jane Smith", "email": "jane@yourdomain.com" }
],
"view": "summary",
"fields": ["id", "name", "email"],
"pagination": {
"limit": 50,
"offset": 0,
"count": 2
}
}Complete Example
// features/employees/employees.ts
export default defineTable(employees, {
guards: {
createable: ['name', 'email', 'phone', 'department'],
updatable: ['name', 'email', 'phone'],
},
masking: {
ssn: { type: 'ssn', show: { roles: ['hr', 'admin'] } },
salary: { type: 'redact', show: { roles: ['hr', 'admin'] } },
personalEmail: { type: 'email', show: { or: 'owner' } },
},
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'] } },
},
views: {
// Directory view - public employee info
directory: {
fields: ['id', 'name', 'email', 'department'],
access: { roles: ['owner', 'admin', 'member'] },
},
// HR view - includes sensitive info
hr: {
fields: ['id', 'name', 'email', 'phone', 'ssn', 'salary', 'department', 'startDate'],
access: { roles: ['owner', 'admin'] },
},
// Payroll export
payroll: {
fields: ['id', 'name', 'ssn', 'salary', 'bankAccount'],
access: { roles: ['owner', 'admin'] },
},
},
});