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

HookRunsReturn
beforeOperationBefore any CRUD operationvoid
beforeReadBefore list/findById queriesvoid
afterReadAfter each document is readvoid
beforeCreateBefore inserting a documentmutated data
afterCreateAfter a document is createdvoid
beforeUpdateBefore updating a documentmutated data
afterUpdateAfter a document is updatedvoid
beforeDeleteBefore deleting a documentvoid
afterDeleteAfter a document is deletedvoid
afterSoftDeleteAfter a soft-deletevoid
beforeBulkCreateBefore a bulk insertmutated docs[]
afterBulkCreateAfter a bulk insertvoid
beforeBulkUpdateBefore a bulk updatemutated docs[]
afterBulkUpdateAfter a bulk updatevoid
beforeBulkDeleteBefore a bulk deletevoid
afterBulkDeleteAfter a bulk deletevoid

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 typeExtra properties
beforeCreatedata
afterCreatedoc
beforeUpdatedata, doc (existing document)
afterUpdatedoc, previousDoc
beforeDelete / afterDeletedoc
afterReaddoc
Bulk hooksdocs (array)

Execution Order

When both global and collection hooks are defined for the same event:

  1. Global hooks run first (in array order)
  2. 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 / beforeUpdate hooks MUST return the (possibly mutated) data object
  • after* hooks return void — they are for side-effects only
  • Each hook type accepts an array of functions — they run sequentially
  • Global hooks have access to ctx.collection.slug to identify which collection triggered the hook