Hooks
Hooks allow you to execute custom logic at various points in the request lifecycle. Lucent supports two levels of hooks:
- Global hooks — run for every collection, defined in
lucent.config.ts - Collection hooks — run for a specific collection, defined in
defineCollection()
Global hooks always run before collection hooks of the same type.
Global Hooks
Global hooks are defined on the globalHooks property of your Lucent config. They receive the same context as collection hooks (including collection, user, and lucent) so you can implement cross-cutting concerns like audit trails, analytics, or logging in one place.
import { defineLucentConfig, logger, type GlobalHooksConfig } from "@codesordinatestudio/lucent";
export default defineLucentConfig({
// ...other config
globalHooks: {
// Runs after every document is created in any collection
afterCreate: [
async (ctx) => {
logger.info(
{ collection: ctx.collection.slug, userId: ctx.user?.id, docId: (ctx.doc as any).id },
"audit:create",
);
},
],
// Runs after every document is updated in any collection
afterUpdate: [
async (ctx) => {
logger.info(
{ collection: ctx.collection.slug, userId: ctx.user?.id, docId: (ctx.doc as any).id },
"audit:update",
);
},
],
// Runs after every document is deleted in any collection
afterDelete: [
async (ctx) => {
logger.info(
{ collection: ctx.collection.slug, userId: ctx.user?.id, docId: (ctx.doc as any).id },
"audit:delete",
);
},
],
} satisfies GlobalHooksConfig,
});
Supported Global Hook Types
| Hook | Runs | Return |
|---|---|---|
beforeOperation | Before any CRUD operation | void |
beforeRead | Before list/findById queries | void |
afterRead | After each document is read | void |
beforeCreate | Before inserting a document | mutated data |
afterCreate | After a document is created | void |
beforeUpdate | Before updating a document | mutated data |
afterUpdate | After a document is updated | void |
beforeDelete | Before deleting a document | void |
afterDelete | After a document is deleted | void |
afterSoftDelete | After a soft-delete | void |
beforeBulkCreate | Before a bulk insert | mutated docs[] |
afterBulkCreate | After a bulk insert | void |
beforeBulkUpdate | Before a bulk update | mutated docs[] |
afterBulkUpdate | After a bulk update | void |
beforeBulkDelete | Before a bulk delete | void |
afterBulkDelete | After a bulk delete | void |
Collection Hooks
Collection hooks are defined in defineCollection() and only run for that collection. They support the same hook types as global hooks.
import { defineCollection } from "@codesordinatestudio/lucent";
export const Posts = defineCollection({
slug: "posts",
fields: [
{ name: "title", type: "text", required: true },
{ name: "status", type: "select", options: ["draft", "published"] },
{ name: "author", type: "relationship", relationTo: "users", required: true },
],
hooks: {
// Before hooks can modify the data and must return it
beforeCreate: [
async ({ data, user }) => {
return { ...data, author: user?.id };
},
],
beforeUpdate: [
async ({ data }) => {
return { ...data, updatedAt: new Date().toISOString() };
},
],
// Before delete can prevent deletion by throwing
beforeDelete: [
async ({ doc }) => {
if ((doc as any).status === "published") {
throw new Error("Cannot delete published posts");
}
},
],
// After hooks are side-effect only (void return)
afterCreate: [
async ({ doc }) => {
await notifyAdmins(`New post: ${(doc as any).title}`);
},
],
afterUpdate: [
async ({ doc }) => {
await updateSearchIndex(doc);
},
],
afterDelete: [
async ({ doc }) => {
await cleanupRelated(doc);
},
],
},
});
Hook Context
All hooks receive a context object with the following properties:
beforeCreate: [
async (ctx) => {
const { req, user, collection, operation, lucent, data } = ctx;
// req - The incoming Request object
// user - Authenticated user (or null)
// collection - The collection definition (slug, fields, etc.)
// operation - "list" | "read" | "create" | "update" | "delete"
// lucent - Lucent Local API (find, findById, create, update, delete)
// data - The incoming document data (before hooks only)
return data;
},
];
Additional context properties vary by hook type:
| Hook type | Extra properties |
|---|---|
beforeCreate | data |
afterCreate | doc |
beforeUpdate | data, doc (existing document) |
afterUpdate | doc, previousDoc |
beforeDelete / afterDelete | doc |
afterRead | doc |
| Bulk hooks | docs (array) |
Execution Order
When both global and collection hooks are defined for the same event:
- Global hooks run first (in array order)
- Collection hooks run second (in array order)
For before hooks that return data (e.g. beforeCreate, beforeUpdate), the output of each hook is passed as input to the next — global hooks feed into collection hooks.
Async Hooks
All hooks can be async:
beforeCreate: [
async ({ data }) => {
const validated = await validateData(data);
return validated;
},
];
Error Handling
Throw errors to stop the pipeline:
beforeCreate: [
async ({ user }) => {
if (!user) {
throw new Error("Authentication required");
}
return {};
},
];
Rules
beforeCreate/beforeUpdatehooks MUST return the (possibly mutated) data objectafter*hooks returnvoid— they are for side-effects only- Each hook type accepts an array of functions — they run sequentially
- Global hooks have access to
ctx.collection.slugto identify which collection triggered the hook