Private packages

@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 by notify() 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).

On this page