This guide is for the engineers, architects, and operators who deploy, configure, secure, and extend Novu. It documents the system architecture, the self-hosting topology, the complete environment-variable reference, security posture, operational procedures, and the internals of the monorepo deeply enough to reason about — or contribute to — the platform.
For day-to-day usage (triggering workflows, the Inbox component, the CLI, providers), see the companion Novu User Guide.
Note
This guide reflects the open-source repository
novuhq/novuat commitb6ab172. Novu is an open-core product: the core is MIT-licensed; theenterprise/tree and*/ee/*folders are under a commercial license. Where a capability is enterprise-only, this guide says so.
Table of Contents
- Executive Summary
- Architecture
- Repository Structure
- System Requirements
- Deployment (Self-Hosting)
- Configuration Reference
- Data Stores & Schema
- The Workflow Engine & Queues
- Authentication & Authorization
- Security Hardening
- Observability & Operations
- Module Reference (Internals)
- Code Patterns & Conventions
- Testing Strategy
- Extending Novu
- Potential Improvements
- Quick Reference
1. Executive Summary
Novu is a notification & agent-communication infrastructure delivered as a set of cooperating Node.js services backed by MongoDB, Redis, ClickHouse, and object storage. A client triggers a workflow via the REST API; the API enqueues work onto Redis-backed BullMQ queues; a worker service executes the workflow's steps (channel sends, digests, delays) through pluggable providers; a websocket service streams in-app notifications to the embeddable Inbox in real time.
| Aspect | Summary |
|---|---|
| Language | TypeScript (Node.js 22), strict monorepo via Nx + pnpm workspaces. |
| Runtime services | api, worker, ws, dashboard, plus optional webhook and inbound-mail. |
| Primary datastore | MongoDB 8.x (via Mongoose, in libs/dal). |
| Queue / cache | Redis (BullMQ for jobs, plus a cache service). |
| Analytics store | ClickHouse (activity/metrics). |
| Object storage | S3-compatible (attachments; LocalStack for local dev). |
| API framework | NestJS 11 (CQRS commands/use-cases pattern). |
| Frontend | React 19 + Vite + Radix UI + Tailwind (dashboard). |
| Auth | Community: Passport (JWT, GitHub OAuth). Enterprise: Clerk / better-auth. |
| Licensing | Open core — MIT core, commercial enterprise/. |
Important
All cross-channel delivery flows through the worker service, not the API. The API is a thin, authenticated front door that validates input and enqueues jobs. Scale the worker independently of the API.
2. Architecture
2.1 Runtime topology
┌─────────────────────────────────────────────┐
Browser / Inbox ◀───▶│ ws (NestJS + socket.io) :3002 │
└───────────────▲─────────────────────────────┘
│ subscriber feed (Redis pub/sub)
Your backend ──trigger──▶ ┌────────────┴───────────┐
POST /v1/events/trigger │ api (NestJS) :3000 │──┐
└────────────┬───────────┘ │ enqueue (BullMQ)
Dashboard (React/Vite) ─▶ api │ ▼
:4000 │ ┌──────────────────┐
│ │ Redis (BullMQ) │
Inbound email (SMTP) ─▶ inbound-mail ──┤ └────────▲─────────┘
Provider webhooks ─▶ webhook ────────┘ │ consume
┌───────┴──────────┐
│ worker (NestJS) │──▶ Email/SMS/Push/Chat providers
└───────┬──────────┘
┌───────────────┬───────────────┬──────────────┴───────────────┐
▼ ▼ ▼ ▼
MongoDB 8.x ClickHouse S3 (attachments) In-App (▶ ws ▶ Inbox)
(entities) (analytics)
2.2 Services
| Service | Package | Port | Responsibility |
|---|---|---|---|
| API | @novu/api-service |
3000 | REST API: auth, trigger ingestion, CRUD for workflows/subscribers/integrations/topics, billing, health. Generates OpenAPI via @nestjs/swagger. |
| Worker | @novu/worker |
3004 (health) | Consumes BullMQ queues; executes workflow steps, digests, delays; calls providers; writes execution details. |
| WS | @novu/ws |
3002 | socket.io gateway streaming in-app notifications to the Inbox; Redis adapter for horizontal scale. |
| Dashboard | @novu/dashboard |
4000 | React SPA (Vite) — the admin UI for workflows, integrations, activity, subscribers. |
| Webhook | @novu/webhook |
— | Receives inbound provider webhooks (delivery status, etc.) via Svix. |
| Inbound-mail | @novu/inbound-mail |
— | SMTP server (smtp-server) that parses inbound email into the conversation model. |
2.3 Shared libraries
| Library | Role |
|---|---|
@novu/dal |
Data Access Layer — Mongoose schemas, repositories (repository pattern), indexes. |
@novu/application-generic |
Shared backend building blocks: use-cases, services (cache, queues, storage, metrics, analytics), SSRF-safe HTTP, auth services, BullMQ wiring. |
@novu/shared |
Cross-cutting types, DTOs, constants, provider catalogs, validation utilities (data/tags filters). |
@novu/framework |
The code-first workflow engine + serve adapters (Bridge). |
@novu/providers |
All channel provider implementations behind a common interface. |
@novu/stateless |
Lightweight, stateless trigger primitives. |
@novu/maily-core / @novu/maily-render |
The no-code email editor (Tiptap-based) and its HTML renderer. |
2.4 Edge workers (enterprise / Cloudflare)
The enterprise/workers/ tree contains Cloudflare Workers (Hono/Wrangler) used in Novu's hosted product:
| Worker | Purpose |
|---|---|
scheduler |
Edge scheduling (Hono + valibot). |
socket |
Edge websocket fan-out (ws, JWT). |
step-resolver |
Executes code-defined steps at the edge in a dispatch namespace (HMAC-secured). |
thalamus-observer |
Agent observability. |
Note
The Cloudflare workers are part of the enterprise/hosted topology. A community self-host does not need them — the in-cluster
wsandworkerservices cover the same responsibilities.
3. Repository Structure
novu/
├── apps/ Runtime services (deployable)
│ ├── api/ @novu/api-service (NestJS REST API)
│ ├── worker/ @novu/worker (BullMQ consumers)
│ ├── ws/ @novu/ws (socket.io gateway)
│ ├── dashboard/ @novu/dashboard (React + Vite SPA)
│ ├── webhook/ @novu/webhook (inbound provider webhooks)
│ └── inbound-mail/ @novu/inbound-mail (SMTP server)
├── libs/ Internal backend libraries
│ ├── dal/ Mongoose schemas + repositories
│ ├── application-generic/ Shared use-cases & services
│ ├── testing/ Test harness (sessions, seed data)
│ ├── automation/ Code generators (e.g. new provider)
│ ├── notifications/ System notification templates
│ └── maily-core, maily-render, ...
├── packages/ Published npm packages (SDKs + CLI)
│ ├── novu/ the `novu` CLI
│ ├── framework/ @novu/framework (code-first workflows)
│ ├── js, react, nextjs, Inbox SDKs
│ │ react-native/
│ ├── api/ @novu/api (generated server SDK → libs/internal-sdk)
│ ├── providers/ channel providers
│ ├── shared/ shared types/DTOs/consts
│ ├── stateless/ stateless triggers
│ └── agent-toolkit/ ACI agent toolkit
├── enterprise/ Commercial-license code
│ ├── packages/ ee-ai, ee-api, ee-auth, ee-billing, ee-translation
│ └── workers/ Cloudflare edge workers
├── docker/ Self-host (community) + local dev compose
├── playground/ Example Next.js & NestJS apps
└── nx.json, pnpm-workspace.yaml, biome.json
Tip
Build orchestration is Nx (
nx run-many,nx affected). Linting/formatting is Biome (not ESLint/Prettier). The default git base branch isnext. Conventional Commits are enforced (type(scope): description).
4. System Requirements
| Component | Requirement |
|---|---|
| Node.js | >=22 <23 (engines field). |
| Package manager | pnpm ^11 (only-allow pnpm enforced via preinstall). |
| MongoDB | 8.x (self-host compose uses mongo:8.0.17). |
| Redis | Any modern Redis (compose uses redis:alpine). |
| ClickHouse | 24.x (for analytics; clickhouse/clickhouse-server:24.3-alpine in local compose). |
| Object storage | S3-compatible bucket (LocalStack for local dev). |
| Docker | Docker Engine + Compose v2 (for self-host). |
| Build tooling | openssl, curl (community installer). |
4.1 Minimum self-host footprint
The community compose runs four app containers (api, worker, ws, dashboard) plus mongodb and redis. For a small deployment, plan for ~2 vCPU / 4 GB RAM as a starting point and scale the worker first under load.
5. Deployment (Self-Hosting)
5.1 One-line community install
curl -fsSL https://raw.githubusercontent.com/novuhq/novu/next/docker/community/setup.sh | bash
The installer (docker/community/setup.sh):
- Checks for
curl,docker,openssl, and Compose v2. - Downloads
docker-compose.ymland.env.example. - Generates secrets with
openssl rand -hex:JWT_SECRET(32),STORE_ENCRYPTION_KEY(16),NOVU_SECRET_KEY(32). chmod 600 .envand runsdocker compose up -d.
Resulting endpoints:
Dashboard : http://localhost:4000
API : http://localhost:3000
WebSocket : http://localhost:3002
5.2 The compose stack
The community docker/community/docker-compose.yml defines:
| Service | Image | Notes |
|---|---|---|
redis |
redis:alpine |
Job queue + cache. Healthcheck redis-cli ping. |
mongodb |
mongo:8.0.17 |
Primary datastore, root creds from env, named volume mongodb:/data/db. |
api |
ghcr.io/novuhq/novu/api:3.17.0 |
Waits for healthy mongo+redis. Health: GET /v1/health-check. |
worker |
ghcr.io/novuhq/novu/worker:3.17.0 |
Consumes queues. Health on :3004. |
ws |
ghcr.io/novuhq/novu/ws:3.17.0 |
socket.io gateway. |
dashboard |
ghcr.io/novuhq/novu/dashboard:3.17.0 |
Served on :4000; built with VITE_API_HOSTNAME / VITE_WEBSOCKET_HOSTNAME. |
Important
The prebuilt images are published to GitHub Container Registry under
ghcr.io/novuhq/novu/<service>:<version>. Pin the version tag (e.g.3.17.0) in production rather than tracking a moving tag, and bump deliberately.
5.3 Production deployment checklist
- [ ] Generate strong, unique
JWT_SECRET,STORE_ENCRYPTION_KEY,NOVU_SECRET_KEYand store them in a secrets manager (never in the repo). - [ ] Point
MONGO_URLat a replica-set MongoDB with auth enabled. - [ ] Use a managed/secured Redis with
REDIS_PASSWORD(and TLS viaREDIS_TLSif available). - [ ] Set
FRONT_BASE_URL,API_ROOT_URL, and dashboardVITE_API_HOSTNAME/VITE_WEBSOCKET_HOSTNAMEto externally reachable HTTPS URLs. - [ ] Terminate TLS at a reverse proxy in front of
api,ws, anddashboard. - [ ] Configure object storage (
S3_BUCKET_NAME,S3_REGION, credentials) for attachments. - [ ] Set
IS_SELF_HOSTED=true. - [ ] Enable rate limiting (
IS_API_RATE_LIMITING_ENABLED=true) and idempotency (IS_API_IDEMPOTENCY_ENABLED=true) if desired. - [ ] Wire health checks (
/v1/health-check) into your orchestrator's liveness/readiness probes. - [ ] Configure Sentry/New Relic if you use them.
5.4 Upgrades & rollback
- Upgrade: bump the image tags for
api/worker/ws/dashboardtogether to the same version anddocker compose up -d. The worker runs MongoDB migrations on boot whenMONGO_AUTO_CREATE_INDEXES/migration scripts apply; review release notes for ClickHouse migrations (clickhouse:migrate:prod). - Rollback: revert the image tags to the previous version. Because services share schemas, roll all four back together; avoid running mismatched versions.
Warning
Do not run a newer
apiagainst an olderworker(or vice-versa) for long — they share the BullMQ job shape and MongoDB schema. Deploy all services as a set.
6. Configuration Reference
All services are configured by environment variables (parsed/validated with envalid/dotenv). The tables below list the variables found in the source.
6.1 Core
| Variable | Service(s) | Purpose |
|---|---|---|
NODE_ENV |
all | production / development / test. |
PORT / API_PORT / WS_PORT / WORKER_PORT |
per service | Listen ports (defaults: api 3000, ws 3002, worker 3004, dashboard 4000). |
API_ROOT_URL |
api, worker | Public base URL of the API. |
FRONT_BASE_URL |
api | Public base URL of the frontend/dashboard. |
API_CONTEXT_PATH / WS_CONTEXT_PATH |
api, ws | Optional path prefix when reverse-proxied under a subpath. |
IS_SELF_HOSTED |
api, libs | Marks a community self-hosted deployment. |
6.2 Secrets
| Variable | Purpose |
|---|---|
JWT_SECRET |
Signs dashboard/user JWTs (community auth) and WS auth. |
STORE_ENCRYPTION_KEY |
Encrypts stored provider credentials / secrets at rest. |
NOVU_SECRET_KEY |
The API secret key used for server-side triggering. |
NOVU_INTERNAL_SECRET_KEY |
Internal service-to-service secret. |
SUBSCRIBER_WIDGET_JWT_EXPIRATION_TIME |
Lifetime of subscriber (Inbox) tokens. |
Caution
STORE_ENCRYPTION_KEYencrypts integration credentials in the database. Rotating or losing it makes stored provider credentials unreadable. Back it up securely and rotate only with a documented re-encryption procedure.
6.3 Datastores
| Variable | Purpose |
|---|---|
MONGO_URL |
MongoDB connection string. |
MONGO_MIN_POOL_SIZE / MONGO_MAX_POOL_SIZE |
Connection pool bounds. |
MONGO_MAX_IDLE_TIME_IN_MS |
Idle connection timeout. |
MONGO_AUTH_MECHANISM |
Mongo auth mechanism. |
MONGO_AUTO_CREATE_INDEXES |
Whether to auto-create indexes on boot. |
REDIS_HOST / REDIS_PORT / REDIS_PASSWORD |
Redis (BullMQ) connection. |
REDIS_DB_INDEX / REDIS_PREFIX / REDIS_TLS |
Redis db index, key prefix, TLS toggle. |
REDIS_CACHE_SERVICE_HOST / REDIS_CACHE_SERVICE_PORT |
Separate cache Redis. |
S3_BUCKET_NAME / S3_REGION / S3_LOCAL_STACK |
Object storage for attachments. |
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY |
S3 credentials. |
6.4 Worker tuning
| Variable | Purpose |
|---|---|
WORKER_DEFAULT_CONCURRENCY |
Concurrent jobs per worker. |
WORKER_DEFAULT_LOCK_DURATION |
BullMQ lock duration (ms). |
ACTIVE_WORKERS |
Which worker queues this instance runs. |
BROADCAST_QUEUE_CHUNK_SIZE / MULTICAST_QUEUE_CHUNK_SIZE |
Fan-out batch sizes for topic/broadcast triggers. |
SQS_DEFAULT_CONCURRENCY / SQS_DEFAULT_VISIBILITY_TIMEOUT / SQS_DEFAULT_BATCH_SIZE / SQS_DEFAULT_WAIT_TIME_SECONDS |
SQS-mode tuning (alternative queue backend). |
IN_MEMORY_CLUSTER_MODE_ENABLED |
In-memory cluster mode flag. |
6.5 Feature flags & behavior
| Variable | Purpose |
|---|---|
IS_API_RATE_LIMITING_ENABLED |
Enable API rate limiting (throttler). |
IS_API_IDEMPOTENCY_ENABLED |
Enable idempotent trigger handling. |
IS_V2_ENABLED |
Enable v2 API surface. |
IS_NEW_MESSAGES_API_RESPONSE_ENABLED |
New messages API response shape. |
IS_EMAIL_INLINE_CSS_DISABLED |
Disable email inline-CSS step. |
IS_USE_MERGED_DIGEST_ID_ENABLED |
Merged digest ID behavior. |
NOVU_STRICT_AUTHENTICATION_ENABLED |
Strict auth mode. |
NOVU_ENTERPRISE / CI_EE_TEST |
Enable enterprise code paths. |
6.6 Auth & integrations
| Variable | Purpose |
|---|---|
GITHUB_OAUTH_CLIENT_ID / GITHUB_OAUTH_CLIENT_SECRET |
GitHub OAuth (community login). |
CLERK_SECRET_KEY |
Clerk auth (enterprise). |
EE_AUTH_PROVIDER |
Selects the enterprise auth provider. |
SVIX_API_KEY |
Svix (webhook delivery infra). |
MAIL_SERVER_DOMAIN |
Inbound mail / domain features. |
NOVU_MANAGED_CLAUDE_API_KEY |
Managed agent (Claude) key. |
STEP_RESOLVER_CF_ACCOUNT_ID / STEP_RESOLVER_CF_API_TOKEN / STEP_RESOLVER_CF_DISPATCH_NAMESPACE |
Cloudflare step-resolver deploy (enterprise). |
SOCKET_IO_ADMIN_USERNAME / SOCKET_IO_ADMIN_PASSWORD_HASH |
socket.io admin UI auth. |
6.7 Observability
| Variable | Purpose |
|---|---|
SENTRY_DSN |
Sentry error reporting. |
NEW_RELIC_ENABLED / NEW_RELIC_APP_NAME / NEW_RELIC_LICENSE_KEY |
New Relic APM. |
ENABLE_OTEL |
OpenTelemetry export. |
MIXPANEL_TOKEN / SEGMENT_TOKEN |
Product analytics. |
IS_ANALYTICS_LOGS_ENABLED |
Analytics logging toggle. |
Note
The dashboard is a static SPA built with Vite; its only runtime env vars are
VITE_API_HOSTNAMEandVITE_WEBSOCKET_HOSTNAME, which are baked at build time and must point to browser-reachable URLs.
7. Data Stores & Schema
7.1 MongoDB (primary)
All durable domain state lives in MongoDB, accessed through libs/dal using Mongoose with the repository pattern. Core entities (see packages/shared/src/entities):
| Entity | Holds |
|---|---|
Organization, Environment, User |
Tenant, environment, and user accounts. |
Subscriber, SubscriberPreference |
Recipients and their channel preferences. |
NotificationTemplate (workflow) |
Workflow definitions and steps. |
Notification, Job, Message |
A triggered notification, its per-step jobs, and rendered messages. |
MessageTemplate |
Per-step channel content templates. |
Integration |
Provider credentials/config per channel & environment. |
Topic |
Subscriber groups for broadcast. |
Layout |
Reusable email wrappers. |
ExecutionDetails |
Per-step execution audit trail (shown in Activity Feed). |
Change |
Versioned changes for promotion between environments. |
Tenant, WorkflowOverride |
Multi-tenant per-tenant overrides. |
Tip
When adding queries, add the matching MongoDB index in the repository. CodeRabbit's repo rules flag schema changes that need migrations and queries missing indexes — follow the repository pattern and keep reads indexed.
Migrations live in apps/api/migrations/ and run via the migration script.
7.2 Redis
Two roles: BullMQ job queues (the heart of async processing) and a cache service (often a separate Redis instance via REDIS_CACHE_SERVICE_*). The ws service also uses a Redis adapter for socket.io fan-out across instances.
7.3 ClickHouse
Analytics and high-volume activity/metrics data (charts, interaction trends, workflow-run counts) are stored in ClickHouse. Migrations are managed with clickhouse-migrations (clickhouse:migrate:local / :prod).
7.4 Object storage (S3)
Attachments and large assets go to an S3-compatible bucket. LocalStack provides S3 locally (docker/local/docker-compose.agent.yml).
8. The Workflow Engine & Queues
8.1 Lifecycle of a trigger
POST /v1/events/trigger
└─ api: authenticate (ApiKey) → validate → upsert subscriber(s)
└─ enqueue "trigger" job (BullMQ)
└─ worker: parse event → fan out per subscriber/topic
└─ build notification jobs (one per step)
└─ execute steps in order:
├─ channel step → resolve provider → send → record ExecutionDetails
├─ digest step → hold & aggregate over window
├─ delay step → schedule resume
└─ inApp step → persist Message → ws → <Inbox/>
8.2 BullMQ queues
The worker runs multiple queues (e.g. standard, workflow, subscriber-process, digest/unsnooze). Concurrency and lock duration are tuned with WORKER_DEFAULT_CONCURRENCY and WORKER_DEFAULT_LOCK_DURATION. Topic/broadcast fan-out is chunked (*_QUEUE_CHUNK_SIZE) to avoid flooding.
Note
Novu also supports an SQS queue backend (the
SQS_DEFAULT_*vars) as an alternative to Redis/BullMQ for managed deployments. Community self-hosts use BullMQ on Redis.
8.3 Digest & delay durability
Digest and delay steps are durable: the worker persists the job state and resumes when the window/timer elapses. This survives restarts and does not hold open connections. Jobs missing required fields fail fast with Job data is missing required fields: ….
8.4 Idempotency & retries
With IS_API_IDEMPOTENCY_ENABLED, duplicate triggers carrying the same idempotency key are de-duplicated. Failed channel sends are retried per the channel's policy; exhausted retries are recorded in ExecutionDetails and surfaced in the Activity Feed.
9. Authentication & Authorization
9.1 Two auth modes
| Mode | Mechanism |
|---|---|
| Community | Passport strategies: JWT (passport-jwt), API key (passport-headerapikey), GitHub OAuth (passport-github2, passport-oauth2). JWT_SECRET signs tokens. |
| Enterprise | Clerk and/or better-auth (@novu/ee-auth), selected via EE_AUTH_PROVIDER / CLERK_*. Supports SSO. |
9.2 API authentication
Server-to-server requests authenticate with Authorization: ApiKey <secret-key> (environment-scoped). The Bridge sync uses the same scheme plus an HMAC signature (t=<timestamp>,v1=<sha256-hex>) for request integrity.
9.3 Subscriber (Inbox) auth
The Inbox authenticates a subscriber with the public Application Identifier plus, in production, an HMAC subscriberHash computed server-side from the Secret Key. Subscriber tokens expire per SUBSCRIBER_WIDGET_JWT_EXPIRATION_TIME.
Warning
Enable HMAC subscriber verification for the In-App integration in production. Without it, a
subscriberIdalone can read another user's feed.
10. Security Hardening
Novu has several first-class security mechanisms visible in the source — treat these as the baseline posture.
10.1 SSRF protection (outbound requests)
Because Novu makes outbound HTTP to user-supplied URLs (Bridge endpoints, webhook providers, provider APIs), it has a hardened outbound HTTP layer. The codebase forbids the naive one-shot validateUrlSsrf check and mandates safeOutboundRequest / safeOutboundJsonRequest (or HttpClientService with enforceSsrfProtection: true), which pins the connection to a validated IP and re-validates every redirect (defeating DNS-rebinding and redirect-based SSRF). Blocked requests raise SsrfBlockedError with reasons INVALID_URL, CREDENTIALS_IN_URL, BLOCKED_HOSTNAME, or DNS_LOOKUP_FAILED.
Caution
This rule is enforced at lint time via Biome
noRestrictedImports. When writing code that makes outbound calls, never importvalidateUrlSsrf— use the safe-outbound helpers so SSRF protection cannot be bypassed.
10.2 Secrets at rest
Provider credentials are encrypted with STORE_ENCRYPTION_KEY before being stored in MongoDB. Keep this key in a secrets manager and back it up.
10.3 HTTP hardening
The API uses helmet for security headers, compression, CORS configured per WIDGET_BASE_URL/DOCS_BASE_URL, and rate limiting via @nestjs/throttler (and @upstash/ratelimit) when IS_API_RATE_LIMITING_ENABLED=true. Input is validated with class-validator + ajv on every DTO.
10.4 Input & filter validation
User-supplied workflow conditions are validated with strict bounds — DataFilterValidationError and TagsFilterValidationError enforce maximum group/value counts, scalar-only values, and mutually-exclusive AND/OR. This prevents pathological or abusive filter payloads.
10.5 Path traversal & file safety
The add-inbox CLI and other file-touching code reject paths outside the working directory (Access denied: Path outside working directory).
10.6 Dependency hygiene
pnpm-workspace.yaml carries an extensive overrides map pinning transitive dependencies above known-vulnerable versions, and minimumReleaseAge (4320 minutes ≈ 3 days) delays adopting brand-new releases to dodge supply-chain attacks. Renovate keeps deps current.
Important
The known issue GH #10124 (ws crash on connection reading
subscriber.contextKeys.length) is a reliability/security-adjacent bug fixed in newer versions — keep thewsimage and client SDKs current.
11. Observability & Operations
11.1 Health checks
Every service exposes GET /v1/health-check (used by the compose healthchecks and suitable for k8s liveness/readiness). Built with @nestjs/terminus and @godaddy/terminus.
curl -fsS http://localhost:3000/v1/health-check
{ "status": "ok", "info": { "mongo": { "status": "up" }, "redis": { "status": "up" } } }
11.2 Logging
Structured logging uses Pino (PinoLogger from @novu/application-generic is mandated over NestJS's default Logger — enforced by Biome). Container logs are JSON with rotation (max-size: 50m, max-file: 5).
11.3 Metrics & tracing
- Sentry (
SENTRY_DSN) for error tracking across all services. - New Relic (
NEW_RELIC_*) for APM. - OpenTelemetry (
ENABLE_OTEL) for traces/metrics export. - Worker emits active-jobs metrics (
active-jobs-metric.service.ts).
11.4 Routine operations
| Task | How |
|---|---|
| View logs | docker compose logs -f <service> |
| Restart a service | docker compose restart worker |
| Scale workers | Run multiple worker containers; tune WORKER_DEFAULT_CONCURRENCY and ACTIVE_WORKERS. |
| Run a DB migration | pnpm --filter @novu/api-service migration <script> |
| Migrate ClickHouse | pnpm --filter @novu/api-service clickhouse:migrate:prod |
| Admin: remove a user/org | admin:remove-user-account / admin:remove-organization scripts (api). |
11.5 Scaling guidance
- The worker is the throughput bottleneck for delivery — scale it horizontally first; BullMQ distributes jobs across instances safely (atomic locks).
- The ws service scales horizontally with the Redis socket.io adapter.
- The api is stateless behind a load balancer.
- Size MongoDB and Redis for peak trigger volume; digests/delays add long-lived job state to Redis.
12. Module Reference (Internals)
The repository contains 212 high-priority module groups. The highest-signal areas for an operator/contributor:
| Area | Path | What's there |
|---|---|---|
| API app | apps/api/src/app/* |
Feature modules: events (trigger), subscribers, workflows-v1/-v2, integrations, topics-v1/-v2, inbox, auth, billing, bridge, environments-v2, agents, domains, rate-limiting. |
| Trigger ingestion | apps/api/src/app/events |
Parse + enqueue trigger events. |
| Worker pipeline | apps/worker/src/app/workflow |
send-message (per channel), digest, delay, add-job, queue-next-job, throttle, inbound-email-parse. |
| Shared use-cases | libs/application-generic/src/usecases |
trigger-event, compile-email-template, get-preferences, process-tenant, message-template CRUD, preview. |
| Services | libs/application-generic/src/services |
cache, BullMQ, storage, metrics, analytics, auth, SQS, readiness. |
| Providers | packages/providers/src/lib/{email,sms,push,chat} |
One folder per provider behind a common base.provider interface. |
| Provider factories | libs/application-generic/src/factories/{mail,sms,push,chat}/handlers |
Map an Integration to a concrete provider instance. |
| Framework | packages/framework/src |
Code-first workflow runtime, step types, serve adapters, validators. |
| DAL | libs/dal/src/repositories |
One repository per entity. |
| Inbox SDK | packages/js/src, packages/react/src |
Client, websocket, UI components. |
| Email editor | libs/maily-core, libs/maily-render |
Tiptap editor + HTML renderer. |
12.1 API resource families (REST)
The API is large and versioned (v1 and v2 paths). Because routes are NestJS decorators (not all captured by static extraction), the authoritative endpoint reference is the generated OpenAPI spec — the API generates it via @nestjs/swagger (exportOpenAPIJSON.ts / generate:swagger) and serves Swagger UI. Major resource families:
/v1/events/trigger— trigger workflows (single, bulk, topic, broadcast);DELETEto cancel by transaction id./v1/subscribers— subscriber CRUD, credentials, preferences./v1/workflows(v1) and v2 equivalents — workflow management./v1/integrations— provider configuration./v1/topics— topic management and subscriptions./v1/inbox— Inbox notifications, counts, preferences./v1/bridge/sync— register code-defined workflows from a Bridge URL./v1/agents/*— ACI agent management & bridge URLs./v1/health-check— health.
Tip
To get the exact, version-matched endpoint list for your deployment, fetch the OpenAPI document from your running API (the dashboard's API reference and
docs.novu.co/api-referenceare generated from it).
13. Code Patterns & Conventions
- CQRS / use-case pattern. API controllers stay thin; business logic lives in commands (
*.command.ts) and use-cases (*.usecase.ts). A Biome plugin enforces command-session exclusion; DTOs have dedicated property-optionality plugins. - Repository pattern for all MongoDB access (
libs/dal). - Provider interface. Every provider implements the standard channel interface (
base.provider.ts); a generator (libs/automation,pnpm generate:provider) scaffolds new ones. - Biome, not ESLint/Prettier. Formatting (2-space, single quotes, semicolons, 120 col) and linting are unified under Biome 2.x with project-specific
noRestrictedImportsrules (PinoLogger over Logger; safe-outbound overvalidateUrlSsrf; root-package imports only). - Nx workspace.
nx run-many/nx affected; build caching via Nx Cloud; independent per-project versioning with Conventional Commits. - Strict import boundaries. Packages must be imported from their root entry (e.g.
@novu/api, not@novu/api/src). - Generators.
hygentemplates (_templates/) scaffold new modules/use-cases (pnpm g:module,pnpm g:usecase).
14. Testing Strategy
| Layer | Tooling | Where |
|---|---|---|
| Unit (backend) | Mocha + Chai + Sinon | *.spec.ts per app/lib |
| E2E (API) | Mocha + Supertest | apps/api/e2e, *.e2e.ts (sharded for v2) |
| Unit (packages) | Jest / Vitest | packages/* (CLI uses Vitest) |
| E2E (dashboard) | Playwright | apps/dashboard/tests |
| Provider tests | per-provider | packages/providers/** (test:providers) |
| Test harness | @novu/testing |
sessions, seed data, queue helpers |
Run, e.g.:
pnpm --filter @novu/api-service test # API unit
pnpm test:providers # all providers
pnpm --filter @novu/dashboard test:e2e # dashboard Playwright
Note
A
stop-onlylint guard prevents committing.onlyin test files. E2E API runs requireNODE_OPTIONS=--no-experimental-strip-types(Node 22 quirk) and spin up MongoDB/Redis (LocalStack for S3).
15. Extending Novu
15.1 Add a new provider
cd libs/automation && pnpm generate:provider(orpnpm generate:providerfrom root) — scaffolds the provider folder.- Implement the standard channel interface in
packages/providers/src/lib/<channel>/<name>. - Add credentials metadata so the dashboard can render the integration form.
- Add a handler in
libs/application-generic/src/factories/<channel>/handlers. - Add provider tests; run
pnpm test:providers.
Tip
The community frequently requests new providers (open issues include WeChat/Weixin #4493, LINE #11048, Google Chat #1478, ValueFirst SMS #10919). A new provider is a well-scoped first contribution.
15.2 Add an API feature
Use the generators (pnpm g:module, pnpm g:usecase) to scaffold a NestJS module + use-case + command following the CQRS pattern, add a repository in libs/dal if it needs persistence (with indexes + migration), and add e2e coverage under apps/api/e2e.
15.3 Code-defined steps (step publish)
novu step publish bundles step handlers and deploys them; the enterprise step-resolver Cloudflare worker can execute them at the edge in a dispatch namespace (HMAC-secured via STEP_RESOLVER_HMAC_SECRET).
16. Potential Improvements
Observations from the source, useful for operators and contributors:
- API endpoint discoverability. The REST surface is large and versioned (v1/v2 coexist, e.g.
topics-v1/topics-v2,workflows-v1/workflows-v2,layouts-v1/layouts-v2). Static analysis captures only a fraction of routes; rely on the generated OpenAPI. Consolidating v1→v2 over time would reduce surface area. - Open issues to track for self-hosters: community org-switching in self-hosted mode (#11356), FCM rich push (#9489), preferences-by-context (#11114), and
NovuModule.registerAsyncsilently ignoringcontrollerDecoratorsin@novu/framework(#11009). Watch these if you depend on those features. - Config sprawl. ~100+ environment variables span services; a single documented
.env.exampleper service (the community installer fetches one at runtime) plus a validated schema dump would aid operators.envalidalready validates — surfacing the schema would help. - Mismatched-version risk. Because
api/worker/wsshare schema and job shapes, version drift is a real operational hazard; pin and deploy as a set. - ClickHouse is an extra moving part. Analytics depends on ClickHouse; small self-hosts that don't need charts could benefit from a documented "no-analytics" mode.
17. Quick Reference
17.1 Services & ports
| Service | Image | Port | Health |
|---|---|---|---|
| api | ghcr.io/novuhq/novu/api |
3000 | /v1/health-check |
| worker | ghcr.io/novuhq/novu/worker |
3004 | /v1/health-check |
| ws | ghcr.io/novuhq/novu/ws |
3002 | /v1/health-check |
| dashboard | ghcr.io/novuhq/novu/dashboard |
4000 | GET / |
| mongodb | mongo:8.0.17 |
27017 | mongosh ping |
| redis | redis:alpine |
6379 | redis-cli ping |
17.2 Critical env vars
| Variable | Must set in production |
|---|---|
JWT_SECRET, STORE_ENCRYPTION_KEY, NOVU_SECRET_KEY |
Yes — strong, unique, backed up. |
MONGO_URL, REDIS_HOST/REDIS_PASSWORD |
Yes. |
API_ROOT_URL, FRONT_BASE_URL |
Yes — public HTTPS. |
VITE_API_HOSTNAME, VITE_WEBSOCKET_HOSTNAME |
Yes (dashboard build). |
IS_SELF_HOSTED=true |
Yes (self-host). |
S3_BUCKET_NAME/S3_REGION/AWS creds |
If using attachments. |
17.3 Common commands
# Self-host
curl -fsSL https://raw.githubusercontent.com/novuhq/novu/next/docker/community/setup.sh | bash
docker compose up -d
docker compose logs -f worker
# Local development (from source)
pnpm install
pnpm start:api ; pnpm start:worker ; pnpm start:ws ; pnpm start:dashboard
# or all at once:
pnpm dev:portless # mprocs-managed multi-process dev
# Build
pnpm build # everything (Nx)
pnpm build:api # one service
nx affected -t build # only changed projects
17.4 Key technologies
| Layer | Tech |
|---|---|
| Monorepo | Nx + pnpm workspaces |
| API | NestJS 11, class-validator, ajv |
| Frontend | React 19, Vite, Radix UI, Tailwind, TanStack Query |
| Queue | BullMQ on Redis (SQS alternative) |
| DB | MongoDB 8 (Mongoose), ClickHouse 24 (analytics) |
| Auth | Passport (community), Clerk/better-auth (enterprise) |
| Webhooks | Svix |
| Lint/format | Biome 2 |
| Email editor | Tiptap (Maily) |
| Edge | Cloudflare Workers (Hono) — enterprise |