| name | payload-collections |
| description | 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 system |
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