Operations
Two features for background work and gradual rollout of new behaviour.
jobs
Status: ✅ Stable
What: Background jobs (sending email, periodic cleanup, webhooks,
re-indexing). Built on BullMQ + Redis with lane routing — you
mark per job whether it should run in the api lane or in a separate
worker lane; the process topology follows.
How it works: r.job({ id: "send-email", runIn: "worker", handler: async (ctx, payload) => { ... } }) registers a job handler.
Trigger paths:
- Manual:
await ctx.jobs.enqueue("send-email", { ... }) - Cron:
r.job({ ..., schedule: "0 9 * * *" })runs daily at 9 AM - Event-driven:
r.job({ ..., on: "incident:created" })reacts to domain events
runIn: "api" runs in the same Bun process as HTTP (good for local
tests), runIn: "worker" requires separate worker containers —
production topology with horizontal scaling.
Logs: every run is recorded in job_runs + job_run_logs with
status (succeeded/failed/retrying). The built-in UI page
/admin/jobs shows the latest runs with a re-run button for failed
jobs.
Example:
// Cron job: nightly cleanupr.job({ id: "cleanup-old-attempts", runIn: "worker", schedule: "0 3 * * *", // daily 3 AM handler: async (ctx) => { await ctx.db.execute(sql` DELETE FROM delivery_attempts WHERE created_at < NOW() - INTERVAL '30 days' `); },});
// Trigger manually from a write handlerawait ctx.jobs.enqueue("send-incident-email", { incidentId: id, recipients: subscribers,});feature-toggles
Status: ✅ Stable
What: Per-tenant feature flags. Operator can enable a new feature for one tenant without a deploy, or roll out experimental behaviour gradually.
How it works: You declare r.toggleable({ id: "new-billing", default: false }). In code:
if (await ctx.toggles.isOn("new-billing")) { ... }. Operator flips
via feature_toggle_set events or an admin endpoint that invalidates
the cache per tenant. State lives in global_feature_state (for
platform defaults) and in a per-tenant override table.
Best practice: toggle death is real — when a toggle is 100 % rolled out, delete it from the code. In tests always set toggles explicitly (see coding standards: “Set/reset feature flags explicitly in tests”); never rely on defaults.
Example:
export const billingFeature = defineFeature("billing", (r) => { r.toggleable({ id: "new-billing-pipeline", default: false });
r.queryHandler({ qn: "billing:invoice:list", handler: async (ctx) => { if (await ctx.toggles.isOn("new-billing-pipeline")) { return ctx.db.list("invoice_v2"); } return ctx.db.list("invoice_v1"); }, });});See also
- Bundled-features overview
- Audit & security
- Recipe
lane-routing— pin jobs to api/worker