@onyx/jobs
Typed fire-and-forget background jobs on Cloudflare Queues.
@onyx/jobs provides fire-and-forget background jobs on Cloudflare Queues. Each job pairs a Zod payload schema with a handler, a registry types the producer side, and payloads are re-validated on consume. Failures retry with backoff (5 attempts) and then land in the onyx-jobs-dlq dead letter queue. The queue consumer and bindings are already wired into the Worker; you only define jobs and enqueue them.
Enqueuing a job
enqueue(name, payload, options?) is typed from the registry: the payload type is inferred from the job's schema. It returns nothing, there is no result or completion signal. Anything that needs progress or an outcome should be a workflow instead.
Real usage from @onyx/auth, which enqueues the welcome email after signup:
import { enqueue } from '@onyx/jobs/client';
// Best-effort: a queue hiccup must never fail the signup request.
try {
await enqueue('send-welcome-email', { userId: user.id });
} catch (error) {
console.error(`[auth] Failed to enqueue welcome email for user ${user.id}:`, error);
}EnqueueOptions supports delaySeconds to hold the message before it becomes visible to the consumer.
Defining a new job
Create a definition in src/jobs/ with defineJob, which correlates the schema and handler so the consumer can dispatch without casts:
import { z } from 'zod';
import { defineJob } from '../define';
export const myJob = defineJob({
name: 'my-job',
schema: z.object({ entityId: z.string() }),
handle: async (payload, context) => {
// payload is z.output of the schema; context is a JobContext
},
});Then list it in src/registry.ts:
export const registry = {
[deliverNotificationJob.name]: deliverNotificationJob,
[sendWelcomeEmail.name]: sendWelcomeEmail,
[myJob.name]: myJob,
} as const;enqueue() and the consumer pick it up with full payload typing. Every handler receives a shared JobContext: db (a Database), mailer (a Mailer), and baseURL (the app origin, for links in emails). Handlers never touch bindings directly.
A useful convention from the built-in jobs: carry ids, not data. send-welcome-email carries only userId and re-reads the user, so a stale payload can never email an outdated address.
Built-in jobs
send-welcome-email: enqueued by the user-create hook in@onyx/auth.deliver-notification: enqueued bynotify()in@onyx/notifications; the fan-out logic lives in that package, this definition only gives it a seat on the queue.
Local dev simulates the queue. Production requires the onyx-jobs and onyx-jobs-dlq queues to be created once (see DEPLOY.md).