Payload Collection Design
Patterns for designing Payload CMS collections that power the Business-as-Code system.
Core Principle
Every Collection is a Noun. Every Noun renders beautifully.
Collection (Payload) → Noun (schema.org.ai) → Component (mdxui)
Collection Domains
Collections are organized by domain in packages/db.sb/src/collections/:
| Domain |
Purpose |
Examples |
| admin |
Platform admin |
Users, Orgs, ApiKeys |
| ai |
AI/ML experiments |
ModelEvals, AIExperiments |
| api |
API infrastructure |
Proxies, Crawlers |
| business |
Business entities |
Businesses, Teams, Goals |
| code |
Code artifacts |
Workers, Artifacts |
| communications |
Messaging |
Messages, Sequences, Channels |
| compliance |
Regulatory |
Policies, Controls, Evidence |
| content |
Documents |
Documents, Files, Presentations |
| data |
Ontology |
Nouns, Verbs, Actions, Events |
| design |
Visual |
Themes |
| experiments |
Testing |
Experiments, Hypotheses, Variants |
| financial |
Money |
Invoices, Payments, Cards |
| integrations |
External |
Webhooks, Triggers, Providers |
| legal |
Contracts |
Contracts |
| marketing |
Demand gen |
Leads, Campaigns, Competitors |
| markets |
Market data |
Industries, Occupations, Tasks |
| product |
Offerings |
Products, Prices, Features, Offers |
| sales |
Revenue |
Deals, Quotes, Proposals |
| startup |
Venture |
Founders, CustomerSegments |
| success |
Customers |
Customers, Contacts, Subscriptions |
| tech |
Technology |
Technologies, Tools |
| tools |
Agent tools |
Browser, Computer |
| vibecode |
Code gen |
Sessions, Generations |
| web |
Websites |
Sites, Pages, Blogs, Docs |
| work |
Execution |
Tasks, Projects, Workflows, Agents |
Collection Structure
import type { CollectionConfig } from 'payload'
export const Things: CollectionConfig = {
slug: 'things',
// Admin UI
admin: {
useAsTitle: 'name',
group: 'Domain',
defaultColumns: ['name', 'status', 'createdAt'],
},
// Access control
access: {
read: () => true,
create: isAuthenticated,
update: isOwnerOrAdmin,
delete: isAdmin,
},
// Fields
fields: [
// ...
],
// Hooks
hooks: {
beforeChange: [],
afterChange: [],
},
}
Field Patterns
Required Fields (every collection)
fields: [
{
name: 'name',
type: 'text',
required: true,
},
]
Standard Optional Fields
{
name: 'description',
type: 'textarea',
},
{
name: 'status',
type: 'select',
defaultValue: 'draft',
options: ['draft', 'active', 'archived'],
},
Relationship Patterns
// Belongs to (many-to-one)
{
name: 'business',
type: 'relationship',
relationTo: 'businesses',
required: true,
},
// Has many (one-to-many via reverse)
// No field needed - query from child
// Many-to-many
{
name: 'industries',
type: 'relationship',
relationTo: 'industries',
hasMany: true,
},
// Polymorphic
{
name: 'actor',
type: 'relationship',
relationTo: ['agents', 'humans', 'serviceAccounts'],
},
MDX Content Field
{
name: 'content',
type: 'richText',
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
// MDX support
],
}),
},
Naming Conventions
Collection Slugs
- Plural, lowercase, hyphenated
customer-segments, journal-entries
Field Names
- camelCase
firstName, createdAt, isActive
Consistent Vocabulary
| Use |
Don't Use |
name |
title, label, heading |
description |
subtitle, summary, body |
status |
state, phase |
isActive |
active, enabled |
createdAt |
created, dateCreated |
Access Control Patterns
// Public read
access: {
read: () => true,
}
// Authenticated only
access: {
read: isAuthenticated,
create: isAuthenticated,
}
// Owner or admin
access: {
read: isOwnerOrAdmin,
update: isOwnerOrAdmin,
delete: isAdmin,
}
// Org-scoped
access: {
read: belongsToOrg,
create: belongsToOrg,
}
Hook Patterns
Auto-populate fields
hooks: {
beforeChange: [
({ data, req }) => {
if (!data.createdBy) {
data.createdBy = req.user?.id
}
return data
},
],
}
Cascade updates
hooks: {
afterChange: [
async ({ doc, req }) => {
// Update related records
await req.payload.update({
collection: 'related',
where: { parent: { equals: doc.id } },
data: { parentName: doc.name },
})
},
],
}
Sync to external systems
hooks: {
afterChange: [
async ({ doc, operation }) => {
if (operation === 'create') {
await stripe.customers.create({ ... })
}
},
],
}
mdxdb Integration
Collections sync bidirectionally with .mdx files via mdxdb:
.mdx file (Business-as-Code)
↕ mdxdb sync
Payload Collection (runtime)
↕ mdxdb query
ClickHouse (analytics)
MDXLD Frontmatter
---
$type: Product
$id: https://acme.com/products/widget
name: Widget Pro
price: 99
---
# {name}
Product description here...
Collection → Noun → Component
Every collection maps to:
- Noun type in schema.org.ai
- TypeScript interface in payload-types.ts
- Zod schema for validation
- mdxui component for rendering
// Collection
export const Products: CollectionConfig = { ... }
// → Generates TypeScript
interface Product {
id: string
name: string
price: number
}
// → Has Zod schema
const ProductSchema = z.object({ ... })
// → Renders via mdxui
<ProductCard {...product} />
<ProductRow {...product} />
<ProductPanel {...product} />
Creating New Collections
- Create file in appropriate domain:
collections/{domain}/{Name}.ts
- Follow field patterns above
- Add to domain index:
collections/{domain}/index.ts
- Add to main index:
collections/index.ts
- Run
pnpm generate:types to update payload-types.ts
- Create corresponding mdxui renderer if needed
Anti-Patterns
DON'T:
- Create duplicate fields across collections (normalize)
- Use inconsistent naming (follow vocabulary table)
- Skip access control (security first)
- Create deeply nested structures (flatten with relationships)
DO:
- Keep collections focused (single responsibility)
- Use relationships over embedding
- Add hooks for derived data
- Document with JSDoc comments
Converted and distributed by TomeVault — claim your Tome and manage your conversions.
1---2name: payload-collections3description: Use when designing, creating, or modifying Payload CMS collections in db.sb - covers field patterns, relationships, hooks, access control, and how collections map to Nouns in the business-as-code system4---56# Payload Collection Design78Patterns for designing Payload CMS collections that power the Business-as-Code system.910## Core Principle1112**Every Collection is a Noun. Every Noun renders beautifully.**1314```15Collection (Payload) → Noun (schema.org.ai) → Component (mdxui)16```1718## Collection Domains1920Collections are organized by domain in `packages/db.sb/src/collections/`:2122| Domain | Purpose | Examples |23|--------|---------|----------|24| admin | Platform admin | Users, Orgs, ApiKeys |25| ai | AI/ML experiments | ModelEvals, AIExperiments |26| api | API infrastructure | Proxies, Crawlers |27| business | Business entities | Businesses, Teams, Goals |28| code | Code artifacts | Workers, Artifacts |29| communications | Messaging | Messages, Sequences, Channels |30| compliance | Regulatory | Policies, Controls, Evidence |31| content | Documents | Documents, Files, Presentations |32| data | Ontology | Nouns, Verbs, Actions, Events |33| design | Visual | Themes |34| experiments | Testing | Experiments, Hypotheses, Variants |35| financial | Money | Invoices, Payments, Cards |36| integrations | External | Webhooks, Triggers, Providers |37| legal | Contracts | Contracts |38| marketing | Demand gen | Leads, Campaigns, Competitors |39| markets | Market data | Industries, Occupations, Tasks |40| product | Offerings | Products, Prices, Features, Offers |41| sales | Revenue | Deals, Quotes, Proposals |42| startup | Venture | Founders, CustomerSegments |43| success | Customers | Customers, Contacts, Subscriptions |44| tech | Technology | Technologies, Tools |45| tools | Agent tools | Browser, Computer |46| vibecode | Code gen | Sessions, Generations |47| web | Websites | Sites, Pages, Blogs, Docs |48| work | Execution | Tasks, Projects, Workflows, Agents |4950## Collection Structure5152```typescript53import type { CollectionConfig } from 'payload'5455export const Things: CollectionConfig = {56 slug: 'things',5758 // Admin UI59 admin: {60 useAsTitle: 'name',61 group: 'Domain',62 defaultColumns: ['name', 'status', 'createdAt'],63 },6465 // Access control66 access: {67 read: () => true,68 create: isAuthenticated,69 update: isOwnerOrAdmin,70 delete: isAdmin,71 },7273 // Fields74 fields: [75 // ...76 ],7778 // Hooks79 hooks: {80 beforeChange: [],81 afterChange: [],82 },83}84```8586## Field Patterns8788### Required Fields (every collection)8990```typescript91fields: [92 {93 name: 'name',94 type: 'text',95 required: true,96 },97]98```99100### Standard Optional Fields101102```typescript103{104 name: 'description',105 type: 'textarea',106},107{108 name: 'status',109 type: 'select',110 defaultValue: 'draft',111 options: ['draft', 'active', 'archived'],112},113```114115### Relationship Patterns116117```typescript118// Belongs to (many-to-one)119{120 name: 'business',121 type: 'relationship',122 relationTo: 'businesses',123 required: true,124},125126// Has many (one-to-many via reverse)127// No field needed - query from child128129// Many-to-many130{131 name: 'industries',132 type: 'relationship',133 relationTo: 'industries',134 hasMany: true,135},136137// Polymorphic138{139 name: 'actor',140 type: 'relationship',141 relationTo: ['agents', 'humans', 'serviceAccounts'],142},143```144145### MDX Content Field146147```typescript148{149 name: 'content',150 type: 'richText',151 editor: lexicalEditor({152 features: ({ defaultFeatures }) => [153 ...defaultFeatures,154 // MDX support155 ],156 }),157},158```159160## Naming Conventions161162### Collection Slugs163- Plural, lowercase, hyphenated164- `customer-segments`, `journal-entries`165166### Field Names167- camelCase168- `firstName`, `createdAt`, `isActive`169170### Consistent Vocabulary171172| Use | Don't Use |173|-----|-----------|174| `name` | `title`, `label`, `heading` |175| `description` | `subtitle`, `summary`, `body` |176| `status` | `state`, `phase` |177| `isActive` | `active`, `enabled` |178| `createdAt` | `created`, `dateCreated` |179180## Access Control Patterns181182```typescript183// Public read184access: {185 read: () => true,186}187188// Authenticated only189access: {190 read: isAuthenticated,191 create: isAuthenticated,192}193194// Owner or admin195access: {196 read: isOwnerOrAdmin,197 update: isOwnerOrAdmin,198 delete: isAdmin,199}200201// Org-scoped202access: {203 read: belongsToOrg,204 create: belongsToOrg,205}206```207208## Hook Patterns209210### Auto-populate fields211212```typescript213hooks: {214 beforeChange: [215 ({ data, req }) => {216 if (!data.createdBy) {217 data.createdBy = req.user?.id218 }219 return data220 },221 ],222}223```224225### Cascade updates226227```typescript228hooks: {229 afterChange: [230 async ({ doc, req }) => {231 // Update related records232 await req.payload.update({233 collection: 'related',234 where: { parent: { equals: doc.id } },235 data: { parentName: doc.name },236 })237 },238 ],239}240```241242### Sync to external systems243244```typescript245hooks: {246 afterChange: [247 async ({ doc, operation }) => {248 if (operation === 'create') {249 await stripe.customers.create({ ... })250 }251 },252 ],253}254```255256## mdxdb Integration257258Collections sync bidirectionally with .mdx files via mdxdb:259260```261.mdx file (Business-as-Code)262 ↕ mdxdb sync263Payload Collection (runtime)264 ↕ mdxdb query265ClickHouse (analytics)266```267268### MDXLD Frontmatter269270```mdx271---272$type: Product273$id: https://acme.com/products/widget274name: Widget Pro275price: 99276---277278# {name}279280Product description here...281```282283## Collection → Noun → Component284285Every collection maps to:2862871. **Noun type** in schema.org.ai2882. **TypeScript interface** in payload-types.ts2893. **Zod schema** for validation2904. **mdxui component** for rendering291292```typescript293// Collection294export const Products: CollectionConfig = { ... }295296// → Generates TypeScript297interface Product {298 id: string299 name: string300 price: number301}302303// → Has Zod schema304const ProductSchema = z.object({ ... })305306// → Renders via mdxui307<ProductCard {...product} />308<ProductRow {...product} />309<ProductPanel {...product} />310```311312## Creating New Collections3133141. Create file in appropriate domain: `collections/{domain}/{Name}.ts`3152. Follow field patterns above3163. Add to domain index: `collections/{domain}/index.ts`3174. Add to main index: `collections/index.ts`3185. Run `pnpm generate:types` to update payload-types.ts3196. Create corresponding mdxui renderer if needed320321## Anti-Patterns322323**DON'T:**324- Create duplicate fields across collections (normalize)325- Use inconsistent naming (follow vocabulary table)326- Skip access control (security first)327- Create deeply nested structures (flatten with relationships)328329**DO:**330- Keep collections focused (single responsibility)331- Use relationships over embedding332- Add hooks for derived data333- Document with JSDoc comments334335---336> Converted and distributed by [TomeVault](https://tomevault.io/claim/dot-do) — claim your Tome and manage your conversions.337<!-- tomevault:4.0:skill_md:2026-04-13 -->