Skip to content

Lifecycle hooks

Hooks run on the server inside the request, after validation (and after create-scope is applied), around the adapter write.

ts
admin.register("User", {
  beforeCreate: async (data) => {
    if (typeof data.password === "string") {
      data.passwordHash = await hash(data.password);
      delete data.password;
    }
    return data;
  },
  afterCreate: async (record) => {
    await notifyOps(`Created user ${record.id}`);
  },
  beforeUpdate: async (id, data) => data,
  afterUpdate: async (record) => {},
  beforeDelete: async (id) => {
    if (id === SUPER_ADMIN_ID)
      throw new Error("Cannot delete the root operator.");
  },
  afterDelete: async (id) => {},
});
HookWhenCan change data
beforeCreateAfter validate + create-scope, before createyes, return the object
afterCreateAfter insertno
beforeUpdateAfter the scoped row is found + validate + scope-field lock, before updateManyyes
afterUpdateAfter reloadno
beforeDeleteAfter the scoped row is found, before deleteManythrow to abort
afterDeleteAfter deleteno

Useful jobs

  • Hash a password (and keep password excluded from the schema)
  • Add derived values to fields that are already writable
  • Refuse to delete Linus
  • Fan out to email / queue after a successful write

Thrown errors become 500 INTERNAL_ERROR unless you throw an AdminApiError. Do not put secrets in the message.

Order on create

  1. Auth, permission, validate payload
  2. Apply simple scope equalities
  3. beforeCreate
  4. Adapter create
  5. afterCreate
  6. audit.write if configured

Hook output is validated again. Hooks cannot add hidden, read-only, unauthorized, or nested-write fields, and simple scope equalities are re-applied after the hook.

Released under the MIT License.