Triggers
Bind a function to an event in your project. The function fires automatically when a matching event happens — no polling, no glue code. Triggers can listen to database mutations, storage object lifecycle, or user registration.
Event families
Events come from three families. Each family binds to a different kind of target — collections for database, buckets for storage, nothing at all for auth (the event fires globally).
Database events
Each trigger is bound to a specific collection. Fires after the matching mutation commits.
db.record.createdA new record was inserted into the collectiondb.record.updatedAn existing record was updateddb.record.deletedA record was deletedStorage events
Each trigger is bound to a specific bucket. Fires after the object lifecycle change commits.
storage.object.createdA new object was uploaded and confirmedstorage.object.updatedAn object's metadata was edited, or the object was overwrittenstorage.object.deletedAn object was deletedAuth events
Auth triggers are not bound to a target. They fire globally on the matching event.
auth.user.registeredA new user account was createdauth.user.deletedA user account was permanently deleted (self-service via the SDK's deleteAccount, or admin action)auth.user.deleted is where your cleanup goes — nothing else does it
created_by, now pointing at a user that no longer exists.That makes this trigger the contract. Subscribe a Function to it and clean up with
deleteWhere(collection, { created_by: userId })— one call per collection, and it runs server-side, so a user closing the tab mid-flow cannot strand anything.If you subscribe nothing, your users' records stay indefinitely. Where deletion is a legal obligation rather than a nicety — GDPR erasure, Apple's account-deletion requirement — that is your obligation to meet, and this trigger is how.
auth.user.registered fires once per user, regardless of method
Creating a trigger
In the dashboard, go to Functions → pick a function → Triggers tab → Add Trigger. Pick the event type — the form adjusts to ask for the right target (collection for database events, bucket for storage events, nothing for auth events).
One example per family:
Function
send-welcome-email
Event
db.record.created
Collection
users
Function
process-image
Event
storage.object.created
Bucket
avatars
Function
send-welcome-email
Event
auth.user.registered
Target
(none — fires for every signup)
Replaying existing records
A trigger fires for changes that happen after it exists. Records already in the collection, seeded or imported before you bound the trigger, never passed through it. Replay runs the trigger's function once for each of them, oldest first, as if each had just been created or updated.
koolbase triggers replay <trigger-id> --project <project-id> --dry-run # counts only
koolbase triggers replay <trigger-id> --project <project-id>- Only
db.record.createdanddb.record.updatedtriggers can be replayed, and only while they are enabled. - Up to 1,000 records per run by default;
--limitgoes up to 5,000. Records beyond the limit are counted and reported, not queued. - Deliveries go through the retry queue, at most 25 every 30 seconds, so real events are never held up behind a replay. A failed delivery is retried and dead-lettered like any other.
- Each delivery gets the normal database event payload, plus
replay: trueand anevent_idthat is the same every time that record is replayed for that trigger.
{
"record": { "$id": "rec_123", "title": "..." },
"replay": true,
"event_id": "replay:<trigger-id>:rec_123"
}Make the function safe to run twice
event_id. If the function has side effects, such as sending an email or inserting rows, check event_id or look for work it already did, so a repeat does no harm.The same is available over the API: POST /v1/projects/{project_id}/triggers/{trigger_id}/replay?dry_run=true&limit=N.
Accessing trigger data
When a trigger fires, ctx.request carries the event name and the matching payload. The payload shape depends on the family.
Database event payload
export async function handler(ctx) {
// ctx.request.event — e.g. 'db.record.created'
// ctx.request.collection — the collection that was mutated, e.g. 'users'
// ctx.request.payload — the event payload (shape varies by event, see below)
const { event, collection, payload } = ctx.request;
// created / updated — payload.record is the record, flat, with
// $-prefixed system fields alongside your own fields:
// payload.record: { $id, $collection, $createdAt, $updatedAt, ...yourFields }
if (event === 'db.record.created' && collection === 'users') {
const record = payload.record;
const email = record.email; // your fields are top-level
const recordId = record.$id; // system fields are $-prefixed
// ... do something with the new user
}
// deleted — the record is gone; you get its id only:
// payload: { record_id }
if (event === 'db.record.deleted') {
const deletedId = payload.record_id;
}
return { ok: true };
}Note the two levels: payload.record
payload.record — reading fields off payload directly will give you undefined. The record itself is flat: your fields at top level, system fields prefixed with $ ($id, $collection, $createdAt, $updatedAt).Storage event payload
export async function handler(ctx) {
// ctx.request.event — e.g. 'storage.object.created'
// ctx.request.bucket — the bucket that fired, e.g. 'avatars'
// ctx.request.payload — the object record
const { event, bucket, payload } = ctx.request;
// payload: {
// id, bucket, path, size, content_type, etag,
// metadata: { ... },
// created_at, updated_at
// }
if (event === 'storage.object.created') {
console.log(`New upload in ${bucket}: ${payload.path} (${payload.size} bytes)`);
console.log('Custom metadata:', payload.metadata);
}
return { ok: true };
}Auth event payload
export async function handler(ctx) {
// ctx.request.event — 'auth.user.registered'
// ctx.request.payload — the new user record
const { event, payload } = ctx.request;
// auth.user.registered → payload: { id, email, name, providers, created_at }
// auth.user.deleted → payload: { reason: 'self_service', user: { email, id } }
// payload: { id, email, name, providers, created_at }
// For auth.user.deleted the payload is:
// { reason: 'self_service', user: { email, id } }
await sendEmail({
to: payload.email,
subject: 'Welcome!',
body: `Hi ${payload.name ?? 'there'}, welcome to MyApp.`,
});
return { ok: true };
}Handling multiple event types in one function
A single function can be bound to several triggers across families. Branch on ctx.request.event:
export async function handler(ctx) {
const { event, payload } = ctx.request;
switch (event) {
case 'auth.user.registered':
await sendEmail({ to: payload.email, subject: 'Welcome!' });
break;
case 'storage.object.created':
if (payload.content_type?.startsWith('image/')) {
await enqueueThumbnail(payload.bucket, payload.path);
}
break;
case 'db.record.deleted':
await invalidateCache(`${ctx.request.collection}:${payload.record_id}`);
break;
}
return { ok: true };
}Trigger observability
Each trigger row in the dashboard shows a 24-hour success rate with a colour-coded progress bar — green (≥80%), amber (50–80%), red (<50%). Triggers with a failure rate above 50% show a "Failing frequently" warning badge.