Record Layouts
Customize how fields are grouped and displayed on the CMS record detail page with code-defined layouts and user-created views.
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:
- Code-defined layouts — developers configure named layouts in
defineTable() - User-created views — end-users create and save custom views via the CMS UI
Code-Defined Layouts
Add a layouts property to your defineTable() config:
export default defineTable(contacts, {
firewall: { organization: {} },
crud: { /* ... */ },
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:
| Property | Type | Default | Description |
|---|---|---|---|
label | string | required | Section header text |
fields | string[] | required | Column names to display |
columns | 1 | 2 | 1 | Number of columns for field layout |
collapsed | boolean | false | Whether 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.
User-Created Custom Views
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 View
- Open any record detail page
- Click the + View button in the header
- In the view builder dialog:
- Name your view
- Add sections and assign fields from a dropdown
- Set columns (1 or 2) and collapsed state per section
- Optionally share with your organization
- Click Create View
The new view appears in the layout dropdown alongside code-defined layouts.
Editing and Deleting Views
- Click the gear icon next to the dropdown to edit the current custom view
- Click the trash icon to delete it (with confirmation)
- Code-defined layouts cannot be edited or deleted from the CMS
Access Control
| Operation | Who Can Do It |
|---|---|
| Create a view | Any member, admin, or owner |
| Edit/delete own views | The creator |
| Edit/delete any view | Admins and owners |
| View shared views | All organization members |
Setting Up the Custom View Feature
To enable user-created views, 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),
});
export default defineTable(customView, {
displayColumn: "name",
firewall: { organization: {} },
crud: {
list: { access: { roles: ["owner", "admin", "member"] } },
get: { 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 will automatically detect the customView table and enable the view builder UI.
Fallback Behavior
| Scenario | Result |
|---|---|
No layouts config, no custom views | Auto-grouping by naming heuristics |
layouts config defined | Uses "default" layout or first available |
| Multiple layouts | Dropdown for switching, persisted per table |
| Custom views created | Appear in dropdown below code-defined layouts |
Next Steps
- Table Views — Column projections for list views
- Schema Format — Full TypeScript type reference
- Database Schema — defineTable() configuration reference