Queues
BullMQ + QueueManager
Named queues and workers; centralized lifecycle, retries, and shutdown. See Queues.
Background jobs
Products need work off the HTTP critical path: email, notifications, retries. BoringStack puts that work in the API with BullMQ on Valkey, shared queue patterns, and dispatch helpers. You ship events and mail without standing up a separate job platform first.
BullMQ
queue engine
Valkey
queue backend
Inline
fallback when off
Queues
Named queues and workers; centralized lifecycle, retries, and shutdown. See Queues.
Precompiled templates, queue-aware sendTemplate(). Cloudflare, Resend, SendGrid, SMTP, or noop.
Notifications
Typed events, dispatch job, channels, dedup, preferences, and SSE pub/sub via Valkey.
flowchart LR
app[Application code] --> reg[eventRegistry]
reg --> dispatch[notification-dispatch queue]
dispatch --> ch{channels}
ch --> inApp[in-app]
ch --> email[email]
ch --> sse[SSE via Valkey pub/sub]
Notification flow: application code emits typed events through the eventRegistry; the notification-dispatch queue resolves channels; dispatched events fan out to three sinks: an in-app row in Postgres, an email template through the email provider, and a live SSE push via Valkey pub/sub.
defineNotificationEvent registers type, payload shape, and which channels apply.notification-dispatch worker resolves channels and delivers (in-app row, email template, live SSE to connected clients).Extension lives under src/lib/notifications/ (events/, dispatch/, channels/, preferences/, pubsub/, dedup.service.ts). Add an event with the scaffold script, register channels, enqueue through the dispatcher. Mail follows the same pattern via email-delivery.
Email uses the same queue-or-inline pattern: precompiled Handlebars JSON, then the configured provider (Cloudflare, Resend, SendGrid, SMTP, noop).
Audit log appends security-relevant events to a dedicated Postgres schema (audit namespace). Fire-and-forget from the API; query and retention are yours to extend.
In-process is the default. Move work to a dedicated worker or service when:
Keep job shapes and contracts stable (payload types, idempotency rules) so API producers change little. BullMQ job names, channel interfaces, and OpenAPI-facing behavior stay the same; only where the worker runs changes.