Guards - Field Modification Rules
Control which fields can be modified during CREATE vs UPDATE operations. Protect sensitive fields and enforce data integrity rules.
Control which fields can be modified in CREATE vs UPDATE operations.
Basic Usage
// features/invoices/invoices.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
import { defineTable } from '@quickback/compiler';
export const invoices = sqliteTable('invoices', {
id: text('id').primaryKey(),
name: text('name').notNull(),
amount: integer('amount').notNull(),
status: text('status').notNull().default('draft'),
invoiceNumber: text('invoice_number'),
organizationId: text('organization_id').notNull(),
});
export default defineTable(invoices, {
firewall: { organization: {} },
guards: {
createable: ["name", "amount", "invoiceNumber"],
updatable: ["name"],
protected: {
status: ["approve", "reject"],
amount: ["reviseAmount"],
},
immutable: ["invoiceNumber"],
},
crud: {
// ...
},
});Configuration Options
guards: {
// Fields allowed on CREATE
createable?: string[];
// Fields allowed on UPDATE/PATCH
updatable?: string[];
// Fields only modifiable via specific actions
protected?: Record<string, string[]>;
// Fields set on CREATE, never modified after
immutable?: string[];
}How It Works
| List | What it controls |
|---|---|
createable | Fields allowed in create (POST) request body |
updatable | Fields allowed in update (PATCH) request body |
protected | Fields blocked from CRUD, only modifiable via named actions |
immutable | Fields allowed on create, blocked on all updates |
Combining lists:
createable+updatable- Most fields go in both (can set on create AND modify later)createableonly - Field is set once, cannot be changed via updateprotected- Don't also list increateableorupdatable(they're mutually exclusive)immutable- Don't also list inupdatable(contradiction)
guards: {
createable: ["name", "description", "category"],
updatable: ["name", "description"],
// "category" is only in createable = set once, can't change via update
protected: {
status: ["approve", "reject"], // NOT in createable/updatable
},
immutable: ["invoiceNumber"], // NOT in updatable
}If a field is not listed anywhere, it cannot be set by the client.
System-Managed Fields (Always Protected)
These are automatically protected - you cannot override:
createdAt,createdBymodifiedAt,modifiedBydeletedAt,deletedBy
Example
guards: {
createable: ["name", "description", "amount"],
updatable: ["name", "description"],
protected: {
status: ["approve", "reject"], // Only via these actions
amount: ["reviseAmount"],
},
immutable: ["invoiceNumber"],
}Disabling Guards
guards: false // Only system fields protectedWhen guards: false is set, all user-defined fields become writable via CRUD operations. This is useful for:
- External sync scenarios where you need full field control
- Batch upsert operations where field restrictions would be limiting
- Simple tables where field-level protection isn't needed
PUT/Upsert with External IDs
When you disable guards AND use client-provided IDs, you unlock PUT (upsert) operations. This is designed for syncing data from external systems.
Requirements for PUT
generateId: falsein database config (client provides IDs)guards: falsein resource definition
How PUT Works
PUT /resource/:id
├── Record exists? → UPDATE (replace all fields)
└── Record missing? → CREATE with provided IDDatabase Config
// quickback.config.ts
export default {
database: {
generateId: false, // Client provides IDs (enables PUT)
// Other options: 'uuid' | 'cuid' | 'nanoid' | 'prefixed' | 'serial'
}
};Table Definition
// features/external/external-accounts.ts
export default defineTable(externalAccounts, {
firewall: {
organization: {}, // Still isolated by org
},
guards: false, // Disables field restrictions
crud: {
put: {
access: { roles: ['admin', 'sync-service'] }
}
},
});What's Still Protected with guards: false
Even with guards: false, system-managed fields are ALWAYS protected:
createdAt,createdBy- Set on INSERT onlymodifiedAt,modifiedBy- Auto-updateddeletedAt,deletedBy- Set on soft delete
Ownership Auto-Population
When PUT creates a new record, ownership fields are auto-set from context:
// Client sends: PUT /accounts/ext-123 { name: "Acme" }
// Server creates:
{
id: "ext-123", // Client-provided
name: "Acme", // Client-provided
organizationId: ctx.activeOrgId, // Auto-set from firewall
createdAt: now, // Auto-set
createdBy: ctx.userId, // Auto-set
modifiedAt: now, // Auto-set
modifiedBy: ctx.userId, // Auto-set
}Use Cases for PUT/External IDs
| Use Case | Why PUT? |
|---|---|
| External API sync | External system controls the ID |
| Webhook handlers | Events come with their own IDs |
| Data migration | Preserve IDs from source system |
| Idempotent updates | Safe to retry (no duplicate creates) |
| Bulk upsert | Create or update in one operation |
ID Generation Options
generateId | PUT Available? | Notes |
|---|---|---|
'uuid' | No | Server generates UUID |
'cuid' | No | Server generates CUID |
'nanoid' | No | Server generates nanoid |
'prefixed' | No | Server generates prefixed ID (e.g. room_abc123) |
'serial' | No | Database auto-increments |
false | Yes (if guards: false) | Client provides ID |
Compile-Time Validation
The Quickback compiler validates your guards configuration and will error if:
- Field in both
createableandprotected- A field cannot be both client-writable on create and action-only - Field in both
updatableandprotected- A field cannot be both client-writable on update and action-only - Field in both
updatableandimmutable- Contradictory: immutable fields cannot be updated - Field in
protecteddoesn't exist in schema - Referenced field must exist in the table - Field in
createable/updatable/immutabledoesn't exist in schema - All referenced fields must exist
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.
Masking - Field Redaction
Hide sensitive data from unauthorized users with automatic field masking. Secure PII and confidential fields while maintaining access for authorized roles.