Pular para o conteúdo

Global Backend Hooks

Este conteúdo não está disponível em sua língua ainda.

Rebase provides two levels of entity lifecycle callbacks — both use the same CollectionCallbacks type from @rebasepro/types:

  • Per-collection callbacks: Defined on individual collection configurations. They run only for that collection.
  • Global callbacks: Defined on initializeRebaseBackend({ callbacks }). They fire on every collection, on every data path (REST API, WebSocket / realtime, server-side rebase.data).

Use global callbacks for:

  • PII masking — redact sensitive fields for non-admin callers across all collections.
  • Unified audit logging — log every create, update, or delete in one place.
  • Cross-cutting validation — enforce invariants that span multiple collections.

Pass the callbacks key to initializeRebaseBackend:

import { initializeRebaseBackend } from "@rebasepro/server";
const instance = await initializeRebaseBackend({
// ... other config
callbacks: {
afterRead({ row, context }) {
// Runs after every entity read, across all collections
return row;
},
beforeSave({ values, context }) {
// Runs before every entity save
return values;
}
}
});

import type { CollectionCallbacks } from "@rebasepro/types";
type CollectionCallbacks = {
afterRead?(props): Record<string, unknown>; // Transform row before returning to caller
beforeSave?(props): Partial<Values>; // Modify values before writing to DB
afterSave?(props): void; // Side-effects after successful save
afterSaveError?(props): void; // Side-effects after a failed save
beforeDelete?(props): boolean | void; // Return false or throw to block deletion
afterDelete?(props): void; // Side-effects after successful deletion
};

All callbacks may return a Promise (async) or a plain value (sync).


Each callback receives a single props object. Common fields:

FieldTypePresent in
collectionResolvedCollectionAll callbacks
pathstringAll callbacks
rowRecord<string, unknown>afterRead, beforeDelete, afterDelete
idstringbeforeSave (optional), afterSave, afterSaveError, beforeDelete, afterDelete
valuesEntityValuesbeforeSave, afterSave, afterSaveError
previousValuesEntityValues (optional)beforeSave, afterSave, afterSaveError
status"new" | "existing"beforeSave, afterSave, afterSaveError
contextRebaseCallContextAll callbacks

context.user contains the authenticated user (uid, roles, etc.), or is undefined for public requests.


[Client Request]
[Hono Router]
┌─────┴───────────────────────────────────────────────────────┐
│ 1. Global Callback: beforeSave (Blocking) │
│ 2. Collection Callback: beforeSave (Blocking) │
└─────┬───────────────────────────────────────────────────────┘
[Database Driver]
┌─────┴───────────────────────────────────────────────────────┐
│ 3. Start PostgreSQL Transaction │
│ 4. Set Config: app.user_id = '<uid>', app.user_roles = ... │
│ 5. Drizzle SQL execution & Postgres RLS evaluation │
│ 6. Commit Transaction │
└─────┬───────────────────────────────────────────────────────┘
┌─────┴───────────────────────────────────────────────────────┐
│ 7. Global Callback: afterSave │
│ 8. Collection Callback: afterSave │
└─────┬───────────────────────────────────────────────────────┘
[Client Response]

  • beforeSave, beforeDelete — blocking. If the callback throws, the operation is rejected with an HTTP 400 error response. The database write never happens.
  • afterRead — blocking. The returned row (or transformed row) is what the caller receives.
  • afterSave, afterDelete, afterSaveError — run after the transaction commits. They do not block the HTTP response.

Redact email addresses for non-admin callers across every collection:

import { initializeRebaseBackend } from "@rebasepro/server";
const instance = await initializeRebaseBackend({
// ... other config
callbacks: {
afterRead({ row, context }) {
const isAdmin = context.user?.roles?.includes("admin");
if (!isAdmin && row.email) {
return { ...row, email: "********" };
}
return row;
}
}
});

Log all deletions across every collection:

import { initializeRebaseBackend } from "@rebasepro/server";
const instance = await initializeRebaseBackend({
// ... other config
callbacks: {
afterDelete({ collection, id, context }) {
console.log(
`[AUDIT] User ${context.user?.uid} deleted ${collection.slug}/${id}`
);
}
}
});

Global callbacks fire for all collections. To scope logic to a single collection, check collection.slug or path:

callbacks: {
beforeSave({ collection, values, context }) {
if (collection.slug === "orders") {
if (!values.total || values.total <= 0) {
throw new Error("Order total must be positive");
}
}
return values;
}
}

For callbacks that only apply to a single collection, prefer per-collection callbacks instead.