Lifecycle Hooks
Lifecycle hooks let you run server-side JavaScript before or after record operations in a collection — without modifying the server code.
Hooks are stored in the database and executed synchronously on every matching operation.
Hook events
Section titled “Hook events”| Event | When it runs | Abort support | Record mutations |
|---|---|---|---|
beforeCreate | Before a record is inserted | Yes — throw to abort | Yes — mutate record to modify data |
afterCreate | After a record is inserted | No — errors are logged only | Ignored |
beforeUpdate | Before a record is updated | Yes — throw to abort | Yes — mutate record (the patch) |
beforeDelete | Before a record is soft-deleted | Yes — throw to abort | Ignored |
Security sandbox
Section titled “Security sandbox”Hooks run in a best-effort sandbox: the following globals are shadowed to undefined inside hook code: Bun, process, require, fetch, eval, Function, XMLHttpRequest, WebSocket, Worker, Blob, File. Hooks cannot access these APIs.
Hooks that exceed 500 ms of synchronous execution are killed with an error.
This is a JavaScript-level sandbox — admin-created hooks should still be treated as trusted code. A subprocess-based OS-level sandbox is planned for a future release.
Function signature
Section titled “Function signature”Hook code is a function body executed with two named arguments:
record— the record data object. ForbeforeCreateandbeforeUpdate, mutations to this object are applied to the write. ForbeforeDelete, this is a snapshot of the record being deleted.context— metadata about the operation:{collection: string, // Collection nameevent: string, // "beforeCreate" | "afterCreate" | "beforeUpdate" | "beforeDelete"userId: string | null // ID of the authenticated user making the request, or null}
Example hooks
Section titled “Example hooks”Validate a required field:
if (!record.title || !record.title.trim()) { throw new Error("title is required");}Set a default value:
if (!record.status) { record.status = "draft";}Reject deletes for published records:
if (record.status === "published") { throw new Error("Cannot delete published records.");}Log after creation (non-fatal):
// afterCreate — errors here are logged, not surfaced to the clientconsole.log("New record created:", record._id);Abort behavior
Section titled “Abort behavior”For beforeCreate, beforeUpdate, and beforeDelete:
- If your hook throws an error, the operation is aborted.
- A
400 Bad Requestresponse is returned with the error message from the thrown error. - No database write occurs.
For afterCreate:
- Errors are caught and logged to the server app log.
- The request succeeds regardless of hook errors.
Hook limitations (MVP)
Section titled “Hook limitations (MVP)”- Hooks are synchronous —
async/await,Promise,fetch, and other async patterns are not supported. - Hooks run in the same process as the DB handler — heavy computation will block writes.
- No access to external modules or Node/Bun APIs — only pure JS logic.
- No persistent state between hook invocations.
Managing hooks via Admin API
Section titled “Managing hooks via Admin API”All hook management requires the ADMIN_SECRET.
List hooks
Section titled “List hooks”GET /api/v1/admin/hooksAuthorization: Bearer <admin-secret>Filter by collection:
GET /api/v1/admin/hooks?collection=postsResponse:
{ "items": [ { "id": "01JKX...", "collection": "posts", "event": "beforeCreate", "code": "if (!record.title) throw new Error(\"title required\");", "enabled": true, "created_at": 1709900000000 } ]}Create a hook
Section titled “Create a hook”POST /api/v1/admin/hooksAuthorization: Bearer <admin-secret>Content-Type: application/json
{ "collection": "posts", "event": "beforeCreate", "code": "if (!record.title) throw new Error(\"title is required\");", "enabled": true}Fields:
collection(required) — collection nameevent(required) —"beforeCreate","afterCreate","beforeUpdate", or"beforeDelete"code(required) — JS function body stringenabled(optional, defaulttrue) — whether the hook runs
Response 201: the created hook object.
Get a hook
Section titled “Get a hook”GET /api/v1/admin/hooks/:idAuthorization: Bearer <admin-secret>Update a hook
Section titled “Update a hook”PATCH /api/v1/admin/hooks/:idAuthorization: Bearer <admin-secret>Content-Type: application/json
{ "code": "if (!record.title) throw new Error(\"title is required\");", "enabled": false}Both fields are optional. Only provided fields are updated.
Delete a hook
Section titled “Delete a hook”DELETE /api/v1/admin/hooks/:idAuthorization: Bearer <admin-secret>Managing hooks in Studio
Section titled “Managing hooks in Studio”Open a collection in Studio and click the Hooks tab. From there you can:
- See all hooks with their event type, enabled status, and code preview
- Add a new hook with the event selector and code editor
- Toggle a hook on or off without deleting it
- Edit hook code
- Delete a hook
Security considerations
Section titled “Security considerations”- Hook code runs inside the BunBase server process with full access to the JavaScript runtime.
- Only trusted administrators (those with
ADMIN_SECRET) can create or modify hooks. - Never expose the Admin API to untrusted users.
- Hook code cannot directly access the SQLite database or BunBase internals — it only receives and mutates the
recordobject. - All hooks for a collection are loaded from the database and cached in memory. Changes via the Admin API invalidate this cache immediately.
- Hooks are deleted automatically when their collection is dropped.