Operations
Two features for background work and gradual rollout of new behaviour.
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(name, { trigger, runIn?, ... }, handler)
registers a job. The trigger shape decides how the job fires:
- Manual:
r.job("send-email", { trigger: { manual: true } }, handler)— invoked viactx.jobs.enqueue("send-email", payload). - Cron:
r.job("nightly-cleanup", { trigger: { cron: "0 9 * * *" } }, handler)— fires on the schedule (leader-elected, single worker). - Event-driven:
r.job("on-incident", { trigger: { on: incidentCreated } }, handler)— fires when the named event is emitted.
runIn: "api" runs in the same Bun process as HTTP (good for local
tests), runIn: "worker" (the default) 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( "cleanup-old-attempts", { trigger: { cron: "0 3 * * *" }, runIn: "worker" }, async (ctx) => { await ctx.db.execute(sql` DELETE FROM delivery_attempts WHERE created_at < NOW() - INTERVAL '30 days' `); },);
// Manual job, triggered from a write handlerr.job( "send-incident-email", { trigger: { manual: true } }, async (ctx, payload) => { /* ... */ },);
// Enqueue from elsewhereawait ctx.jobs.enqueue("send-incident-email", { incidentId: id, recipients: subscribers,});feature-toggles
Section titled “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({ default: false }) once
per feature — the feature name itself is the toggle ID. In code:
if (await ctx.toggles.isOn("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({ default: false });
r.queryHandler({ name: "invoice-list", schema: z.object({}), handler: async (_query, ctx) => { if (await ctx.toggles.isOn("billing")) { return ctx.db.list("invoice_v2"); } return ctx.db.list("invoice_v1"); }, });});See also
Section titled “See also”- Bundled-features overview
- Audit & security
- Recipe
lane-routing— pin jobs to api/worker