Lifecycle Hooks
Hooks in Kyro CMS allow you to intercept, validate, or modify data at various stages of the document lifecycle. They are essential for tasks like hashing passwords, auto-generating fields (like slugs or timestamps), interacting with third-party APIs, or enforcing complex validation rules.
There are three levels of hooks in Kyro CMS:
- Collection Hooks: Run on collections (e.g., Posts, Users).
- Global Hooks: Run on singletons (e.g., Settings, Navigation).
- Field Hooks: Run on specific fields within a Collection or Global schema.
Hook Arguments (HookArgs)
All hooks receive a unified HookArgs object. This object contains context about the current operation:
export interface HookArgs<T = any> {
collection?: string;
global?: string;
data?: T; // The incoming data (before saving)
originalDoc?: T; // The existing document (before changes)
doc?: T; // The resulting document (after changes/reads)
req: Request; // The incoming HTTP request
user?: User; // The currently authenticated user
operation: 'create' | 'read' | 'update' | 'delete';
tenantId?: string; // ID of the tenant (if multi-tenancy is active)
field?: string; // Field name (for Field Hooks)
value?: any; // Current field value (for Field Hooks)
previousValue?: any; // Previous field value (for Field Hooks)
siblingData?: Record<string, any>; // Data of sibling fields
context?: Record<string, any>; // Custom context passed between hooks
}If a hook returns a value (or a Promise resolving to a value), that value replaces the current data or value in the lifecycle. If a hook returns void, the pipeline continues without modifying the data.
1. Collection Hooks
Collection Hooks run during the lifecycle of documents within a specific collection. They are defined in your CollectionConfig under the hooks property.
Standard Lifecycle Hooks
beforeValidate: Runs before the incoming data is validated against the schema. Ideal for sanitizing or casting data.beforeChange: Runs immediately before a document is saved (either created or updated) to the database. Useful for auto-generating fields or hashing passwords.afterChange: Runs immediately after a document has been saved. Useful for triggering side effects like sending emails, triggering webhooks, or syncing to external services.beforeRead: Runs before a document is fetched from the database. Can be used to enforce read-level row security or modify query parameters.afterRead: Runs after a document is fetched, but before it is returned to the client. Useful for removing sensitive fields (like passwords) or computing virtual fields.beforeDelete: Runs right before a document is permanently deleted from the database.afterDelete: Runs immediately after a document has been deleted. Useful for cleaning up associated files or external records.
Auth-Specific Hooks
If your collection is authentication-enabled (auth: true), you also have access to:
beforeLoginafterLoginafterLogoutafterRefreshafterForgotPassword
Example: Collection Hooks
import { CollectionConfig } from '@kyro-cms/core';
import bcrypt from 'bcryptjs';
export const Users: CollectionConfig = {
slug: 'users',
auth: true,
fields: [
{ name: 'email', type: 'text', required: true },
{ name: 'password', type: 'text', required: true },
{ name: 'fullName', type: 'text' },
],
hooks: {
beforeChange: [
async ({ data, operation }) => {
// Automatically hash passwords on create or when updated
if (operation === 'create' || (operation === 'update' && data.password)) {
const salt = await bcrypt.genSalt(10);
data.password = await bcrypt.hash(data.password, salt);
}
return data; // Always return the mutated data!
}
],
afterChange: [
async ({ doc, operation }) => {
if (operation === 'create') {
console.log(`New user registered: ${doc.email}`);
}
}
],
afterRead: [
({ doc }) => {
// Strip the password hash before sending to the client
if (doc?.password) {
delete doc.password;
}
return doc;
}
]
}
};2. Global Hooks
Globals are singletons (e.g., Site Settings). Their hooks mirror the Collection lifecycle but exclude deletion and auth-specific events.
Available Global Hooks:
beforeValidatebeforeChangeafterChangebeforeReadafterRead
Example: Global Hooks
export const SiteSettings: GlobalConfig = {
slug: 'site-settings',
fields: [
{ name: 'siteName', type: 'text' },
{ name: 'maintenanceMode', type: 'checkbox' }
],
hooks: {
afterChange: [
async ({ doc }) => {
if (doc.maintenanceMode) {
// Notify the infrastructure team or clear caches
await fetch('https://api.example.com/clear-cache', { method: 'POST' });
}
}
]
}
};3. Field Hooks
Field hooks allow you to isolate logic to a single field, making your schema cleaner and your logic highly reusable.
Available Field Hooks:
beforeValidatebeforeChangeafterChangeafterRead
Example: Field Hooks (Auto-generating a Slug)
Instead of managing slug generation in a Collection hook where you have to look up the field manually, you can attach it directly to the slug field.
import { Field } from '@kyro-cms/core';
export const SlugField: Field = {
name: 'slug',
type: 'text',
hooks: {
beforeChange: [
({ value, siblingData }) => {
// If a slug was manually provided, use it
if (value) {
return value.toLowerCase().replace(/\s+/g, '-');
}
// Otherwise, auto-generate it from the title
if (siblingData?.title) {
return siblingData.title.toLowerCase().replace(/\s+/g, '-');
}
return value;
}
]
}
};TIP
In Field Hooks, you receive value (the value of the current field) and siblingData (a map of the data for all other fields at the same nesting level). This is incredibly useful for computing derived values.
4. The HookPipeline Pattern
Under the hood, Kyro CMS executes hooks using the HookPipeline class (import { HookPipeline } from '@kyro-cms/core').
HookPipeline implements a type-safe Pipeline / Chain of Responsibility Pattern:
import { HookPipeline } from '@kyro-cms/core';
// Programmatically run a pipeline of hooks over custom data
const finalData = await HookPipeline.run(myHooksArray, {
collection: 'posts',
operation: 'create',
data: rawPostData,
req: currentRequest,
});Benefits of HookPipeline:
- Sequential Execution: Hooks run in exact array order, passing mutated data to the next step.
- Error Isolation: Async errors in hooks are wrapped with collection context details, preventing unhandled promise rejections.
- Type Safety: Fully typed with generics
HookPipeline<T>.