Quickback Docs

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

ListWhat it controls
createableFields allowed in create (POST) request body
updatableFields allowed in update (PATCH) request body
protectedFields blocked from CRUD, only modifiable via named actions
immutableFields allowed on create, blocked on all updates

Combining lists:

  • createable + updatable - Most fields go in both (can set on create AND modify later)
  • createable only - Field is set once, cannot be changed via update
  • protected - Don't also list in createable or updatable (they're mutually exclusive)
  • immutable - Don't also list in updatable (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, createdBy
  • modifiedAt, modifiedBy
  • deletedAt, 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 protected

When 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

  1. generateId: false in database config (client provides IDs)
  2. guards: false in resource definition

How PUT Works

PUT /resource/:id
├── Record exists? → UPDATE (replace all fields)
└── Record missing? → CREATE with provided ID

Database 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 only
  • modifiedAt, modifiedBy - Auto-updated
  • deletedAt, 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 CaseWhy PUT?
External API syncExternal system controls the ID
Webhook handlersEvents come with their own IDs
Data migrationPreserve IDs from source system
Idempotent updatesSafe to retry (no duplicate creates)
Bulk upsertCreate or update in one operation

ID Generation Options

generateIdPUT Available?Notes
'uuid'NoServer generates UUID
'cuid'NoServer generates CUID
'nanoid'NoServer generates nanoid
'prefixed'NoServer generates prefixed ID (e.g. room_abc123)
'serial'NoDatabase auto-increments
falseYes (if guards: false)Client provides ID

Compile-Time Validation

The Quickback compiler validates your guards configuration and will error if:

  1. Field in both createable and protected - A field cannot be both client-writable on create and action-only
  2. Field in both updatable and protected - A field cannot be both client-writable on update and action-only
  3. Field in both updatable and immutable - Contradictory: immutable fields cannot be updated
  4. Field in protected doesn't exist in schema - Referenced field must exist in the table
  5. Field in createable/updatable/immutable doesn't exist in schema - All referenced fields must exist

On this page