Appearance
Guards - Field Modification Rules
Control which fields can be modified in CREATE vs UPDATE operations.
Configuration Options
typescript
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)
typescript
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
typescript
guards: {
createable: ["name", "description", "amount"],
updatable: ["name", "description"],
protected: {
status: ["approve", "reject"], // Only via these actions
amount: ["reviseAmount"],
},
immutable: ["invoiceNumber"],
}Disabling Guards
typescript
guards: false // Only system fields protectedPUT/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
typescript
// quickback.config.ts
export default {
database: {
generateId: false, // Client provides IDs (enables PUT)
// Other options: 'uuid' | 'cuid' | 'nanoid' | 'serial'
}
};Resource Definition
typescript
defineResource(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:
typescript
// 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 |
'serial' | No | Database auto-increments |
false | Yes (if guards: false) | Client provides ID |