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:typesto 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
微信扫一扫