NestJS
NestJS. Worker Manager ships a NestJS module plus a plain adapter you can wire manually.
Install
Also install the adapter for the HTTP platform your Nest app uses (Express is the default):
Supported NestJS versions
@worker-manager/nestjs supports NestJS 9, 10, 11 and 12. The suite runs against both 11 and 12 on
every CI build.
NestJS 12 ships as ESM only, so a Nest 12 application has to be ESM itself. @worker-manager/nestjs
is published as CommonJS and its named exports are importable from an ESM app, so nothing about
the setup below changes on Nest 12.
Module-based setup (recommended)
Register WorkerManagerModule.forRoot() in your root module, then WorkerManagerModule.forFeature() per queue from the feature module.
forRoot() options, all optional:
forFeature() options (pass either name or queue):
name: queue name registered withBullModule.registerQueue. The module resolves the instance from Nest's DI container.queue: a queue instance to register directly, instead of resolving it byname. See Queues with the same name below.adapter:BullMQAdapterorBullAdapter.options: queue adapter options likereadOnlyModeordescription.
To register several queues at once, pass multiple option objects:
Queues with the same name
@nestjs/bullmq builds a queue's DI token from its name alone, and the prefix is not part of it. So if you run the same queue name under two prefixes (a common multi-tenant setup), both share one DI token and a name lookup can only ever return one of them. Registering both by name makes one queue shadow the other on the board.
Pass the instances directly via queue instead. Hold the queues somewhere you control (a provider, a service, wherever you created them) and hand them to forFeature:
The board keys entries by prefix + name, so the two show up as tenant-a:emails and tenant-b:emails. Set each adapter's prefix to match the queue's own prefix so the labels line up.
Async configuration
WorkerManagerModule.forRootAsync() takes imports plus one of useFactory (with inject),
useClass or useExisting. The latter two name a provider implementing
WorkerManagerOptionsFactory:
Authentication
auth puts @worker-manager/auth in front of every board route: the
page, the API and the static assets. It works the same on Express and Fastify, and the mount path
includes the Nest global prefix.
Basic
Unauthenticated requests get 401 with a WWW-Authenticate: Basic challenge and
{ "error": { "key": "ERRORS.UNAUTHORIZED" } }. Credentials are compared in constant time.
Keycloak, configured from ConfigService
Browsers go through the OIDC authorization code flow with PKCE and get an encrypted session
cookie; API clients may send Authorization: Bearer <access token> instead. A user without one of
requiredRoles gets 403 with ERRORS.FORBIDDEN. The module also serves GET /queues/auth/me
and GET /queues/auth/logout. See Keycloak auth for the Keycloak client
settings.
Token, with a login form for browsers
Scripts send the token in X-Board-Token or as a Bearer token; API calls without it get 401
JSON. A browser opening /admin/queues is sent to a login form at /admin/queues/auth/login,
which trades the token for an encrypted SameSite=Strict session cookie. See
Token auth.
Custom
authenticate(req) resolves the user or null (401); onUnauthenticated(req, res) can answer
instead. See Custom auth, including a Cloudflare Access example.
Testing with Test.createTestingModule
The module works in a Nest TestingModule without an explicit adapter, on Express and Fastify
alike:
compile() builds providers before createNestApplication() tells the app which HTTP platform it
runs on, so the board mounts on a stand-in and the real adapter is picked in app.init(). The
board injected with @InjectWorkerManager() is usable during compile(); queues added to it then
are applied once the adapter exists. Before 2.2.0 this threw could not pick a server adapter for the "unknown" HTTP platform, and passing adapter explicitly was the workaround.
If the suite fails loading @worker-manager/fastify with Must use import to load ES Module, see
Troubleshooting.
PostgreSQL-backed queues
BullMQ v6 can store queues in PostgreSQL (see PostgreSQL backend). Such a queue has no Redis connection and usually no DI token, so pass the instance:
@nestjs/bullmq has no way to pass a backend factory, so PostgreSQL queues and their workers
are created with bullmq directly, as above, rather than through BullModule.registerQueue and
@Processor. Redis queues can keep using @nestjs/bullmq in the same app; if your Redis queues
must stay on BullMQ v5, install v6 under an alias for the Postgres ones
("bullmq-v6": "npm:bullmq@^6", then import { Queue } from 'bullmq-v6').
If the queue is created inside a provider instead, inject the board with @InjectWorkerManager() and
call board.addQueue(new BullMQAdapter(queue)) from onModuleInit. Redis and PostgreSQL queues can share one board; the datastore panel reports
Postgres stats for the Postgres queue.
Running next to @bull-board/nestjs
Migrating one service at a time? Worker Manager's module registers its providers under its own
DI tokens (worker_manager_*) since 1.0.1, so the legacy @bull-board/nestjs module and this one
can be imported in the same app on different routes, each with its own forFeature queues.
You can inject the board instance anywhere:
Several boards
Give each forRoot() a name and its own route to mount several boards in one application,
for example one per team or one per datastore:
- A named board's providers use the tokens
worker_manager_options:<name>,worker_manager_adapter:<name>andworker_manager_instance:<name>(getWorkerManagerToken(name)returns the last one). The unnamed board keeps the plainworker_manager_*tokens, so an existingforRoot()withoutnameis unchanged and can sit next to named ones. forFeature(name, ...queues)registers into the named board;forFeature(...queues)still targets the unnamed one.- With
forRootAsync(),namegoes on the async options, next touseFactory: it decides the tokens, so it has to be known before the factory runs. - Inject a named board with
@InjectWorkerManager(name):
Names are letters, digits, ., _ and -. Give every board its own route and do not nest one
under another (/queues and /queues/ops): on Express the outer board's middleware also matches
the inner board's paths and answers for them.
Each board applies its own auth on its own prefix. With Keycloak:
- Register every board's redirect URI in the Keycloak client,
https://<host>/<route>/auth/callbackfor eachroute(and a post-logout redirect URI ofhttps://<host>/<route>/). WithpublicUrl, set it per board. - A named board's session cookie is
wm_session_<name>instead ofwm_session, scoped to the board's path, unlessauth.cookie.nameis set. Boards never share a session: log in once per board. The cookie secret can be the same.
pg-boss board
engine: 'pg-boss' mounts a board over a pg-boss schema, with
the same shell, auth and routing as a BullMQ board. It needs pg-boss 12.24 or later, Node 22.12 or
later and one more package:
@worker-manager/pg-boss is an optional peer: it is only loaded when a board asks for the pg-boss
engine. The board never migrates, supervises or creates anything in the database.
See the pg-boss engine for connection modes, indexes and what the board shows.
Hand it the app's own pg-boss instance, already started, as a provider token:
pgBoss takes:
readOnly: true makes the whole board read-only. The board lists the queues of its schema, so
queues at the root and forFeature() into a pg-boss board are configuration errors. The engine's
own read pool is closed with the application; the instance and a pool you pass stay yours.
@InjectWorkerManager('pgboss') resolves { engine, close() }.
Plain adapter setup
If you'd rather wire the server adapter yourself (custom middleware, existing Nest conventions), do it in a module's configure():
Full runnable examples
- Redis with
@nestjs/bullmqand basic auth:examples/nestjs/redis - PostgreSQL-backed BullMQ v6 queues:
examples/nestjs/postgres - Keycloak auth configured from
ConfigService:examples/nestjs/keycloak - Fastify platform with a custom auth hook:
examples/nestjs/fastify-custom-auth - The pg-boss board over the app's own pg-boss instance:
examples/nestjs/pg-boss
Next steps
- UIConfig: title, logo, locale, polling.
- Read-only mode: disable destructive actions.
- Visibility guard: scope visible queues per request.
- Formatters: rewrite job fields for the UI.