Skip to content

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 via ctx.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 cleanup
r.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 handler
r.job(
"send-incident-email",
{ trigger: { manual: true } },
async (ctx, payload) => {
/* ... */
},
);
// Enqueue from elsewhere
await ctx.jobs.enqueue("send-incident-email", {
incidentId: id,
recipients: subscribers,
});

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");
},
});
});