Upgrading to 1.0
What changed at the first major — and why most apps need no code changes.
OpenQueue 1.0 freezes the public surface. It also splits the BullMQ engine out of
@openqueue/core so the core runtime is transport-neutral, and it lifts the Node
floor to match h3 v2. This guide covers every change, but leads with the part
that matters most: the common path is unchanged.
Requirements
- Node
>= 20.11.1or Bun>= 1.2for the Node-capable packages (@openqueue/core,@openqueue/sdk,@openqueue/client,@openqueue/workbench,@openqueue/world-bullmq,@openqueue/world-postgres,@openqueue/worker). 20.11.1 is the floor because h3 v2 requires it; 22+ is recommended. Node 18 and 20-before-20.11 are both past end-of-life. @openqueue/cliremains Bun-only (bun >= 1.2). The worker's production artifact (built by the CLI via Nitro) runs on Node^20.19 || >=22.12or Bun.- All
@openqueue/*packages release in lockstep — every one jumps from0.1.xto1.0.0together.
The 90% path is unchanged
If your app is a worker.config.ts with the redis sugar and tasks discovered
from a directory, nothing changes. This config compiles and runs identically
on 1.0:
import { defineConfig } from '@openqueue/sdk';
export default defineConfig({
namespace: 'my-app',
dirs: ['./worker'],
redis: { url: process.env.REDIS_URL! },
});task(), task.trigger(), task.schedules.*, enqueueFlow(), the error
taxonomy, postgresAdapter, drains, and the openqueue dev | build | start
commands are all unchanged. The redis: { url } block is now sugar that
@openqueue/worker resolves to a BullMQ world for you — you don't install or
import anything new for it.
Bump your engines to Node 20.11.1 / Bun 1.2, update to 1.0.0, and you're done.
The rest of this page is for apps that reached below the config surface.
Build output
openqueue build now compiles the worker into a self-contained Nitro
node-server bundle at .output (configurable via build.outDir), replacing
the old .openqueue/build/manifest.mjs. openqueue start prefers this artifact
and falls back to booting from source when it is absent — the commands and your
config are unchanged.
- The artifact runs on Node
^20.19 || >=22.12or Bun (Nitro's floor, above the package's own>= 20.11.1). - A direct
node .output/server/index.mjsboot defaults to port 3000 unlessPORTis set;openqueue startinjectsPORT/NITRO_PORTto preserve the8090default. See Deployment.
New packages
The BullMQ transport used to live inside @openqueue/core. At 1.0 it moves into
its own package so core carries no bullmq/ioredis and can run on the edge.
| Package | What it is |
|---|---|
@openqueue/world-bullmq | The default delivery path — a Redis/BullMQ transport paired with a write-through durable store. The redis config sugar resolves to this; @openqueue/worker depends on it so you never install it directly for the common path. |
@openqueue/world-postgres | A self-migrating Postgres world — a SELECT … FOR UPDATE SKIP LOCKED transport plus a Drizzle-backed store, with zero Redis. One database powers both jobs and history. |
@openqueue/client | A fetch-only HTTP client for a deployed worker. Zero Redis/DB — safe in Node, Bun, and edge runtimes. Re-exported as @openqueue/sdk/client. |
@openqueue/sdk deliberately does not re-export world-bullmq — keeping
light installs light is the point of the split. The worker and CLI carry the
default path.
Removed core APIs
These low-level exports left @openqueue/core. You almost certainly never called
them directly — the config-driven path and task()/trigger() never touched
them. If you did wire a runtime by hand, here are the replacements:
| Removed | Replacement |
|---|---|
configureEnqueue | createClient({ host }) from @openqueue/client / @openqueue/sdk/client — connection-free HTTP dispatch. |
createQueueClient, QueueClient, QueueClientOptions | Removed — there is no in-process producer client at 1.0. Dispatch over HTTP with createClient({ host }); a host app embedding a producer plane uses createControlRuntime from @openqueue/core/control (Two-plane). |
createConnection, closeConnection, QueueConnection | The world owns its connections. worldBullmq({ url }) opens and closes them; pass your own producer/consumer to keep ownership. |
createQueue, defaultJobOptions, queue | The transport owns queues. Reach them via worldBullmq or isBullmqTransport(runtime.transport). |
createWorker, CreateWorkerOptions | createQueueWorker({ world, tasks }), or just run @openqueue/worker. |
createQueueSchedules | Schedules come off the runtime: runtime.schedules / client.schedules. |
publishQueueCatalog, readQueueCatalog, resolveQueueCatalogTask, queueCatalogKey, queueCatalogPublishedAtKey | The Redis catalog format is now private to world-bullmq. Read the catalog via client.catalog / runtime.catalog. |
bullPrefix, DEFAULT_BULL_PREFIX, redisKey | BullMQ key details live in the world: worldBullmq({ prefix }) (absorbs the old bullPrefix). |
QueueDefinitionInput | Removed (unreferenced helper type; QueueDefinition remains). |
createQueueWorker now takes a world (a WorldFactory) instead of a redis
connection:
// before (0.1.x)
const client = createQueueClient({ redis: { url } });
// after (1.0) — connection-free (recommended for app code)
import { createClient } from '@openqueue/client';
const client = createClient({ host: 'https://worker.example.com' });Type changes
EnqueueResultis{ runId, jobId }. The duplicatedidandtransportJobIdfields are gone.runIdis the durable inspect handle;jobIdis the transport handle. (QueueRun.transportJobIdstays.)ttlis removed fromEnqueueOptionsand task definitions. It was a silent no-op on every transport — a typed feature that did nothing — so there is nothing to migrate. For run-duration limits, enforce them in the handler (e.g.AbortSignal.timeoutaround external calls).storageis flattened.storage: { adapter: postgresAdapter(...) }becomesstorage: postgresAdapter(...).QueueConfigcollapsed intoOpenQueueConfig. The alias is gone; importOpenQueueConfig.WorkerConfig,TelemetryConfig, anddefineWorkerConfigare removed. This was dead scaffolding — OpenTelemetry is wired through the process's own tracer provider and the@opentelemetry/*peer deps, not a config block.WorldContextis flattened to{ namespace: string }(no morectx.namespace.namespace) — relevant only if you author a world.
Workbench
@openqueue/workbench/honois now@openqueue/workbench/h3. The adapter moved from Hono to h3. Update the import and hand it anH3app instead of a Hono one; the dashboard, routes, and auth are otherwise identical.- Node floor is 20.11.1 (h3 v2's minimum). This is the one package whose floor moved specifically for h3.
- Dependencies dropped:
hono,@hono/zod-openapi, and@scalar/hono-api-referenceare gone. OpenAPI is now generated framework-free from the route table via zod'sz.toJSONSchema. workbench.authaccepts basic credentials or an orderedAuthStrategywalk. The old{ username, password }shape still works (it's sugar for[httpBasic(...)]).- Runs page on non-BullMQ worlds. The dashboard's Runs page reads BullMQ
directly, so on a
world-postgres(or any non-BullMQ world) it renders empty — run history for those worlds lives on the control API (/openqueue/v1) and the client, not the dashboard. The worker logs a one-line pointer at boot. See Workbench → On non-BullMQ worlds.
Security behavior changes
- jose upgraded to v6. No API change on our surface; it drops the Node 18 support we no longer need.
- Trusted-issuer JWTs with a configured
tenantClaimnow fail closed. If a strategy is configured to scope by a tenant claim and the token doesn't carry a string claim, the request is rejected (401) rather than granted cross-tenant access. Deployments that relied on claim-less tokens passing will now see 401s — this is intended. - The
/openqueue/v1control API is fail-closed in production. When neitherapi.tokennorapi.authis set, the API is open in development and returns 401 whenNODE_ENV=production. It also fails closed on a runtime that hidesprocess(edge/serverless), where a non-production environment can't be confirmed — setapi.token/api.authexplicitly there. An emptyapi.auth: []always 401s. - Wire envelopes tightened: unknown control-API routes return a
404not_foundenvelope, and an unsupported transport capability (e.g. flows on a Postgres world) returns501 unsupported_capabilityinstead of a generic error.
New surfaces
1.0 also freezes several surfaces that landed during the umbrella program:
@openqueue/core/auth— theAuthStrategyordered walk plus shipped strategies (apiKey,httpBasic,jwtHmac,oidc,localDev,none). Used byapi.authandworkbench.auth.@openqueue/core/control—createControlRuntime, a producer/observer runtime over a world with no consumers, for the edge/serverless two-plane topology. Import-clean of ioredis/bullmq.@openqueue/core/world— the world contract (OpenQueueWorld,QueueTransport,WorldFactory,WORLD_SPEC_VERSION) and store-author helpers (filterRuns,filterSchedules,runFromSnapshot). See Worlds.@openqueue/workbench/control—buildControlApp, the h3 control-API app.@openqueue/client/@openqueue/sdk/client— the connection-free HTTP client and the/openqueue/v1wire contract.- The
/openqueue/v1control API — mounted by the worker unconditionally.
What the semver guarantee covers
Frozen at 1.0 (breaking changes to these ship only in 2.0):
@openqueue/sdk/@openqueue/coreindex and subpaths (./auth,./control,./drizzle,./types,./world).@openqueue/client(.and./wire) and the/openqueue/v1wire contract.- The world contract at
WORLD_SPEC_VERSION = 1. OpenQueueConfig.- The CLI commands.
@openqueue/workbenchpackage exports.
Not frozen: the Workbench dashboard internals (the React app and its private routes evolve freely). h3 stays pinned to its release-candidate line until a stable h3 v2 ships — that unpin is a 1.0.x follow-up, not a breaking change.
After 1.0, any feat! (breaking) commit computes the next major.