Skip to content

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 cleanup
r.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 handler
await 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