Entity Callbacks
Overview
Section titled “Overview”Callbacks let you hook into the entity lifecycle to:
- Transform data before saving (computed fields, slugification)
- Validate business rules beyond schema validation
- Trigger side effects after writes (send emails, sync APIs, update caches)
- Filter/transform data after reading
Defining Callbacks
Section titled “Defining Callbacks”const articlesCollection: EntityCollection = { slug: "articles", callbacks: { beforeSave: async ({ values, entityId, status }) => { // Auto-generate slug from title if (values.title) { values.slug = values.title .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/(^-|-$)/g, ""); }
// Set timestamps if (status === "new") { values.created_at = new Date(); } values.updated_at = new Date();
return values; },
afterSave: async ({ values, entityId }) => { // Send notification console.log(`Article ${entityId} saved: ${values.title}`); },
beforeDelete: async ({ entityId }) => { // Prevent deletion of published articles // Throw to block the deletion },
afterRead: async ({ entity }) => { // Transform data after loading return entity; } }, properties: { /* ... */ }};Callback Reference
Section titled “Callback Reference”beforeSave
Section titled “beforeSave”Called before an entity is written to the database. Return the modified values.
beforeSave: async ({ values, // Entity values entityId, // Entity ID (null for new entities) status, // "new" | "existing" | "copy" previousValues, // Previous values (for updates) context // Full Rebase context}) => { // Return modified values return { ...values, updated_at: new Date() };}Throw an error to block the save:
beforeSave: async ({ values }) => { if (values.price < 0) { throw new Error("Price cannot be negative"); } return values;}afterSave
Section titled “afterSave”Called after a successful save. Use for side effects.
afterSave: async ({ values, // Saved values entityId, // Entity ID previousValues, // Previous values (null for new entities) status, // "new" | "existing" | "copy" context}) => { // Send webhook await fetch("https://api.slack.com/webhook", { method: "POST", body: JSON.stringify({ text: `New article: ${values.title}` }) });}afterSaveError
Section titled “afterSaveError”Called when a save operation fails.
afterSaveError: async ({ values, entityId, error, context}) => { console.error("Save failed:", error);}afterRead
Section titled “afterRead”Called after reading entities from the database. Transform the data for display.
afterRead: async ({ entity, // The entity to transform context}) => { // Add computed fields return { ...entity, values: { ...entity.values, displayName: `${entity.values.first_name} ${entity.values.last_name}` } };}beforeDelete
Section titled “beforeDelete”Called before an entity is deleted. Throw to block deletion.
beforeDelete: async ({ entityId, entity, context}) => { if (entity.values.status === "published") { throw new Error("Cannot delete published articles. Unpublish first."); }}afterDelete
Section titled “afterDelete”Called after a successful deletion.
afterDelete: async ({ entityId, entity, context}) => { // Cleanup related data console.log(`Article ${entityId} deleted`);}Property Callbacks
Section titled “Property Callbacks”You can also define callbacks at the property level for field-specific transformations:
properties: { email: { type: "string", name: "Email", callbacks: { beforeSave: ({ value }) => value?.toLowerCase().trim(), afterRead: ({ value }) => value // Could decrypt, etc. } }}Next Steps
Section titled “Next Steps”- Security Rules — Row Level Security
- Entity History — Audit trail