Every app eventually grows a notification hairball: transactional email here, an SMS provider there, an in-app bell icon nobody's preference settings actually respect, and a sendEmail() function copy-pasted into fourteen files. Novu (https://novu.co) is open-source notification infrastructure that untangles it: one API and one workflow editor that fan out to email, SMS, push, and in-app channels, with built-in user preferences and a notification center component. This guide self-hosts Novu on PandaStack.
What Novu needs
Novu is a multi-service system. The core pieces:
- API — the main backend you send notification triggers to.
- Worker — processes notification jobs asynchronously.
- Web — the admin dashboard (workflow editor, subscriber management).
- MongoDB — primary data store (Novu uses MongoDB, not SQL).
- Redis — queues and caching.
PandaStack has managed MongoDB and Redis, which handles the two stateful services cleanly. The API, worker, and web run as container apps.
Step 1: Provision MongoDB and Redis
Dashboard (https://dashboard.pandastack.io):
- 1Databases → New Database → MongoDB — Novu's primary store.
- 2Databases → New Database → Redis — job queues.
Copy both connection strings. Managed instances mean you get backups and monitoring instead of running database containers yourself.
Step 2: Deploy the API
Novu publishes official images. Deploy the API image:
- 1New App → Container App, Novu API image, container port 3000.
- 2Core environment variables:
| Variable | Value |
|---|---|
MONGO_URL | your managed MongoDB connection string |
REDIS_HOST / REDIS_PORT / REDIS_PASSWORD | from managed Redis |
JWT_SECRET | openssl rand -hex 32 |
STORE_ENCRYPTION_KEY | a 32-char key — encrypts provider credentials at rest |
API_ROOT_URL | https://your-novu-api.pandastack.io |
- 1Deploy.
STORE_ENCRYPTION_KEY is important: it encrypts the third-party provider credentials (your SendGrid key, Twilio token) that Novu stores. Treat it like a root secret.
Step 3: Deploy the worker
Create a second container app from the Novu worker image, with the same MONGO_URL, Redis, and secret env vars. No public port — it drains the notification queue.
As with every worker-backed system in this series: skip it and your notifications enqueue but never send. If triggers return 200 but nothing arrives, the worker is your first suspect.
Step 4: Deploy the web dashboard
- 1New App → Container App, Novu web image, container port 4200.
- 2Set the API URL env vars so the dashboard talks to your deployed API (
https://your-novu-api.pandastack.io). - 3Deploy. Admin UI at
https://your-novu.pandastack.io.
Step 5: Configure a channel provider
In the Novu dashboard, go to Integrations and connect a provider — e.g. an email provider via SMTP or API key, or an SMS provider. Novu stores these credentials encrypted (that's what STORE_ENCRYPTION_KEY protects). You can connect multiple providers per channel and Novu handles failover.
Step 6: Build a notification workflow
Workflows are where Novu earns its keep. In the editor:
- 1Create a workflow, e.g.
order-shipped. - 2Add steps across channels: an in-app notification, then an email, then (optionally) an SMS after a delay if unread.
- 3Use variables (
{{orderNumber}}) that you'll pass at trigger time. - 4Subscriber preferences are respected automatically — a user who opted out of SMS won't get the SMS step.
Step 7: Trigger notifications from your app
From your backend, one API call fans out across every channel in the workflow:
import { Novu } from '@novu/node'
const novu = new Novu(process.env.NOVU_API_KEY, {
backendUrl: 'https://your-novu-api.pandastack.io',
})
await novu.trigger('order-shipped', {
to: { subscriberId: 'user-123', email: 'a@b.com', phone: '+15551234567' },
payload: { orderNumber: 'A-4417' },
})Your application code no longer knows or cares which email/SMS provider is behind it. Swap providers in the dashboard without touching code.
Step 8: Add the in-app notification center
Novu ships a prebuilt notification-bell component for React (and other frameworks) that connects to your self-hosted API:
import { NovuProvider, PopoverNotificationCenter, NotificationBell } from '@novu/notification-center'
<NovuProvider
subscriberId={currentUser.id}
applicationIdentifier={process.env.NEXT_PUBLIC_NOVU_APP_ID}
backendUrl="https://your-novu-api.pandastack.io"
socketUrl="wss://your-novu-api.pandastack.io"
>
<PopoverNotificationCenter>
{({ unseenCount }) => <NotificationBell unseenCount={unseenCount} />}
</PopoverNotificationCenter>
</NovuProvider>Because PandaStack container apps support WebSocket connections, the real-time bell updates work in production without extra plumbing.
Step 9: Operational notes
- Backups — subscribers, workflows, and preferences live in MongoDB. Confirm PandaStack's managed backups are on.
- Secrets —
JWT_SECRETandSTORE_ENCRYPTION_KEYare critical; losingSTORE_ENCRYPTION_KEYmeans re-entering every provider credential. - Scale the worker independently during high-volume sends (a big marketing blast) without touching the API.
- Is it worth it? For a single transactional email, calling your email provider directly is less to run. Self-host Novu when you have multiple channels, want user preferences respected centrally, and are tired of provider logic scattered through your codebase.
Wrap-up
Novu turns scattered notification code into one API and one workflow editor across email, SMS, push, and in-app. On PandaStack it's API + worker + web container apps backed by managed MongoDB and Redis — and, as always, don't forget the worker. Docs: https://docs.pandastack.io. Start free at https://dashboard.pandastack.io.