Quickback Docs

Record Layouts

Customize how fields are grouped and displayed on the CMS record detail page with code-defined and user-created layouts.

Record Layouts

The CMS record detail page groups fields into collapsible sections. By default, fields are auto-grouped by naming heuristics (Identity, Contact Info, Financial, etc.). With record layouts, you control the exact grouping.

There are two layers:

  1. Code-defined layouts — developers configure named layouts in the table config
  2. Custom layouts — end-users create and save record layouts through the CMS table settings

Code-Defined Layouts

Add a layouts property to your table config:

// quickback/features/contacts/contacts.ts
import { feature, q } from "@quickback/compiler";

export default feature("contacts", {
  columns: {
    id:             q.id(),
    name:           q.text().required(),
    status:         q.text().required(),
    email:          q.text().optional(),
    phone:          q.text().optional(),
    mobile:         q.text().optional(),
    address1:       q.text().optional(),
    address2:       q.text().optional(),
    city:           q.text().optional(),
    state:          q.text().optional(),
    zip:            q.text().optional(),
    notes:          q.text().optional(),
    internalNotes:  q.text().optional(),
    organizationId: q.scope("organization"),
    ...q.audit(),
    ...q.softDelete(),
  },
  firewall: [{ field: 'organizationId', equals: 'ctx.activeOrgId' }],
  // Contact details are detected as sensitive; on a CRM contact record they
  // are the point of the record, so this one is reviewed and left visible.
  masking: { email: false, phone: false, mobile: false },
  create: { /* ... */ },
  update: { /* ... */ },
  delete: { /* ... */ },
  layouts: {
    default: {
      sections: [
        { label: "Contact Info", columns: 2, fields: ["name", "email", "phone", "mobile"] },
        { label: "Address", columns: 2, fields: ["address1", "address2", "city", "state", "zip"] },
        { label: "Internal Notes", collapsed: true, fields: ["notes", "internalNotes"] }
      ]
    },
    compact: {
      sections: [
        { label: "Summary", fields: ["name", "status", "email"] }
      ]
    }
  }
});

Each layout has an ordered list of sections. Each section specifies:

PropertyTypeDefaultDescription
labelstringrequiredSection header text
fieldsstring[]requiredColumn names to display
columns1 | 21Number of columns for field layout
collapsedbooleanfalseWhether the section starts collapsed

Fields not assigned to any section are collected into an "Other Fields" section at the bottom.

Layout Switcher

When a table has multiple named layouts, a dropdown appears in the record detail header. Selections persist per table using localStorage.

If only one layout is defined, it's used automatically without showing a dropdown.

Custom Record Layouts

End-users can create their own record layouts via the CMS UI. These are stored in the database and can be shared with other organization members.

Creating a Layout

  1. Open the table
  2. Click the settings cog in the table header
  3. Open Record layouts and click Create
  4. In the record layout dialog:
    • Name your layout
    • Add sections and assign fields from a dropdown
    • Set columns (1 or 2) and collapsed state per section
    • Optionally share with your organization
  5. Click Create layout

The new layout appears in the record-page layout dropdown alongside code-defined layouts.

Editing and Deleting Layouts

  • Open Record layouts from the table header to edit or delete a custom layout
  • Open a record and click Customize layout to edit the active custom layout against that record
  • Code-defined layouts cannot be edited or deleted from the CMS

Access Control

OperationWho Can Do It
Create a layoutAny member, admin, or owner
Edit/delete own layoutsThe creator
Edit/delete any layoutAdmins and owners
View shared layoutsAll organization members

Setting Up Custom Layout Storage

To enable custom record layouts, add a customView table to your Quickback project:

// quickback/features/cms/custom-view.ts
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
import { defineTable } from "@quickback/compiler";

export const customView = sqliteTable("custom_view", {
  id: text("id").primaryKey(),
  organizationId: text("organization_id").notNull(),
  tableName: text("table_name").notNull(),
  name: text("name").notNull(),
  description: text("description"),
  layoutConfig: text("layout_config").notNull(),
  isShared: integer("is_shared", { mode: "boolean" }).default(false),
  // ── quickback:audit (compiler-managed — edits are validated, not merged) ──
  // No deletedAt/deletedBy: this resource hard-deletes (see `delete.mode` below),
  // and declaring the soft-delete pair on a hard-delete table is a compile error.
  createdAt: text("created_at").notNull().default('1970-01-01T00:00:00.000Z').$defaultFn(() => new Date().toISOString()),
  modifiedAt: text("modified_at").notNull().default('1970-01-01T00:00:00.000Z').$defaultFn(() => new Date().toISOString()).$onUpdate(() => new Date().toISOString()),
  createdBy: text("created_by"),
  modifiedBy: text("modified_by"),
});

export default defineTable(customView, {
  displayColumn: "name",
  firewall: [{ field: 'organizationId', equals: 'ctx.activeOrgId' }],
  read: {
    access: { roles: ["owner", "admin", "member"] },
  },
  create: { access: { roles: ["owner", "admin", "member"] } },
  update: {
    access: {
      or: [
        { roles: ["owner", "admin"] },
        { roles: ["member"], record: { createdBy: { equals: "$userId" } } }
      ]
    }
  },
  delete: {
    access: {
      or: [
        { roles: ["owner", "admin"] },
        { roles: ["member"], record: { createdBy: { equals: "$userId" } } }
      ]
    },
    mode: "hard"
  },
  guards: {
    createable: ["tableName", "name", "description", "layoutConfig", "isShared"],
    updatable: ["name", "description", "layoutConfig", "isShared"]
  }
});

Compile your project to generate the API endpoints. The CMS automatically detects the customView table and enables record layout management.

Fallback Behavior

ScenarioResult
No layouts config, no custom layoutsAuto-grouping by naming heuristics
layouts config definedUses "default" layout or first available
Multiple layoutsDropdown for switching, persisted per table
Custom layouts createdAppear in dropdown below code-defined layouts

Next Steps

On this page