Dyrected
Model ContentContent Rules

Overview

Use hooks safely in Cloud as content rules, or run unrestricted function hooks in self-hosted Dyrected.

Hooks are where content changes get cleaned up, shaped, or reacted to.

The important first decision is runtime: Cloud hooks are content rules Dyrected can serialize and run on the hosted content backend, while self-hosted Dyrected can run arbitrary TypeScript functions inside your server.

By the end of this page, you should know which hook shape fits your runtime and where to put the behavior you need.

Start with the runtime boundary

Use Cloud-safe hooks when the behavior should survive sync:schema and run in Dyrected Cloud. In practice, that means hooks defined as serializable Jexl-style content rules, not arbitrary functions.

Use function hooks when the behavior needs arbitrary server code. Function hooks are self-hosted runtime behavior. Reach for them when the logic needs async work, external packages, database writes after persistence, custom integrations, or side effects that belong inside your application server.

If you are building for Cloud and need asynchronous side effects, do not assume a function hook will run on the hosted backend. Cloud content events and webhooks are the intended model for that work, but they are still coming soon.

The hook surfaces

Dyrected has four hook surfaces:

  • Collection hooks for document-level logic on repeatable content.
  • Global hooks for document-level logic on singleton data such as site settings.
  • Field hooks for logic that belongs to one field value.
  • Admin field hooks for reactive form behavior in the dashboard.

Admin hooks improve the editing experience, but they are not a security boundary. If a rule must hold on every API path, enforce it with access control, a server hook, or a Cloud-safe rule that runs on the backend.

Cloud hooks are content rules

In Cloud, hooks are content rules. They are serializable, so Dyrected can send them to Cloud during schema sync and evaluate them on the hosted content backend.

The recommended Cloud path is:

  1. use declarative Jexl-style strings for simple beforeRead, afterRead, and beforeChange transforms
  2. use declarative Jexl-style admin.hooks.onChange for browser-side form reactivity that should survive schema sync
  3. keep asynchronous side effects out of Cloud hooks until Cloud content events and webhooks are public
  4. keep arbitrary function hooks in self-hosted projects

For the full compatibility table, exact expression contexts, and what sync:schema preserves or strips, read Cloud-safe hooks.

Where each family fits

Collections

Collection hooks wrap the full lifecycle of a document in a collection:

  • beforeRead
  • afterRead
  • beforeChange
  • afterChange
  • beforeDelete
  • afterDelete

Reach for them when the logic depends on several fields, the whole document, or the surrounding operation.

Globals

Global hooks work the same way, but globals only support the phases that make sense for a singleton document:

  • beforeRead
  • afterRead
  • beforeChange
  • afterChange

There are no delete hooks because globals are updated in place rather than removed.

Fields

Field hooks stay close to one value:

  • beforeChange
  • afterRead

They are a good fit for value-level normalization, masking, formatting, and recursive behavior inside nested fields.

Admin field hooks

Admin field hooks run in the form UI:

  • admin.hooks.onChange
  • admin.hooks.options

Use them to derive a slug, recalculate a total, or update select options as editors change related fields.

admin.hooks.onChange also has a declarative string form, which makes it the recommended option when you want the same form behavior to survive Cloud schema sync.

  • Use a Cloud-safe hook when the behavior is a simple content rule that must run in Cloud.
  • Use a field hook when one value can be transformed on its own.
  • Use a collection or global hook when the logic depends on several fields or the surrounding operation.
  • Use an admin hook when the goal is immediate editor feedback in the dashboard.
  • Use access control when the question is who can read or write.
  • Use a self-hosted function hook when the behavior needs arbitrary TypeScript or server-side side effects.

Runtime rules worth remembering

  • Hooks in an array run in order.
  • When a before* hook returns a value, that value becomes the input to the next hook in the chain.
  • Declarative string hooks run synchronously.
  • Mixed arrays of functions and strings still chain in order.
  • Throwing inside beforeRead, beforeChange, or beforeDelete stops the operation.
  • afterChange and afterDelete are for side effects after persistence. Their return values are ignored, and their errors are isolated so a successful write is not turned into a 500.
  • Server hooks receive a database adapter, but only afterChange and afterDelete get a writable one.

Generated reference

The contracts below are generated from the public @dyrected/core exports by @dyrected/knowledge, so every hook signature stays in sync with the package. Use them when you need the exact argument and return types behind the guidance above.

AuthenticatedUser

Base shape of an authenticated user as decoded from the JWT.

The actual shape will include every field on your auth collection — this interface only guarantees the properties that Dyrected always stamps on the token. Extend it in your own codebase for stronger typing:

export interface AuthenticatedUser {
  /** The user's document ID in the database. */
  sub: string;
  /** The user's email address. */
  email?: string;
  /** Slug of the collection this user was authenticated against. */
  collection: string;
  /** Array of role strings, if your auth collection has a `roles` field. */
  roles?: string[];
  /** Any additional fields from the auth collection document. */
  [key: string]: unknown;
}
OptionDescription
sub (required)The user's document ID in the database.
email (optional)The user's email address.
collection (required)Slug of the collection this user was authenticated against.
roles (optional)Array of role strings, if your auth collection has a `roles` field.

CollectionAfterChangeHook

Runs after a document is created or updated in the database.

export type CollectionAfterChangeHook<TDoc extends object = Record<string, unknown>> = (args: {
  doc: TDoc;
  previousDoc?: TDoc;
  req: HookRequestContext;
  user?: AuthenticatedUser;
  operation: "create" | "update";
  db: DatabaseAdapter;
}) => void | Promise<void>;

CollectionAfterDeleteHook

Runs after a document has been deleted from the database.

export type CollectionAfterDeleteHook<TDoc extends object = Record<string, unknown>> = (args: {
  id: string;
  doc: TDoc;
  req: HookRequestContext;
  user?: AuthenticatedUser;
  db: DatabaseAdapter;
}) => void | Promise<void>;

CollectionAfterReadHook

Runs after a document (or list of documents) is fetched from the database, before the response is sent to the client.

export type CollectionAfterReadHook<TDoc extends object = Record<string, unknown>> = (args: {
  doc: TDoc;
  req: HookRequestContext;
  user?: AuthenticatedUser;
  db: ReadonlyDatabaseAdapter;
}) => TDoc | Promise<TDoc>;

CollectionAfterReadHookEntry

Exported type from @dyrected/core.

export type CollectionAfterReadHookEntry<
  TDoc extends object = Record<string, unknown>,
> = CollectionAfterReadHook<TDoc> | DeclarativeHookExpression;

CollectionBeforeChangeHook

Runs before a document is created or updated in the database.

export type CollectionBeforeChangeHook<TDoc extends object = Record<string, unknown>> = (args: {
  data: Partial<TDoc>;
  doc?: TDoc;
  req: HookRequestContext;
  user?: AuthenticatedUser;
  operation: "create" | "update";
  db: ReadonlyDatabaseAdapter;
}) => Partial<TDoc> | void | Promise<Partial<TDoc> | void>;

CollectionBeforeChangeHookEntry

Exported type from @dyrected/core.

export type CollectionBeforeChangeHookEntry<
  TDoc extends object = Record<string, unknown>,
> = CollectionBeforeChangeHook<TDoc> | DeclarativeHookExpression;

CollectionBeforeDeleteHook

Runs before a document is deleted from the database.

export type CollectionBeforeDeleteHook<TDoc extends object = Record<string, unknown>> = (args: {
  id: string;
  doc: TDoc;
  req: HookRequestContext;
  user?: AuthenticatedUser;
  db: ReadonlyDatabaseAdapter;
}) => void | Promise<void>;

CollectionBeforeReadHook

Runs before Dyrected queries the database for a list or single-document fetch.

Return a new where query object to override or extend the current filter. Return undefined (or nothing) to leave the query unchanged.

export type CollectionBeforeReadHook = (args: {
  req: HookRequestContext;
  query?: Record<string, unknown>;
  user?: AuthenticatedUser;
  db: ReadonlyDatabaseAdapter;
}) => Record<string, unknown> | void | Promise<Record<string, unknown> | void>;

CollectionBeforeReadHookEntry

Exported type from @dyrected/core.

export type CollectionBeforeReadHookEntry =
  | CollectionBeforeReadHook
  | DeclarativeHookExpression;

DeclarativeHookExpression

Exported type from @dyrected/core.

export type DeclarativeHookExpression = string;

FieldAdminHooks

Exported type from @dyrected/core.

export type FieldAdminHooks<TValue> = {
  admin?: {
    hooks?: {
      onChange?: FieldAdminOnChangeHook<TValue> | DeclarativeHookExpression;
    };
  };
};

FieldAdminOnChangeHook

Exported type from @dyrected/core.

export type FieldAdminOnChangeHook<TValue = unknown> = (
  args: FieldAdminOnChangeHookArgs<TValue>,
) => unknown;

FieldAdminOnChangeHookArgs

Exported interface from @dyrected/core.

export interface FieldAdminOnChangeHookArgs<TValue = unknown> {
  /** Current field value in the form state. */
  value: TValue;
  /** Current values for sibling fields at the same nesting level. */
  siblingData: Record<string, unknown>;
  /** Current values for the entire form. */
  data: Record<string, unknown>;
  /** Imperative setter for async or derived updates. */
  setValue: (value: unknown) => void;
}
OptionDescription
value (required)Current field value in the form state.
siblingData (required)Current values for sibling fields at the same nesting level.
data (required)Current values for the entire form.
setValue (required)Imperative setter for async or derived updates.

FieldAdminOptionsHook

Exported type from @dyrected/core.

export type FieldAdminOptionsHook = (
  args: FieldAdminOptionsHookArgs,
) => FieldAdminOptionsHookResult | Promise<FieldAdminOptionsHookResult>;

FieldAdminOptionsHookArgs

Exported interface from @dyrected/core.

export interface FieldAdminOptionsHookArgs {
  /** Current values for sibling fields at the same nesting level. */
  siblingData: Record<string, unknown>;
  /** Current values for the entire form. */
  data: Record<string, unknown>;
}
OptionDescription
siblingData (required)Current values for sibling fields at the same nesting level.
data (required)Current values for the entire form.

FieldAdminOptionsHookResult

Exported type from @dyrected/core.

export type FieldAdminOptionsHookResult = Array<
  string | { label: string; value: unknown }
>;

FieldAfterReadHook

Exported type from @dyrected/core.

export type FieldAfterReadHook<
  TValue = unknown,
  TDoc extends object = Record<string, unknown>,
> = (args: FieldAfterReadHookArgs<TValue, TDoc>) => unknown;

FieldAfterReadHookArgs

Exported interface from @dyrected/core.

export interface FieldAfterReadHookArgs<
  TValue = unknown,
  TDoc extends object = Record<string, unknown>,
> {
  /** Raw stored field value before this hook transforms it. */
  value: TValue;
  /** Full document currently being returned to the caller. */
  doc: TDoc;
  /** Authenticated user requesting the document, if any. */
  user?: AuthenticatedUser;
  /** Read-only database adapter for related lookups. */
  db: ReadonlyDatabaseAdapter;
}
OptionDescription
value (required)Raw stored field value before this hook transforms it.
doc (required)Full document currently being returned to the caller.
user (optional)Authenticated user requesting the document, if any.
db (required)Read-only database adapter for related lookups.

FieldBeforeChangeHook

Exported type from @dyrected/core.

export type FieldBeforeChangeHook<
  TValue = unknown,
  TDoc extends object = Record<string, unknown>,
> = (args: FieldBeforeChangeHookArgs<TValue, TDoc>) => unknown;

FieldBeforeChangeHookArgs

Exported interface from @dyrected/core.

export interface FieldBeforeChangeHookArgs<
  TValue = unknown,
  TDoc extends object = Record<string, unknown>,
> {
  /** Current field value after previous hooks in the chain. */
  value: TValue;
  /** Existing stored document before the write, if this is an update. */
  originalDoc?: TDoc;
  /** Full incoming payload being written. */
  data: Record<string, unknown>;
  /** Authenticated user performing the write, if any. */
  user?: AuthenticatedUser;
  /** Read-only database adapter for related lookups. */
  db: ReadonlyDatabaseAdapter;
}
OptionDescription
value (required)Current field value after previous hooks in the chain.
originalDoc (optional)Existing stored document before the write, if this is an update.
data (required)Full incoming payload being written.
user (optional)Authenticated user performing the write, if any.
db (required)Read-only database adapter for related lookups.

FieldHook

Exported type from @dyrected/core.

export type FieldHook<TDoc extends object = Record<string, unknown>, TValue = unknown> = FieldBeforeChangeHook<
  TValue,
  TDoc
>;

FieldHooks

Exported type from @dyrected/core.

export type FieldHooks<TValue> = {
  hooks?: {
    beforeChange?: Array<FieldBeforeChangeHook<TValue> | DeclarativeHookExpression>;
    afterRead?: Array<FieldAfterReadHook<TValue>>;
  };
};

GlobalAfterChangeHook

Runs after the global document is updated. Side-effects only.

export type GlobalAfterChangeHook<TDoc extends object = Record<string, unknown>> = (args: {
  doc: TDoc;
  previousDoc?: TDoc;
  req: HookRequestContext;
  user?: AuthenticatedUser;
  operation: "update";
  db: DatabaseAdapter;
}) => void | Promise<void>;

GlobalAfterReadHook

Runs after the global document is fetched, before the response is sent.

export type GlobalAfterReadHook<TDoc extends object = Record<string, unknown>> = (args: {
  doc: TDoc;
  req: HookRequestContext;
  user?: AuthenticatedUser;
  db: ReadonlyDatabaseAdapter;
}) => TDoc | Promise<TDoc>;

GlobalAfterReadHookEntry

Exported type from @dyrected/core.

export type GlobalAfterReadHookEntry<
  TDoc extends object = Record<string, unknown>,
> = GlobalAfterReadHook<TDoc> | DeclarativeHookExpression;

GlobalBeforeChangeHook

Runs before the global document is updated. Operation is always 'update' (globals cannot be created or deleted).

export type GlobalBeforeChangeHook<TDoc extends object = Record<string, unknown>> = (args: {
  data: Partial<TDoc>;
  doc?: TDoc;
  req: HookRequestContext;
  user?: AuthenticatedUser;
  operation: "update";
  db: ReadonlyDatabaseAdapter;
}) => Partial<TDoc> | void | Promise<Partial<TDoc> | void>;

GlobalBeforeChangeHookEntry

Exported type from @dyrected/core.

export type GlobalBeforeChangeHookEntry<
  TDoc extends object = Record<string, unknown>,
> = GlobalBeforeChangeHook<TDoc> | DeclarativeHookExpression;

GlobalBeforeReadHook

See: CollectionBeforeReadHook

export type GlobalBeforeReadHook = CollectionBeforeReadHook;

GlobalBeforeReadHookEntry

Exported type from @dyrected/core.

export type GlobalBeforeReadHookEntry = CollectionBeforeReadHookEntry;

HookFunction

Exported type from @dyrected/core.

export type HookFunction<TDoc extends object = Record<string, unknown>> = (args: {
  data?: Partial<TDoc>;
  doc?: TDoc;
  user?: AuthenticatedUser;
  req?: HookRequestContext;
  operation?: "create" | "update" | "delete";
  db?: DatabaseAdapter;
  [key: string]: unknown;
}) => unknown | Promise<unknown>;

HookRequestContext

Minimum HTTP request context passed to every server-side hook and resolver.

The full Web Standard Request is available as raw when you need it, but most hooks only need query (URL search parameters).

export interface HookRequestContext {
  /** Parsed URL query-string parameters, e.g. `{ page: '2', search: 'hello' }`. */
  query: Record<string, string>;
  /** Incoming HTTP headers, lowercased. */
  headers: Record<string, string>;
  /** The raw Web Standard `Request` object. Useful for streaming or advanced header inspection. */
  raw?: Request;
}
OptionDescription
query (required)Parsed URL query-string parameters, e.g. `{ page: '2', search: 'hello' }`.
headers (required)Incoming HTTP headers, lowercased.
raw (optional)The raw Web Standard `Request` object. Useful for streaming or advanced header inspection.

On this page

Dyrected| Cloud

Get your backend ready in minutes

Use a managed database, storage, APIs, and admin dashboard without setting up the infrastructure yourself.

Set Up My Backend