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
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---5
6# Payload Collection Design
7
8Patterns for designing Payload CMS collections that power the Business-as-Code system.
9
10## Core Principle
11
12**Every Collection is a Noun. Every Noun renders beautifully.**
13
14```
15Collection (Payload) → Noun (schema.org.ai) → Component (mdxui)
16```
17
18## Collection Domains
19
20Collections are organized by domain in `packages/db.sb/src/collections/`:
21
22| 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 |
49
50## Collection Structure
51
52```typescript
53import type { CollectionConfig } from 'payload'
54
55export const Things: CollectionConfig = {
56 slug: 'things',
57
58 // Admin UI
59 admin: {
60 useAsTitle: 'name',
61 group: 'Domain',
62 defaultColumns: ['name', 'status', 'createdAt'],
63 },
64
65 // Access control
66 access: {
67 read: () => true,
68 create: isAuthenticated,
69 update: isOwnerOrAdmin,
70 delete: isAdmin,
71 },
72
73 // Fields
74 fields: [
75 // ...
76 ],
77
78 // Hooks
79 hooks: {
80 beforeChange: [],
81 afterChange: [],
82 },
83}
84```
85
86## Field Patterns
87
88### Required Fields (every collection)
89
90```typescript
91fields: [
92 {
93 name: 'name',
94 type: 'text',
95 required: true,
96 },
97]
98```
99
100### Standard Optional Fields
101
102```typescript
103{
104 name: 'description',
105 type: 'textarea',
106},
107{
108 name: 'status',
109 type: 'select',
110 defaultValue: 'draft',
111 options: ['draft', 'active', 'archived'],
112},
113```
114
115### Relationship Patterns
116
117```typescript
118// Belongs to (many-to-one)
119{
120 name: 'business',
121 type: 'relationship',
122 relationTo: 'businesses',
123 required: true,
124},
125
126// Has many (one-to-many via reverse)
127// No field needed - query from child
128
129// Many-to-many
130{
131 name: 'industries',
132 type: 'relationship',
133 relationTo: 'industries',
134 hasMany: true,
135},
136
137// Polymorphic
138{
139 name: 'actor',
140 type: 'relationship',
141 relationTo: ['agents', 'humans', 'serviceAccounts'],
142},
143```
144
145### MDX Content Field
146
147```typescript
148{
149 name: 'content',
150 type: 'richText',
151 editor: lexicalEditor({
152 features: ({ defaultFeatures }) => [
153 ...defaultFeatures,
154 // MDX support
155 ],
156 }),
157},
158```
159
160## Naming Conventions
161
162### Collection Slugs
163- Plural, lowercase, hyphenated
164- `customer-segments`, `journal-entries`
165
166### Field Names
167- camelCase
168- `firstName`, `createdAt`, `isActive`
169
170### Consistent Vocabulary
171
172| 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` |
179
180## Access Control Patterns
181
182```typescript
183// Public read
184access: {
185 read: () => true,
186}
187
188// Authenticated only
189access: {
190 read: isAuthenticated,
191 create: isAuthenticated,
192}
193
194// Owner or admin
195access: {
196 read: isOwnerOrAdmin,
197 update: isOwnerOrAdmin,
198 delete: isAdmin,
199}
200
201// Org-scoped
202access: {
203 read: belongsToOrg,
204 create: belongsToOrg,
205}
206```
207
208## Hook Patterns
209
210### Auto-populate fields
211
212```typescript
213hooks: {
214 beforeChange: [
215 ({ data, req }) => {
216 if (!data.createdBy) {
217 data.createdBy = req.user?.id
218 }
219 return data
220 },
221 ],
222}
223```
224
225### Cascade updates
226
227```typescript
228hooks: {
229 afterChange: [
230 async ({ doc, req }) => {
231 // Update related records
232 await req.payload.update({
233 collection: 'related',
234 where: { parent: { equals: doc.id } },
235 data: { parentName: doc.name },
236 })
237 },
238 ],
239}
240```
241
242### Sync to external systems
243
244```typescript
245hooks: {
246 afterChange: [
247 async ({ doc, operation }) => {
248 if (operation === 'create') {
249 await stripe.customers.create({ ... })
250 }
251 },
252 ],
253}
254```
255
256## mdxdb Integration
257
258Collections sync bidirectionally with .mdx files via mdxdb:
259
260```
261.mdx file (Business-as-Code)
262 ↕ mdxdb sync
263Payload Collection (runtime)
264 ↕ mdxdb query
265ClickHouse (analytics)
266```
267
268### MDXLD Frontmatter
269
270```mdx
271---
272$type: Product
273$id: https://acme.com/products/widget
274name: Widget Pro
275price: 99
276---
277
278# {name}
279
280Product description here...
281```
282
283## Collection → Noun → Component
284
285Every collection maps to:
286
2871. **Noun type** in schema.org.ai
2882. **TypeScript interface** in payload-types.ts
2893. **Zod schema** for validation
2904. **mdxui component** for rendering
291
292```typescript
293// Collection
294export const Products: CollectionConfig = { ... }
295
296// → Generates TypeScript
297interface Product {
298 id: string
299 name: string
300 price: number
301}
302
303// → Has Zod schema
304const ProductSchema = z.object({ ... })
305
306// → Renders via mdxui
307<ProductCard {...product} />
308<ProductRow {...product} />
309<ProductPanel {...product} />
310```
311
312## Creating New Collections
313
3141. Create file in appropriate domain: `collections/{domain}/{Name}.ts`
3152. Follow field patterns above
3163. 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.ts
3196. Create corresponding mdxui renderer if needed
320
321## Anti-Patterns
322
323**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)
328
329**DO:**
330- Keep collections focused (single responsibility)
331- Use relationships over embedding
332- Add hooks for derived data
333- Document with JSDoc comments