Hooks

Hooks are lifecycle functions that run before or after CRUD operations on a collection. They let you inject business logic — validating data, enriching records, triggering side effects — without modifying the generated routes.

Registering Hooks

Use app.hooks(collectionSlug, hooks) in src/hooks.ts:

// src/hooks.ts
import { app } from "./app";

app.hooks("todos", {
  beforeCreate: async (ctx) => {
    // Auto-assign the current user as author if not provided
    if (!ctx.data.author) ctx.data.author = ctx.user?.id || "anonymous";
    return ctx.data;
  },
  afterCreate: async (ctx) => {
    console.log(`Todo created: ${ctx.data.id}`);
  },
});

Available Hooks

HookWhen it runsCan modify data?
beforeCreateBefore a record is insertedYes — return modified data
afterCreateAfter a record is insertedNo (side effects only)
beforeUpdateBefore a record is updatedYes — return modified data
afterUpdateAfter a record is updatedNo
beforeDeleteBefore a record is deletedNo
afterDeleteAfter a record is deletedNo

The Hook Context

Each hook receives a HookContext:

interface HookContext {
  request: Request;         // The raw HTTP request
  user: AuthUser | null;    // The authenticated user
  radiant: RadiantRuntime;  // The runtime instance
  collection: string;       // The collection slug
  data: any;                // The data being created/updated/deleted
}

Before Hooks (Data Modification)

Before hooks can modify the data before it reaches the database. Return the modified data from the function:

app.hooks("posts", {
  beforeCreate: async (ctx) => {
    // Generate a slug from the title
    ctx.data.slug = ctx.data.title.toLowerCase().replace(/\s+/g, "-");
    // Set the author
    ctx.data.author = ctx.user?.id;
    return ctx.data;
  },
  beforeUpdate: async (ctx) => {
    // Update the slug if the title changed
    if (ctx.data.title) {
      ctx.data.slug = ctx.data.title.toLowerCase().replace(/\s+/g, "-");
    }
    return ctx.data;
  },
});

If you don't return anything, the original data is used unchanged.

After Hooks (Side Effects)

After hooks run after the database operation completes. Use them for side effects — sending emails, logging, cache invalidation, triggering webhooks:

import { Resend } from "resend";
const resend = new Resend(process.env.RESEND_API_KEY);

app.hooks("users", {
  afterCreate: async (ctx) => {
    // Send a welcome email
    await resend.emails.send({
      from: "welcome@myapp.com",
      to: ctx.data.email,
      subject: "Welcome!",
      html: `<h1>Hi ${ctx.data.name}</h1><p>Welcome aboard!</p>`,
    });
  },
  afterDelete: async (ctx) => {
    console.log(`User deleted: ${ctx.data.id}`);
    // Clean up related data
    await ctx.radiant.delete("sessions", ctx.data.id);
  },
});

Throwing Errors

Any hook can throw an error to abort the operation. The error is returned to the client as an error response:

app.hooks("todos", {
  beforeCreate: async (ctx) => {
    if (!ctx.data.title || ctx.data.title.trim().length === 0) {
      throw new Error("Title is required");
    }
    if (ctx.data.title.length > 200) {
      throw new Error("Title must be 200 characters or less");
    }
    return ctx.data;
  },
});

Multiple Collections

Register hooks for as many collections as needed:

app.hooks("users", {
  beforeCreate: async (ctx) => {
    // Normalise email
    ctx.data.email = ctx.data.email.toLowerCase().trim();
    return ctx.data;
  },
});

app.hooks("todos", {
  beforeCreate: async (ctx) => {
    if (!ctx.data.author) ctx.data.author = ctx.user?.id;
    return ctx.data;
  },
  afterUpdate: async (ctx) => {
    // Notify the author if the todo was completed
    if (ctx.data.completed) {
      // ... send notification
    }
  },
});

Globals

Globals support hooks too. The beforeUpdate and afterUpdate hooks fire when the global document is saved:

app.hooks("siteSettings", {
  beforeUpdate: async (ctx) => {
    // Log maintenance mode changes
    if (ctx.data.maintenanceMode) {
      console.log("Maintenance mode enabled");
    }
    return ctx.data;
  },
});