Collections
Advanced features
Collections Deep Dive
Advanced collection features for building robust data models.
File-Based Collections
Collections are defined in separate files for better organization:
collections/
├── users.ts
├── posts.ts
├── comments.ts
config/
├── config.ts// collections/posts.ts
export const posts = collection({
slug: 'posts',
fields: { ... }
})
// config/database.ts
import { posts } from '../collections/posts'
export const { collections } = defineConfig({
collections: [posts]
})Relations
Collections can reference each other through relation fields.
One-to-One
// collections/users.ts
export const users = collection({
slug: 'users',
fields: {
profile: field({
type: relation('profiles', { singular: true })
})
}
})
// collections/profiles.ts
export const profiles = collection({
slug: 'profiles',
fields: {
userId: field({
type: relation('users')
})
}
})One-to-Many
// collections/posts.ts
export const posts = collection({
slug: 'posts',
fields: {
authorId: field({
type: relation('users')
}),
// Reverse relation (virtual field)
comments: field({
type: reverseRelation({
collection: 'comments',
via: 'postId'
})
})
}
})
// collections/comments.ts
export const comments = collection({
slug: 'comments',
fields: {
postId: field({
type: relation('posts')
})
}
})Many-to-Many
export const posts = collection({
slug: 'posts',
fields: {
categories: field({
type: relation('categories', {
many: true,
through: 'postCategories' // Join table
})
})
}
})Hooks
Hooks let you run code at specific points during collection operations.
Hook Types
export const posts = collection({
slug: 'posts',
hooks: {
beforeCreate: [
async ({ data }) => {
// Modify data before create
data.slug = slugify(data.title)
}
],
afterCreate: [
async ({ result }) => {
// Run side effects after create
await searchEngine.index(result)
}
],
beforeUpdate: [
async ({ data, where }) => {
// Validate or transform before update
await logUpdate('posts', where.id, data)
}
],
afterUpdate: [
async ({ result }) => {
// Invalidate caches, notify, etc.
await cache.invalidate(`posts:${result.id}`)
}
]
},
fields: { ... }
})Multiple Hooks
Hooks can be arrays for composition:
hooks: {
beforeCreate: [
validateTitle,
generateSlug,
setDefaults
],
afterUpdate: [
updateSearchIndex,
invalidateCache
]
}
// Defined as separate functions
function validateTitle({ data }) {
if (data.title.length < 3) {
throw new Error('Title is too short')
}
}Hook Context
Hooks receive context about the operation:
beforeCreate: async ({ data, context }) => {
// data: Input data for the operation
// context: { collection, locale, currentUser, ... }
}Plugins
Plugins extend collections with additional functionality.
Applying Plugins
Plugins are applied at configuration level:
// config/database.ts
export const { collections } = defineConfig({
plugins: [
timestampsPlugin(),
softDeletePlugin({
collections: ['posts', 'comments']
}),
seoPlugin({
collections: ['posts', 'pages']
})
],
collections: [posts, comments]
})What Plugins Can Do
Plugins can:
- Add fields to collections (timestamps, soft delete, SEO fields)
- Add field types (custom field types available globally)
- Add collections (create new collections automatically)
- Add hooks (inject behavior at operation boundaries)
- Add operations (new methods on collections)
- Add utilities (helper functions)
Native Plugins
Built-in plugins for common needs:
- Versioning - Track all changes to records
- Cache - Intelligent caching layer
- Soft Delete - Safe deletion with recovery
- SEO - Meta tags and optimization
Metadata
Collections can include metadata for better documentation and tooling.
export const posts = collection({
slug: 'posts',
// Human-readable labels
label: 'Blog Post',
plural: 'Posts',
description: 'Blog posts and articles',
// Admin UI configuration
admin: {
icon: 'document',
defaultSort: 'createdAt',
defaultSortDirection: 'desc'
},
// Custom metadata
metadata: {
featureFlags: ['full-text-search', 'revisions'],
permissions: ['create', 'read', 'update', 'delete']
},
fields: { ... }
})Validation
Cross-field and custom validation:
export const posts = collection({
slug: 'posts',
validation: {
// Cross-field validation
password: {
matches: (value, { data }) => {
return value === data.confirmPassword
}
}
},
fields: {
password: field({ type: text() }),
confirmPassword: field({ type: text() })
}
})Collection Operations
Collections provide automatic CRUD operations:
// Find
await collections.posts.findMany({
where: { status: { equals: 'published' } },
orderBy: { createdAt: 'desc' },
limit: 10
})
// Find unique
await collections.posts.findUnique({
where: { id: 1 }
})
// Create
await collections.posts.create({
data: {
title: 'Hello World',
content: '...'
}
})
// Update
await collections.posts.update({
where: { id: 1 },
data: { title: 'Updated' }
})
// Delete
await collections.posts.delete({
where: { id: 1 }
})
// Count
await collections.posts.count({
where: { status: { equals: 'published' } }
})Collections are Composable: Everything about collections—from fields to hooks to plugins—is designed to be composed from simple, reusable pieces. This makes your data model maintainable and scalable.