Complete Example
See what you define and what Quickback generates
This example shows a complete resource definition using all five security layers, and the production code Quickback generates from it.
What You Define
A customers resource for a multi-tenant SaaS application with:
- Organization-level data isolation
- Role-based CRUD permissions
- Field-level write protection
- PII masking for sensitive fields
- Column-level views for different access levels
Schema
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
export const customers = sqliteTable('customers', {
id: text('id').primaryKey(),
organizationId: text('organization_id').notNull(),
name: text('name').notNull(),
email: text('email').notNull(),
phone: text('phone'),
ssn: text('ssn'),
});
// Note: createdAt, createdBy, modifiedAt, modifiedBy are auto-injectedResource Configuration
import { defineResource } from '@quickback/define';
import { customers } from './schema';
export default defineResource(customers, {
// FIREWALL: Data isolation
firewall: {
organization: {}
},
// ACCESS: Role-based permissions
crud: {
list: { access: { roles: ['owner', 'admin', 'member', 'support'] } },
get: { access: { roles: ['owner', 'admin', 'member', 'support'] } },
create: { access: { roles: ['admin'] } },
update: { access: { roles: ['admin', 'support'] } },
delete: { access: { roles: ['admin'] }, mode: 'hard' },
},
// GUARDS: Field-level protection
guards: {
createable: ['name', 'email', 'phone', 'ssn'],
updatable: ['name', 'email', 'phone'],
immutable: ['ssn'], // Can set once, never change
},
// MASKING: PII redaction
masking: {
ssn: { type: 'ssn', show: { roles: ['admin'] } },
phone: { type: 'phone', show: { roles: ['admin', 'support'] } },
email: { type: 'email', show: { roles: ['admin'] } },
},
// VIEWS: Column-level projections
views: {
summary: {
fields: ['id', 'name', 'email'],
access: { roles: ['owner', 'admin', 'member', 'support'] },
},
full: {
fields: ['id', 'name', 'email', 'phone', 'ssn'],
access: { roles: ['owner', 'admin'] },
},
},
});What Quickback Generates
Run quickback compile and Quickback generates production-ready code.
Security Helpers
import { eq, and } from 'drizzle-orm';
import { customers } from './schema';
import type { AppContext } from '../../lib/types';
import { masks } from '../../lib/masks';
/**
* Firewall conditions for customers
* Pattern: Organization only
*/
export function buildFirewallConditions(ctx: AppContext) {
const conditions = [];
// Organization isolation
conditions.push(eq(customers.organizationId, ctx.activeOrgId!));
return and(...conditions);
}
/**
* Guards configuration for field modification rules
*/
export const GUARDS_CONFIG = {
createable: new Set(['name', 'email', 'phone', 'ssn']),
updatable: new Set(['name', 'email', 'phone']),
immutable: new Set(['ssn']),
protected: {},
systemManaged: new Set([
'createdAt', 'createdBy',
'modifiedAt', 'modifiedBy',
'deletedAt', 'deletedBy'
]),
};
/**
* Validates field input for CREATE operations.
*/
export function validateCreate(input: Record<string, any>) {
const systemManaged: string[] = [];
const protectedFields: Array<{ field: string; actions: string[] }> = [];
const notCreateable: string[] = [];
for (const field of Object.keys(input)) {
if (GUARDS_CONFIG.systemManaged.has(field)) {
systemManaged.push(field);
continue;
}
if (field in GUARDS_CONFIG.protected) {
const actions = [...GUARDS_CONFIG.protected[field]];
protectedFields.push({ field, actions });
continue;
}
if (!GUARDS_CONFIG.createable.has(field) && !GUARDS_CONFIG.immutable.has(field)) {
notCreateable.push(field);
}
}
const valid = systemManaged.length === 0 &&
protectedFields.length === 0 &&
notCreateable.length === 0;
return { valid, systemManaged, protected: protectedFields, notCreateable };
}
/**
* Validates field input for UPDATE operations.
*/
export function validateUpdate(input: Record<string, any>) {
const systemManaged: string[] = [];
const immutable: string[] = [];
const protectedFields: Array<{ field: string; actions: string[] }> = [];
const notUpdatable: string[] = [];
for (const field of Object.keys(input)) {
if (GUARDS_CONFIG.systemManaged.has(field)) {
systemManaged.push(field);
continue;
}
if (GUARDS_CONFIG.immutable.has(field)) {
immutable.push(field);
continue;
}
if (field in GUARDS_CONFIG.protected) {
const actions = [...GUARDS_CONFIG.protected[field]];
protectedFields.push({ field, actions });
continue;
}
if (!GUARDS_CONFIG.updatable.has(field)) {
notUpdatable.push(field);
}
}
const valid = systemManaged.length === 0 &&
immutable.length === 0 &&
protectedFields.length === 0 &&
notUpdatable.length === 0;
return { valid, systemManaged, immutable, protected: protectedFields, notUpdatable };
}
/**
* Masks sensitive fields based on user role
*/
export function maskCustomer<T extends Record<string, any>>(
record: T,
ctx: AppContext
): T {
const masked: any = { ...record };
// email: show to admin only
if (!ctx.roles?.includes('admin')) {
if (masked['email'] != null) {
masked['email'] = masks.email(masked['email']);
}
}
// phone: show to admin, support
if (!ctx.roles?.some(r => ['admin', 'support'].includes(r))) {
if (masked['phone'] != null) {
masked['phone'] = masks.phone(masked['phone']);
}
}
// ssn: show to admin only
if (!ctx.roles?.includes('admin')) {
if (masked['ssn'] != null) {
masked['ssn'] = masks.ssn(masked['ssn']);
}
}
return masked as T;
}
export function maskCustomers<T extends Record<string, any>>(
records: T[],
ctx: AppContext
): T[] {
return records.map(r => maskCustomer(r, ctx));
}
// CRUD Access configuration
export const CRUD_ACCESS = {
list: { access: { roles: ['owner', 'admin', 'member', 'support'] } },
get: { access: { roles: ['owner', 'admin', 'member', 'support'] } },
create: { access: { roles: ['admin'] } },
update: { access: { roles: ['admin', 'support'] } },
delete: { access: { roles: ['admin'] }, mode: 'hard' },
};
// Views configuration
export const VIEWS_CONFIG = {
summary: {
fields: ['id', 'name', 'email'],
access: { roles: ['owner', 'admin', 'member', 'support'] },
},
full: {
fields: ['id', 'name', 'email', 'phone', 'ssn'],
access: { roles: ['owner', 'admin'] },
},
};API Routes
The generated routes wire everything together:
import { Hono } from 'hono';
import { eq, and } from 'drizzle-orm';
import { customers } from './schema';
import {
buildFirewallConditions,
validateCreate,
validateUpdate,
maskCustomer,
maskCustomers,
CRUD_ACCESS
} from './customers.resource';
import { evaluateAccess } from '../../lib/access';
import { AuthErrors, AccessErrors, GuardErrors } from '../../lib/errors';
const app = new Hono();
// GET /customers - List with firewall + masking
app.get('/', async (c) => {
const ctx = c.get('ctx');
const db = c.get('db');
// Access check
if (!await evaluateAccess(CRUD_ACCESS.list.access, ctx)) {
return c.json(AccessErrors.roleRequired(
CRUD_ACCESS.list.access.roles,
ctx.roles
), 403);
}
// Query with firewall conditions
const results = await db.select().from(customers)
.where(buildFirewallConditions(ctx));
// Apply masking before returning
return c.json({
data: maskCustomers(results, ctx),
});
});
// POST /customers - Create with guards
app.post('/', async (c) => {
const ctx = c.get('ctx');
const db = c.get('db');
// Access check
if (!await evaluateAccess(CRUD_ACCESS.create.access, ctx)) {
return c.json(AccessErrors.roleRequired(
CRUD_ACCESS.create.access.roles,
ctx.roles
), 403);
}
const body = await c.req.json();
// Guards validation
const validation = validateCreate(body);
if (!validation.valid) {
if (validation.systemManaged.length > 0) {
return c.json(GuardErrors.systemManaged(validation.systemManaged), 400);
}
if (validation.notCreateable.length > 0) {
return c.json(GuardErrors.fieldNotCreateable(validation.notCreateable), 400);
}
}
// Apply ownership and audit fields
const data = {
id: 'cst_' + crypto.randomUUID().replace(/-/g, ''),
...body,
organizationId: ctx.activeOrgId,
createdAt: new Date().toISOString(),
createdBy: ctx.userId,
modifiedAt: new Date().toISOString(),
modifiedBy: ctx.userId,
};
const result = await db.insert(customers).values(data).returning();
return c.json(maskCustomer(result[0], ctx), 201);
});
// PATCH /customers/:id - Update with guards
app.patch('/:id', async (c) => {
const ctx = c.get('ctx');
const db = c.get('db');
const id = c.req.param('id');
// Fetch with firewall
const [record] = await db.select().from(customers)
.where(and(buildFirewallConditions(ctx), eq(customers.id, id)));
if (!record) {
return c.json({ error: 'Not found', code: 'NOT_FOUND' }, 404);
}
// Access check
if (!await evaluateAccess(CRUD_ACCESS.update.access, ctx)) {
return c.json(AccessErrors.roleRequired(
CRUD_ACCESS.update.access.roles,
ctx.roles
), 403);
}
const body = await c.req.json();
// Guards validation
const validation = validateUpdate(body);
if (!validation.valid) {
if (validation.immutable.length > 0) {
return c.json(GuardErrors.fieldImmutable(validation.immutable), 400);
}
if (validation.notUpdatable.length > 0) {
return c.json(GuardErrors.fieldNotUpdatable(validation.notUpdatable), 400);
}
}
// Apply audit fields
const data = {
...body,
modifiedAt: new Date().toISOString(),
modifiedBy: ctx.userId,
};
const result = await db.update(customers).set(data)
.where(eq(customers.id, id)).returning();
return c.json(maskCustomer(result[0], ctx));
});
export default app;Runtime Behavior
Masking by Role
| Role | email | phone | ssn |
|---|---|---|---|
| admin | john@example.com | 555-123-4567 | 123-45-6789 |
| support | j***@e******.com | 555-123-4567 | *****6789 |
| member | j***@e******.com | ******4567 | *****6789 |
Views + Masking Interaction
Views control which fields are returned. Masking controls what values are shown.
| Endpoint | Role | ssn in response? | ssn value |
|---|---|---|---|
/customers/views/summary | member | No | N/A |
/customers/views/summary | admin | No | N/A |
/customers/views/full | member | Yes | *****6789 |
/customers/views/full | admin | Yes | 123-45-6789 |
Guards Enforcement
# ✅ Allowed: Create with createable fields
POST /customers
{ "name": "Jane", "email": "jane@example.com", "ssn": "987-65-4321" }
# ❌ Rejected: Update immutable field
PATCH /customers/cst_123
{ "ssn": "000-00-0000" }
# → 400: "Field 'ssn' is immutable and cannot be modified"
# ❌ Rejected: Set system-managed field
POST /customers
{ "name": "Jane", "createdBy": "hacker" }
# → 400: "System-managed fields cannot be set: createdBy"Generated API Endpoints
| Method | Endpoint | Access |
|---|---|---|
GET | /customers | owner, admin, member, support |
GET | /customers/:id | owner, admin, member, support |
POST | /customers | admin |
PATCH | /customers/:id | admin, support |
DELETE | /customers/:id | admin |
GET | /customers/views/summary | owner, admin, member, support |
GET | /customers/views/full | admin |
Key Takeaways
- ~50 lines of configuration generates ~500+ lines of production code
- Security is declarative - you describe what you want, not how
- All layers work together - Firewall → Access → Guards → Masking
- Code is readable and auditable - no magic, just TypeScript
- Type-safe from definition to API - Drizzle schema drives everything