file: ./content/docs/community/add-a-new-provider.mdx # Add a New Provider Steps to add a new provider to Novu Interested in expanding Novu's capabilities? By contributing to our growing ecosystem, you can enhance Novu's reach and impact. ## How to add a new provider? Novu currently supports five channels `in_app`, `push`, `email`, `chat` and `sms`. For `in_app` we support only our own provider, so new providers cannot be added to this channel. For the other four channels, we support the integration of external providers. This guide will help in adding new providers for any of these 4 channels. In this guide, we are adding a new provider for the email channel, but all of the mentioned steps are similar for other channels as well. ## Description Providers allow us to handle message delivery over multiple channels. We have multiple providers for each channel (SMS, Email, Push and Chat). To get started with adding a new provider let's look at setting up our repository. ## Requirements * Node.js version v20.8.1 * MongoDB * Redis * **(Optional)** pnpm - Needed if you want to install new packages * **(Optional)** localstack (required only in S3 related modules) Need help installing the requirements? [Read more here](/run-in-local-machine) We have used pnpm package manager in this guide. You can use npm as well. ## Initialization Fork the novu repository and clone it in your local machine. ```shell git clone https://github.com/<'YOUR_GITHUB_USER_NAME'>/novu.git ``` To set up the repository, run the initial setup command: ```shell pnpm run setup:project ``` ## Generate provider After the project is initialized, a new provider can be generated using the below command. ```shell pnpm run generate:provider ``` Use the above command at the root of the project. Choose the provider type. ```shell ? What type ❯ EMAIL SMS PUSH CHAT ``` Use `up` and `down` arrow to switch `channel` type and press `enter` to select. For this example, we will be selecting `EMAIL` as our provider type. The name for our provider will be `example-provider`. ``` ? Write the provider name`kebab-cased` (e.g. proton-mail, outlook365, yahoo-mail): example-provider ``` Make sure your selected name is not conflicting with our existing provider's name. Boilerplate files for this new `example-provider` is generated in your local machine project. > In above example, we have given our provider name as example-provider for simplicity. If provider you want to add have name as twilio, don't use twilio-provider as name, instead use twilio only. If one provider supports multiple channels like infobip supports both sms and email channels, use infobip-email or infobip-sms to differentiate these providers. Once our `example-provider` is generated we will need to begin working from `packages/providers/src/lib/email/example-provider` directory. Make sure to write the test for this new provider. ```typescript import { ChannelTypeEnum, ISendMessageSuccessResponse, IEmailOptions, IEmailProvider, } from '@novu/stateless'; export class ExampleProviderEmailProvider implements IEmailProvider { id = 'example-provider'; channelType = ChannelTypeEnum.EMAIL as ChannelTypeEnum.EMAIL; constructor( private config: { apiKey: string; } ) {} async sendMessage(options: IEmailOptions): Promise { return { id: 'id_returned_by_provider', date: 'current_time', }; } } ``` ### Template test case for `example-provider`. ```ts packages/providers/src/lib/example-provider/example-provider.provider.sepc.ts import { ExampleProviderEmailProvider } from './example-provider.provider'; test('should trigger example-provider library correctly', async () => {}); ``` ## Add provider logos In order to present this new provider in the `integration store` we need logo in light mode. Add light color svg logo in `apps/dashboard/public/images/providers/light/square` directory. Use the provider name as the file name. The sample name for our above-added provider is `example-provider.svg`. ## Add config items to the list In order to build the UI integration store, we need to provide it list of all provider integrations. This part is made up of two parts: * Create credentials config * Add ProviderId Enum * Add provider config to framework * Add provider configuration to the providers list ### 1. Create credentials config Every provider requires some credentials to create an instance. Novu will add these credentials fields in the integration store provider's form so that users can use their credentials to connect to their preferred provider to use for that channel notification. For example, in the above added `example-provider`, we have only one credential `ApiKey`. We will need to add a config object for `example-provider` with all existing provider's configs like below. ```ts title="packages/shared/src/consts/providers/credentials/provider-credentials.ts" export const exampleProviderConfig: IConfigCredentials[] = [ { key: CredentialsKeyEnum.ApiKey, displayName: 'API Key', description: 'This is API key for example provider', type: 'text', required: true, }, ...mailConfigBase, ]; ``` 1. Here the `key` is of type `CredentialsKeyEnum`. > If a new key is added, add this key at these 3 places:- > > * In `CredentialsKeyEnum` at file `packages/shared/src/types/providers.ts` > * In `ICredentials` at file `packages/shared/src/entities/integration/credential.interface.ts` > * In `CredentialsDto` at file `apps/api/src/app/integrations/dtos/credentials.dto.ts` > * In `credentials` field of `integrationSchema` at file `libs/dal/src/repositories/integration/integration.schema.ts` 2. `displayName` is a human-friendly easy to understand name which will be shown in the provider integration form for this credential. 3. `description` is a field that can be used to share more information about the credential. 4. `type` here means text field type. this can be a string for text, text for text-area, or a switch for the toggle. 5. `required` is of boolean type. 6. `mailConfigBase` is an object having default credentials required by any `email` provider. Make sure not to add duplicate providers which are already there in `mailConfigBase`. In the case of another channel provider, we will use that channel config base in place of `mailConfigBase`. > A credential can be made secret by adding in `./secure-credentials.ts` file. ### 2. Add ProviderId Enum Add this new provider id in the respective channel provider id enum in file `packages/shared/src/types/providers.ts`. As our `example-provider` is of email type, add this in `EmailProviderIdEnum` with all existing providers like below ```ts title="packages/shared/src/types/providers.ts" export enum EmailProviderIdEnum { ExampleProvider = 'example-provider', } ``` ### 3. Add provider config to framework * Add provider in `EmailProviderIdEnum` at file `packages/framework/src/shared.ts` ```ts title="packages/framework/src/shared.ts" export enum EmailProviderIdEnum { ExampleProvider = 'example-provider', } ``` * Add provider schema at `packages/framework/src/schemas/providers/email/index.ts` ```ts title="packages/framework/src/schemas/providers/email/index.ts" export const emailProviderSchema = { 'example-provider': genericProviderSchemas, } ``` ### 4. Add provider to providers list Now we need to add the provider data to the list located at `packages/shared/src/consts/providers/channels/email.ts`. Note that the `id` is the provider's name, `displayName` is the provider's name in Pascal's case, `credentials` are the ones we created in the previous step, `logoFileName` should be as it was on the adding logo step (with the format type included). ```ts title="packages/shared/src/consts/providers/channels/email.ts" import { exampleProviderConfig } from '../credentials'; export const emailProviders: IProvider[] = [ { id: 'example-provider', displayName: 'Example Provider', channel: ChannelTypeEnum.EMAIL, credentials: exampleProviderConfig, // Use valid documentation link docReference: 'https://docs.example-provider.com/', logoFileName: { light: 'example-provider.svg', dark: 'example-provider.svg' }, }, ]; ``` ## Add provider handler in the API ### 1. Create a provider handler In order to map internally the different providers' credentials, we need to add a provider handler at the respective channel handlers located. For Email, it can be found at `libs/application-generic/src/factories/mail/handlers`. Other channel handlers can also be found here. Create a new file `example-provider.handler.ts` here with the following code ```tsx libs/application-generic/src/factories/mail/handlers/example-provider.handler.ts import { ChannelTypeEnum, EmailProviderIdEnum } from '@novu/shared'; import { ExampleProviderEmailProvider } from '@novu/providers'; import { BaseHandler } from './base.handler'; export class ExampleProviderHandler extends BaseHandler { constructor() { super(EmailProviderIdEnum.ExampleProvider, ChannelTypeEnum.EMAIL); } buildProvider(credentials, from: string) { const config: { apiKey: string } = { apiKey: credentials.apiKey }; this.provider = new ExampleProviderEmailProvider(config); } } ``` Add this line given below to export this handler ```tsx libs/application-generic/src/factories/mail/handlers/index.ts export * from './example-provider.handler'; ``` ### 2. Add handler to factory The last step is to initialize the handler in the factory located in `libs/application-generic/src/factories/mail/mail.factory.ts` ```tsx libs/application-generic/src/factories/mail/mail.factory.ts import { ExampleProviderHandler } from './handlers'; export class MailFactory { handlers: IMailHandler[] = [new ExampleProviderHandler()]; } ``` ### Final Steps Now, build the project again using this command ```shell pnpm run setup:project ``` Run novu in your local machine. Read [here](/community/run-in-local-machine) to learn on how to run novu on a local machine and test this new provider. Run the below command in the root of the project to run the providers test ```shell pnpm run test:providers ``` If everything is working fine without any error, commit your local branch changes, push this branch and create a new pull request to our main repo. Hurray 🎉! You have successfully added a new provider in Novu! In this guide, we have used only one credential `apiKey` for our `example-provider`. This is for reference purposes only. A provider can have more than one credential as per its `SDK` requirements. At each step, you will need to add all credentials carefully. Check providers references below for more information. ### Reference for Adding New Providers * [SendGrid Email Provider](https://github.com/novuhq/novu/blob/next/packages/providers/src/lib/email/sendgrid/sendgrid.provider.ts) * [Twilio SMS Provider](https://github.com/novuhq/novu/blob/next/packages/providers/src/lib/sms/twilio/twilio.provider.ts) * [FCM Push Provider](https://github.com/novuhq/novu/blob/next/packages/providers/src/lib/push/fcm/fcm.provider.ts) * [Slack Chat Provider](https://github.com/novuhq/novu/blob/next/packages/providers/src/lib/chat/slack/slack.provider.ts) file: ./content/docs/community/changelog.mdx # Changelog See the most recent changes and learn about how to shape Novu's future import { Card, Cards } from 'fumadocs-ui/components/card'; import { MapIcon } from 'lucide-react'; } href="https://go.novu.co/changelog?utm_source=docs"> Learn about what's changed, new features, bug fixes, and see Novu's history across versions. Using the changelog, you can: * Keep an eye on the latest updates, * Learn more about the newly added features and improvements, and * Stay informed about bug fixes and enhancement ## Getting involved: Community is at the heart of everything we do at Novu. To get more involved with the community, you can: * **Fork and Contribute** to our open issues as well as suggest new ideas at our [github repository](https://github.com/novuhq/novu) * **Join our community** to ask questions, engage with other users and share ideas. Here's the [joining link.](https://discord.gg/novu?ref=docs-join-our-community) * **Participate in our office hours** to learn more and connect with our core team. Join us [here.](https://www.youtube.com/@novuhq/streams) Remember, we're not building just another product, but a community of passionate developers who shape its evolution. Our changelog isn't just a list of updates but a reflection of our journey together. Your voice matters, and your ideas and feedback are what fuel our progress and shape our future. So, join us in this adventure, and – Let's build something amazing, one feature at a time! file: ./content/docs/community/code-of-conduct.mdx # Code of Conduct The set of rules and guidelines that govern interaction among community members As a community-driven company, Novu is committed to creating an inclusive and welcoming environment for all members, regardless of factors such as age, body size, disability, ethnicity, gender identity, experience level, education, socio-economic status, nationality, personal appearance, race, religion, or sexual orientation. However, diverse communities may face challenges, such as potential misunderstandings and miscommunications. To ensure respectful interactions, free from behaviour that may create an unsafe environment, we have established this Code of Conduct. These guidelines do not cover every possible scenario comprehensively but serve as a guiding light towards courteous interactions among community members, aligning with the overarching principle of avoiding unprofessional behaviour. This Code of Conduct applies to all events and participants, aiming to maintain a welcoming and healthy environment for our community. Traits of a Novu community member include: ### Being considerate and using appropriate channels Contributions of every kind have far-ranging consequences. Just as your work depends on the work of others, decisions you make surrounding your contributions to the Novu community will affect your fellow community members. Use appropriate channels for what you're about to say and refrain from tagging a role that sends out a lot of pings. You are strongly encouraged to take those consequences into account while making decisions. ### Adhering to these standards It's crucial to keep in mind that our community members are from all kinds of backgrounds, so the members are expected to: * Demonstrate empathy and kindness toward other people * Be respectful of differing opinions, viewpoints, and experiences * Give and accept constructive feedback gracefully * Accept responsibility and apologise to those affected by mistakes, and learn from such experiences * Focus on what is best not just for us as individuals, but for the overall community! ### Patience Our community thrives on the generosity of volunteered time. Questions, contributions, and support requests may embark on a time-travelling journey before finding their destination. Repeated "bumps" or persistent "reminders" don't display patience and are looked down upon. Lastly, it is a bad practice to ask general questions to a specific person (in direct messages for example). Try to ask in public as much as you can, and patiently wait for the response ### Inclusivity, kindness and respectfulness Please be courteous and respectful to fellow members. Avoid offensive comments related to age, body size, disability, ethnicity, gender identity, experience level, education, socio-economic status, nationality, personal appearance, race, religion, or sexual orientation. Strictly prohibited are sexualized imagery, violence, intimidation, stalking, disruptions, sharing personal information without explicit permission, unwanted physical contact, and unwelcome sexual attention. Use inclusive language respecting our community's diversity. Avoid assumptions about others' backgrounds. Maintain a positive and professional demeanour, refraining from threatening or inappropriate behaviour. **We have zero tolerance for discrimination**. Any form of discrimination, including harassment, will lead to immediate consequences, potentially expulsion. ### Inquisitive ***The only stupid question is the one that does not get asked***. We encourage our users to ask early and ask often. Rather than asking whether you can ask a question (the answer is always yes!), instead, simply ask your question. You are encouraged to provide as many specifics as possible. Code snippets in the form of images are bad practice. Instead, use text formatted as code (using backticks) on Discord or simply send a gist. Refrain from pasting multiple lines of code directly into the chat channels - instead use [gist.github.com](http://gist.github.com/) or another paste site to provide code snippets. ### Helpful Novu welcomes users of all skill levels. We were all beginners once, and a supportive environment is essential for the community to thrive. While it can be repetitive to answer the same questions, members are expected to be courteous and helpful to everyone. Avoid sarcastic responses and prioritize useful information. Everyone should read the provided documentation. We're here to answer questions, offer guidance, and suggest workflows, but not to do your job for you. ### Anti-harassment policy Harassment includes (but is not limited to) all of the following behaviors: * Offensive comments related to gender (including gender expression and identity), age, sexual orientation, disability, physical appearance, body size, race, and religion. * Derogatory terminology including words commonly known to be slurs * Posting sexualized images or imagery in public spaces * Deliberate intimidation * Stalking * Posting others' personal information without explicit permission * Sustained disruption of talks or other events * Inappropriate physical contact * Unwelcome sexual attention Immediate compliance is expected from participants asked to cease harassing behaviour. Sponsors must adhere to the anti-harassment policy, refraining from using sexualized images or creating a sexualized environment at events. Volunteer organizers, including meetup staff, should also avoid sexualized attire. Continuing inappropriate behaviour after being asked to stop constitutes harassment, even if not explicitly mentioned in this policy. It is respectful to stop doing something when asked to do so, and all community members are expected to promptly comply with such requests. ### Reporting policy violations Instances of abusive, harassing, or otherwise unacceptable behaviour may be reported to anyone with administrative power in the community (Admins or Moderators on Discord, members of the 'DevRel' role), or to the local organizers of an event or to [support@novu.co](mailto:support@novu.co). Meetup organizers are encouraged to prominently display points of contact for reporting unacceptable behaviour at local events. If a participant engages in harassing behaviour, the meetup organizers may take any action they deem appropriate. These actions may include but are not limited to warning the offender, expelling the offender from the event, and barring the offender from future community events. Organizers will be happy to help participants contact security or local law enforcement, provide escorts to an alternate location, or otherwise assist those experiencing harassment to feel safe during the meetup. We value the safety and well-being of our community members and want everyone to feel welcome at our events, both online and offline. We expect all participants, organizers, speakers, and attendees to follow these policies at all of our event venues and event-related social events. ### Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 1. **Correction** * `Community Impact`: Use of inappropriate language or other behaviour deemed unprofessional or unwelcome in the community. * `Consequence`: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behaviour was inappropriate. A public apology may be requested. 2. **Warning** * `Community Impact`: A violation through a single incident or series of actions. * `Consequence`: A warning with consequences for continued behaviour. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 3. **Temporary Ban** * `Community Impact`: A serious violation of community standards, including sustained inappropriate behaviour. * `Consequence`: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 4. **Permanent Ban** * `Community Impact`: Demonstrating a pattern of violation of community standards, including sustained inappropriate behaviour, harassment of an individual, or aggression toward or disparagement of classes of individuals. * `Consequence`: A permanent ban from any sort of public interaction within the community. The Novu Community Code of Conduct is licensed under the Creative Commons Attribution-Share Alike 3.0 license. Our Code of Conduct was adapted from Codes of Conduct of other open-source projects, including: * **Contributor Covenant** * **Elastic** * **The Fedora Project** * **OpenStack** * **Ansible** * **Puppet Labs** * **Ubuntu** Community Impact Guidelines were inspired by Mozilla's code of conduct enforcement ladder. For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). Translations are available at [https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations). file: ./content/docs/community/feature-flags.mdx # Feature Flags Learn how to enable/disable specific features in Novu Feature flags allow to turn certain functionality on and off based on configuration. In this way, users can preview beta features in their deployments. To enable the specific feature, you need to pass an environment variable to all services: Feature flag environment variable accepts boolean values: `true` or `false`. * `IS_MULTI_PROVIDER_CONFIGURATION_ENABLED` adds ability to connect multiple providers per channel and make them active. It also shows redesigned integrations store page.
Redesigned Integrations store page
Redesigned Integrations store page.
* `IS_MULTI_TENANCY_ENABLED` adds ability to manage tenants from dashboard.
Tenants page
Once multi-tenancy is enabled, the Tenants page will appear.
* `IS_TEMPLATE_STORE_ENABLED` enables template store which contains pre-made workflows for common use cases (like **Password Reset** workflow etc).
Template store
With template store enabled, you can choose from pre-made workflows.
file: ./content/docs/community/get-involved.mdx # Get Involved Your guide for engaging with the Novu Community ## Our Community At Novu, we're a vibrant, open-source community driven by passion, collaboration, and a shared love for pushing boundaries. We believe in the power of community-driven development, where every voice matters and every contribution counts. As such, contributions are at the heart of how we evolve and progress forward. ## Join the conversation * Read our [code of conduct](/community/code-of-conduct) * Say "Hi!" in our [Discord Server](https://discord.gg/novu?ref=docs-community-introduction) * Explore our [open issues](https://github.com/novuhq/novu/issues) * Subscribe to our newsletter [to stay in the know](https://novu.co/novu-community-2-0/?utm_campaign=docs-comm2-get-involved) ## Getting help Our fellow community members are always ready to help you get past a blocker. However, you can take a few things into consideration to help them help you: * Share as many details as you can share. It will help us in debugging the issue. * If there is any bug, please share steps to reproduce that bug. * If there is any issue in running Novu on a local machine, please share system details like operating system, RAM size, npm version and node version. * If there is any issue in our backend SDKs, please mention the SDK version and relevant details. * If looking for self-hosting support, please share `Novu Version` and remote server details. Our latest version is `0.24.0`. Please be patient with self-hosting help. We are a small team and we will try our best to help you. Use Discord #community-self-host channel for questions related to self hosting. ## Contributing All community members are of one of the following four types: 1. Open source users 2. Active member 3. Power member 4. Novu Ambassador ### Become an active community member * Everyone who joins our community becomes an open-source user automatically. * You can become an active community member by: * Submitting >2 PRs * Opening >2 issues * Sending >5 messages on Github * Commenting >3 times on Github * Active community members are one step closer to becoming Power community members and then an active moderator. ### Become a power community member * All active community members are eligible to become a power community member and can become so by having at least one of the following: * Submitting 3 PRs * Opening 3 Issues * Sending 10 Discord messages * Commenting >10 times on Github * Power community members are just one step away from becoming a Novu ambassador. ### Become a Novu Ambassador A Novu ambassador is a trusted power member of the community that can do the following: * Love exploring and sharing their knowledge of any technology with other developers. * Often write and speak about Novu. * Possess strong Novu expertise and often help and support Novu users within the community. * Socially influence developers with some knowledge of Novu. * Consistently contribute to Novu OSS product. We are launching the Novu Ambassador program soon with amazing perks. Stay tuned! ## Activities Are you passionate about notifications like we are? There are many different—and easy—ways to get involved. ### Help in onboarding and answering questions. * Welcome new users into the community * Answer questions in the [community support channel](https://discord.com/channels/895029566685462578/1019663407915483176). * Offer input and opinions about various solutions you have tried. ### Improve our documentation * Check out our [GitHub repo](https://github.com/novuhq/docs) * No documentation is perfect, and neither is ours * Help us improve our docs by * Updating outdated examples * Correcting typos and language for clarity * Find and fix broken links, etc. ### Help with the SDKs * Link to [contributors guide](https://github.com/novuhq/novu/blob/next/CONTRIBUTING.md) * We have SDKs in various languages and frameworks, most written and maintained by community members. * You can either become a maintainer there or help the existing maintainers by bringing the SDK up to speed with the latest features present in the core product. * We have backend SDKs in the following: [Node.js](https://github.com/novuhq/novu/tree/next/packages/node), [PHP](https://github.com/novuhq/novu-php), [.NET](https://github.com/novuhq/novu-dotnet), [Elixir](https://github.com/novuhq/novu-elixir), [Go](https://github.com/novuhq/go-novu), [Ruby](https://github.com/novuhq/novu-ruby), [Python](https://github.com/novuhq/novu-python) [Laravel](https://github.com/novuhq/novu-laravel), and [Kotlin](https://github.com/novuhq/novu-kotlin) * And Inbox SDKs: [React](https://github.com/novuhq/novu/tree/next/packages/react), [JavaScript](https://github.com/novuhq/novu/tree/next/packages/js) ### Create content * Content writers program: Apply to become a content writer with us and get paid to write about use-cases highlighting how you solved a problem using Novu * Write an article about what you built with Novu and share it with the community. We'll give you a shout out! ### PRs, issues, and bug reports * Check out our [contributors guide](https://github.com/novuhq/novu/blob/next/CONTRIBUTING.md) * Once you've gone through our [development process](/community/run-in-local-machine), you can contribute directly to open issues. * Open a new issue if a relevant one isn't already open. * Also, you can create bug reports if you find a bug somewhere. * Or a feature request if you find something that should be a feature but isn't. At Novu, we believe that no contribution is small, and the only wrong question is the one that doesn't get asked. So feel free to ask any question or raise that Pull Request. You're always welcome here! 🤗 ### Participate in office hours * We have frequent (replace with interval— twice weekly, weekly, etc.) and topic-specific office hours that offer the ability to engage in real-time with Novu staff and other community members * In these sessions, offer input, opinions, examples, etc. to contribute! We're excited to have you on board and look forward to your valuable contributions! Together, we'll shape the future of Novu. 🫂 file: ./content/docs/community/overview.mdx # Community Overview Get started and get involved with the Novu Project Welcome to the Novu community! As the leading open-source notification infrastructure, Novu enables developers and product teams to manage notifications across multiple channels seamlessly. Whether you're building in-app notifications, email alerts, or SMS updates, Novu provides the tools to simplify integration and enhance your communication strategy. Our mission is to create a platform where every developer can easily implement notifications, with a strong, growing community to support you in every step. ## Community details The Novu community is built on collaboration. Whether you're contributing code, helping with documentation, or supporting others in Discord, we welcome everyone. We have dedicated teams across various areas: * **Developers:** Working on the core project and building new features. * **Contributors:** Writing documentation, reporting bugs, and suggesting improvements. * **Maintainers:** Ensuring the health of the project by reviewing and merging contributions. * **Ambassadors**: The most involved contributors and community members nominated by the Community and approved by Novu. You can find our official community communication channels on Discord. Whether you're a seasoned developer or just getting started, there's a place for you here. ### Getting involved There are many ways to get involved: * Contribute code, raise issues, or request features on [GitHub](https://github.com/novuhq/novu). * Join discussions in the community on [Discord](https://discord.gg/novu). * Attend Novu events and participate in hackathons to connect with other community members. [Learn how to get involved with your new favorite open source project](/community/get-involved) ## Novu Cloud, or Novu Project? The Novu Project is our fully open source, community backed project. It is a complete notifications infrastructure platform that offers all the core components you need to implement notifications. Novu Cloud is our commercial service offering. It is a superset of the Novu Project that includes additional features and capabilities businesses require to succeed with notifications at scale. [See the full list of differences between Novu Cloud and Novu.](/community/project-differences) ## Using Novu There are two ways to get started with Novu. ### 1. Create a free account in our hosted Novu Cloud service **Who it's for:** Most businesses, smaller organizations and teams, businesses that do not want to architect and host their own infrastructure, and businesses that do not want to worry about upgrades and system maintenance. **Who it's not for:** Organizations that want to self-host their own instance of Novu, or plan on making customizations to their Novu installation. [Learn more about Novu Cloud's features, capabilities, and pricing.](https://novu.co/pricing) ### 2. Self-host Self-hosting Novu is a great way to rapidly test new custom providers, or to test your contributions. **Use the Community Docker instances** * **Who it's for:** Individuals and organizations that want to experiment with more advanced notifications infrastructure architectures, businesses that have a strict requirement to run notifications on premises or inside their own network boundary. * **Who it's not for:** Non-technical users or teams that are not familiar with implementation and management of container infrastructure. Organizations that require rapid scale-up out-of-the-box, and that don't have the skills and knowledge required to implement and adjust deployments. [Learn how to deploy Novu with Docker](/community/self-hosting-novu/deploy-with-docker) **Build it yourself** * **Who it's for:** Highly technical users that are deeply familiar with the required infrastructure components and technologies required to run it either locally or in your own cloud. Developers that want to experiment. Builders that are actively contributing to the Novu Project, and are testing new functionality. * **Who it's not for:** Nearly everyone else. ;) [Learn how to run Novu on your local machine](/community/run-in-local-machine) ## Get help For assistance with a Paid or Trial Novu Cloud account? Please use the in-app chat widget which is available in the lower right hand corner of the Novu Dashboard after you sign in. 🙂 Should you encounter an issue with either your free Novu Cloud account, or your Novu Project instance, the community is here to help. Our primary channel for community support is our Discord community. [Learn how to get help with your Novu Project or Free-tier Novu Cloud account](/community/get-involved#getting-help) ### Events Staying in tune with our upcoming community events, workshops, and hackathons is easy. Keep an eye out for new announcements in the [announcement](https://discord.com/channels/895029566685462578/1040040454906986578) channel in our [Discord community.](https://discord.gg/novu) That's the best way to stay updated about new events. You can also subscribe to our [youtube channel](https://www.youtube.com/@novuhq) and follow us on [dev.to](https://dev.to/novu). file: ./content/docs/community/project-differences.mdx # Self-Hosted and Novu Cloud Understand the difference between Self-Hosted and Novu Cloud ## Better together Self-Hosted is where the community comes together with our development teams to improve notifications for everyone. Novu Cloud is our commercial offering, and where we extend the capabilities of the Self-Hosted and make them accessible and consumable by all teams and businesses. Most simply, the Self-Hosted is how you test and experiment with notifications infrastructure, and Novu Cloud is how you press the easy button for all things notifications. ### Key differences Our goal is always provide a functional, powerful, and complete notifications infrastructure platform, whether you opt to self host or use our cloud offering. However, certain features we build into Novu Cloud are not available in the Self-Hosted. When this happens, it's for several reasons: * We rely on a paid third-party service provider * The feature is complex and requires significant investment (read: money!) time to build and maintain * The feature is primarily targeted at the largest and most sophisticated of use cases, and supporting these in self-hosted environments is challenging ### Current matrix Below you will find a table that outlines the best available Novu Cloud Tier, compared to the available Self-Hosted capabilities. | CAPABILITY | SELF-HOSTED | NOVU CLOUD | | -------------------------------------------------- | ----------------------- | ---------- | | Activity feed retention | Infratructure dependent | ✅ | | Block-based email editor | ✅ | ✅ | | Channels supported: Email, In-app, SMS, Chat, Push | ✅ | ✅ | | Code-based Framework workflows | ✅ | ✅ | | Custom environments | ❌ | ✅ | | Digest | ✅ | ✅ | | GUI-based workflows | ✅ | ✅ | | Max workflows | Infratructure dependent | Custom | | Provider integrations | Infratructure dependent | Unlimited | | Subscribers | Infratructure dependent | Unlimited | | **INBOX** | | | | Bell component | ✅ | ✅ | | Inbox component | ✅ | ✅ | | Inbox content component | ✅ | ✅ | | Notifications component | ✅ | ✅ | | Remove Novu branding | ❌ | ✅ | | Snooze | ❌ | ✅ | | User preferences component | ✅ | ✅ | | **ACCOUNT ADMINISTRATION AND SECURITY** | | | | Built-In authentication | ❌ | ✅ | | Custom SAML SSO, OIDC enterprise providers | ❌ | ✅ | | Max team members | 1 | Custom | | Multi-Factor Authentication (MFA) | ❌ | ✅ | | Role-Based Access Control (RBAC) | ❌ | Q3 2025 | | Standard SAML authentication (Google, Github) | ❌ | ✅ | | **COMPLIANCE** | | | | Custom security reviews | ❌ | ✅ | | Data Processing Agreements | ❌ | Custom | | GDPR | Installation dependent | ✅ | | HIPAA BAA | ❌ | ✅ | | SOC 2 / ISO 27001 | ❌ | ✅ | ### Ready to Scale? Upgrade to Novu Cloud If you're currently self-hosting Novu, Novu offers an Enterprise Edition offering for self-hosted users and a managed cloud based deployment. [Contact Us](https://novu.co/contact-us) to learn more. #### Why Upgrade to Novu Cloud? * **Zero Infrastructure Management** * No server maintenance, scaling headaches, and deployment complexities * Focus on what matters most - building your product * Let our team handle the heavy lifting * **Enterprise-Grade Features at Your Fingertips** * Access advanced security features including SAML SSO and MFA * Benefit from enterprise compliance (SOC 2, ISO 27001, HIPAA) * **Always Up-to-Date** * Automatic updates and improvements * Access to the latest features and security patches * No more manual upgrades or maintenance windows * **Support** * Expert assistance for seamless migration * Priority support for your team * Custom security reviews and compliance guidance file: ./content/docs/community/roadmap.mdx # Roadmap Learn about our roadmap import { Card, Cards } from 'fumadocs-ui/components/card'; import { MapIcon } from 'lucide-react'; } href="https://roadmap.novu.co/roadmap"> Learn about ongoing developments, upcoming plans, and items in our backlog.. Your involvement in shaping the future of our notification infrastructure solution is highly encouraged. ## Get involved **Submit Ideas:** If you have a new feature idea or an enhancement suggestion, don't hesitate to submit it through our [roadmap page](https://roadmap.novu.co/roadmap). Your creativity fuels our innovation! **Upvote and Comment:** Review the existing feature requests and upvote the ones that resonate with you. Feel free to provide additional insights or use-cases through comments. **Contribute:** If you're a developer, you can actively contribute to the development of the features in progress or even the backlog ones. [Join our community of contributors](https://discord.gg/novu?ref=docs-contribute) and help us bring these enhancements to life. Remember, this roadmap is a living document that evolves based on your input and the direction the community decides to take. Your voice matters, and we're excited to work together in building a robust notification infrastructure solution that meets everyone's needs. file: ./content/docs/community/run-in-local-machine.mdx # Run Novu in local machine Prerequisites and steps to run Novu in local machine. Learn how to set up Novu on your local environment for testing and development. import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/novuhq/novu) ### Requirements * Node.js version v20.8.1 * MongoDB * Redis * **(Optional)** pnpm - Needed if you want to install new packages * **(Optional)** localstack (required only in S3 related modules) We recommend having at least 8GB of RAM to run Novu on a local machine as Novu has multiple services running together with external services like redis, mongodb etc. ### Setup the project After installing the required services on your machine, you can clone and set up your forked version of the project: 1. Clone the repository ```shell git clone https://github.com/novuhq/novu.git ``` ```shell git clone https://github.com/{YOUR_GITHUB_USER_NAME}/novu.git ``` 2. Install all dependencies ```shell cd novu && npm run setup:project ``` 3. Run the project ```shell npm run start ``` The `npm run start` will start the Jarvis CLI tool which allows you to run the whole project with ease. If you only want to run parts of the platform, you can use the following run commands from the root project: * **start:dev** - Synonym to `npm run start` * **start:dashboard** - Only starts the dashboard management platform * **start:ws** - Only starts the WebSocket service for notification center updates * **start:widget** - Starts the widget wrapper project that hosts the notification center inside an iframe * **start:api** - Runs the API in watch mode * **start:worker** - Runs the worker application in watch mode * **start:dal** - Runs the Data Access Layer package in watch mode * **start:shared** - Starts the watch mode for the shared client and API library * **start:notification-center** - Runs and builds the React package for the Novu notification center ### Set up your environment variables If you have used Jarvis CLI tool from the previous step you don't need to setup the env variables as Jarvis will do that on the first run if setup wasn't done before. The command `npm run setup:project` creates default environment variables that are required to run Novu in a development environment. However, if you want to test certain parts of Novu or run it in production mode, you need to change some of them. These are all the available environment variables: * `NODE_ENV` (default: local)The environment of the app. Possible values are: dev, test, production, ci, local * `S3_LOCAL_STACK`The AWS endpoint for the S3 Bucket required for storing various media * `S3_BUCKET_NAME`The name of the S3 Bucket * `S3_REGION`The AWS region of the S3 Bucket * `PORT`The port on which the API backend should listen on * `FRONT_BASE_URL`The base url on which your frontend is accessible for the user. (e.g. dashboard.novu.co) * `DISABLE_USER_REGISTRATION` (default: false)If users should not be able to create new accounts. Possible values are: true, false * `REDIS_HOST`The domain / IP of your redis instance * `REDIS_PORT`The port of your redis instance * `REDIS_PASSWORD`Optional password of your redis instance * `REDIS_DB_INDEX`The Redis database index * `REDIS_CACHE_SERVICE_HOST`The domain / IP of your redis instance for caching * `REDIS_CACHE_SERVICE_PORT`The port of your redis instance for caching * `REDIS_CACHE_DB_INDEX`The Redis cache database index * `REDIS_CACHE_TTL`The Redis cache ttl * `REDIS_CACHE_PASSWORD`The Redis cache password * `REDIS_CACHE_CONNECTION_TIMEOUT`The Redis cache connection timeout * `REDIS_CACHE_KEEP_ALIVE`The Redis cache TCP keep alive on the socket timeout * `REDIS_CACHE_FAMILY`The Redis cache IP stack version * `REDIS_CACHE_KEY_PREFIX`The Redis cache prefix prepend to all keys * `REDIS_CACHE_SERVICE_TLS`The Redis cache TLS connection support * `IN_MEMORY_CLUSTER_MODE_ENABLED`The flag that enables the cluster mode. It might be Redis or ElastiCache cluster, depending on the env variables set for either service. * `ELASTICACHE_CLUSTER_SERVICE_HOST`ElastiCache cluster host * `ELASTICACHE_CLUSTER_SERVICE_PORT`ElastiCache cluster port * `REDIS_CLUSTER_SERVICE_HOST`Redis cluster host * `REDIS_CLUSTER_SERVICE_PORTS`Redis cluster ports * `REDIS_CLUSTER_DB_INDEX`Redis cluster database index * `REDIS_CLUSTER_TTL`Redis cluster ttl * `REDIS_CLUSTER_PASSWORD`Redis cluster password * `REDIS_CLUSTER_CONNECTION_TIMEOUT`Redis cluster connection timeout * `REDIS_CLUSTER_KEEP_ALIVE`Redis cluster TCP keep alive on the socket timeout * `REDIS_CLUSTER_FAMILY`Redis cluster IP stack version * `REDIS_CLUSTER_KEY_PREFIX`Redis cluster prefix prepend to all keys * `JWT_SECRET`The secret keybase which is used to encrypt / verify the tokens issued for authentication * `SENDGRID_API_KEY`The api key of the Sendgrid account used to send various emails * `MONGO_URL`The URL of your MongoDB instance * `MONGO_MAX_POOL_SIZE`The max pool size of the MongoDB connection * `NOVU_SECRET_KEY`The api key of dashboard.novu.co used to send various emails * `SENTRY_DSN`The DSN of sentry.io used to report errors happening in production * `NODE_ENV` (default: local) - The environment of the app. Possible values are: dev, test, production, ci, local * `PORT` - The port on which the Worker app should listen on * `STORE_ENCRYPTION_KEY` - The encryption key used to encrypt/decrypt provider credentials * `MAX_NOVU_INTEGRATION_MAIL_REQUESTS` - The number of free emails that can be sent with the Novu email provider * `NOVU_EMAIL_INTEGRATION_API_KEY` - The Novu email provider Sentry API key * `STORAGE_SERVICE` - The storage service name: AWS, GCS, or AZURE * `S3_LOCAL_STACK` - The LocalStack service URL * `S3_BUCKET_NAME` - The name of the S3 Bucket * `S3_REGION` - The AWS region of the S3 Bucket * `GCS_BUCKET_NAME` - The name of the GCS Bucket * `AZURE_ACCOUNT_NAME` - The name of the Azure account * `AZURE_ACCOUNT_KEY` - The Azure account key * `AZURE_HOST_NAME` - The Azure host name * `AZURE_CONTAINER_NAME` - The Azure container name * `AWS_ACCESS_KEY_ID` - The AWS access key * `AWS_SECRET_ACCESS_KEY` - The AWS secret access key * `REDIS_HOST` - The domain / IP of your redis instance * `REDIS_PORT` - The port of your redis instance * `REDIS_PASSWORD` - Optional password of your redis instance * `REDIS_DB_INDEX` - The Redis database index * `REDIS_CACHE_SERVICE_HOST` - The domain / IP of your redis instance for caching * `REDIS_CACHE_SERVICE_PORT` - The port of your redis instance for caching * `REDIS_CACHE_DB_INDEX` - The Redis cache database index * `REDIS_CACHE_TTL` - The Redis cache ttl * `REDIS_CACHE_PASSWORD` - The Redis cache password * `REDIS_CACHE_CONNECTION_TIMEOUT` - The Redis cache connection timeout * `REDIS_CACHE_KEEP_ALIVE` - The Redis cache TCP keep alive on the socket timeout * `REDIS_CACHE_FAMILY` - The Redis cache IP stack version * `REDIS_CACHE_KEY_PREFIX` - The Redis cache prefix prepend to all keys * `REDIS_CACHE_SERVICE_TLS` - The Redis cache TLS connection support * `IN_MEMORY_CLUSTER_MODE_ENABLED` - The flag that enables the cluster mode. It might be Redis or ElastiCache cluster, depending on the env variables set for either service. * `ELASTICACHE_CLUSTER_SERVICE_HOST` - ElastiCache cluster host * `ELASTICACHE_CLUSTER_SERVICE_PORT` - ElastiCache cluster port * `REDIS_CLUSTER_SERVICE_HOST` - Redis cluster host * `REDIS_CLUSTER_SERVICE_PORTS` - Redis cluster ports * `REDIS_CLUSTER_DB_INDEX` - Redis cluster database index * `REDIS_CLUSTER_TTL` - Redis cluster ttl * `REDIS_CLUSTER_PASSWORD` - Redis cluster password * `REDIS_CLUSTER_CONNECTION_TIMEOUT` - Redis cluster connection timeout * `REDIS_CLUSTER_KEEP_ALIVE` - Redis cluster TCP keep alive on the socket timeout * `REDIS_CLUSTER_FAMILY` - Redis cluster IP stack version * `REDIS_CLUSTER_KEY_PREFIX` - Redis cluster prefix prepend to all keys * `MONGO_URL` - The URL of your MongoDB instance * `MONGO_MAX_POOL_SIZE` - The max pool size of the MongoDB connection * `NEW_RELIC_APP_NAME` - The New Relic app name * `NEW_RELIC_LICENSE_KEY` - The New Relic license key * `SEGMENT_TOKEN` - The Segment Analytics token * `REACT_APP_ENVIRONMENT` The environment of the app. Possible values are: dev, test, production, ci, local * `REACT_APP_API_URL` The base url on which your API backend would be accessible * `REACT_APP_WS_URL` The base url on which your WebSocket service would be accessible * `SKIP_PREFLIGHT_CHECK` (default: true)Solves a problem with React App dependency tree. When configuring different than default values for the API and WebSocket URLs, in order for the Web app to apply the changes done to the `./env` file, it is needed to run the script `pnpm envsetup`. This will generate a file called `env-config.js` that will be copied inside of the `public` folder of the application. Its purpose is to inject in the `window._env_`object the chosen environment variables that manage the URLs the Web client will call to access to the API backend and the WebSocket service. * `NODE_ENV` (default: local)The environment of the app. Possible values are: dev, test, production, ci, local * `SENTRY_DSN`The DSN of sentry.io used to report errors happening in production * `REDIS_HOST`The domain / IP of your redis instance * `REDIS_PORT`The port of your redis instance * `REDIS_DB_INDEX`The database index of your redis instance * `REDIS_PASSWORD`Optional password of your redis instance * `JWT_SECRET`The secret keybase which is used to encrypt / verify the tokens issued for authentication * `MONGO_URL`The URL of your MongoDB instance * `MONGO_MAX_POOL_SIZE`The max pool size of the MongoDB connection * `PORT`The port on which the WebSocket service should listen on ### Running tests After making changes, you can run the tests for the respective package using the appropriate CLI commands: ### API To run the API tests, run the following command: ```shell npm run start:worker:test npm run start:e2e:api ``` The tests create a new instance of Novu and a test db and run the tests against it. The test db is removed after all tests have finished running. ### Dashboard To run the front end tests for the dashboard project using cypress you need to install localstack. The cypress tests perform E2E tests. To be able to perform E2E tests, you need to run the API service in the appropriate test environment. Run the services in test env with the following commands: ```shell npm run start:dashboard npm run start:api:test npm run start:worker:test npm run start:ws:test ``` Run the cypress test suite with the following command: ```shell cd apps/dashboard && npm run cypress:run ``` To open the cypress management window to debug tests, run the following commands: ```shell cd apps/dashboard && npm run cypress:open ``` ### Different ports used by the services * **3000** - API * **3002** - WebSocket Service * **3003** - Webhook Service * **3004** - Worker Service * **4200** - Dashboard Management UI * **4701** - Iframe embed for notification center * **4500** - Widget Service ### Testing providers To run tests against the providers' folder, you can use the `npm run test:providers` command. ### Local environment setup script (beta) As an option in our script runner `Jarvis` we have made available an option to run [this script](https://github.com/novuhq/novu/blob/2f2abdcaaad8a7735e0a2d488607c3276c8975fd/scripts/dev-environment-setup.sh) that will automatically try to install all the dependencies needed to be able to run Novu locally, as the previous step of installing the project dependencies through `pnpm install`. When executing it inside `Jarvis`, you will need to have previously installed by yourself `git` and `node`, as we mentioned earlier on this page. The script can be run on its own without any previous dependency installed, as it is prepared to execute the following tasks: * Check the running OS in the local machine (currently only MacOSx and [GNU Linux](https://en.wikipedia.org/wiki/GNU/Linux_naming_controversy)supported) * Install of OS dependencies (currently only MacOSx supported) -- MacOSx: It will execute the following tasks --- Will try to install or update [XCode](https://developer.apple.com/xcode/) (skippable step; though XCode installs `[git](https://git-scm.com/)` that is a required dependency for later) --- Will install [Rosetta](https://support.apple.com/en-gb/HT211861) for Apple CPUs --- Will set up some opinionated OS settings * Will check if `[git](https://git-scm.com/)` is installed and if not will abort the operation * Will make [ZSH](https://en.wikipedia.org/wiki/Z_shell) the default shell to be able to execute the next task * Will (opinionatedly) install [Oh My Zsh!](https://ohmyz.sh/) (skippable task) * Will (opinionatedly) install the [Homebrew](https://brew.sh/) package manager and will set up your local environment to execute it besides adding some casks * Will (opinionatedly) install [NVM](https://github.com/nvm-sh/nvm) as a Node.js version manager * Will install the required [Node.js](https://nodejs.org/en/) version to be able to [run Novu](https://github.com/novuhq/novu/blob/2f2abdcaaad8a7735e0a2d488607c3276c8975fd/package.json#L180) * Will install [PNPM](https://pnpm.io/) as a package manager, required dependency for some of the tasks inside Novu's scripts * Will install [Docker](https://www.docker.com/) as containerized application development tool * Will install required databases [MongoDB](https://www.mongodb.com/) (Community version) and [Redis](https://redis.io/) through Homebrew * Will install the [AWS CLI](https://aws.amazon.com/cli/) tool (not required to run Novu; it is a core maintainer used tool) * Will create a local development domain `local.novu.co` in your local machine * Will clone the Novu repository in your local machine (skippable step) to a selected folder `$HOME/Dev` This script has only been thoroughly tested in MacOSx. Little testing has been run in GNU Linux. This script is not bullet-proof and some of the tasks have intertwined dependencies with each other. We have tried to make it as idempotent as possible but some loose knots will probably show because of conflicts between versions of the different dependencies. Please report to us any problem found and we will try to fix or assist though we do not have the resources to make it idempotent in every potential system and potential combinations file: ./content/docs/api-reference/idempotency.mdx # Idempotency Learn how to use idempotency keys to safely retry API requests without causing duplicate operations Idempotency is a crucial feature for building reliable integrations with Novu's API. It allows you to safely retry requests without worrying about duplicate operations, which is especially important for critical actions like triggering workflows. An idempotent request is one that can be made multiple times with the same effect as making it once. This is achieved by including a unique idempotency key with your request. Idempotency feature is currently not enabled for all organizations. Please contact us at [support@novu.co](mailto:support@novu.co) to enable it for your organization. ## How It Works When you send a request with an idempotency key: 1. Novu checks if a request with the same key has been processed before 2. If it's a new key, the request is processed normally and the response is cached 3. If the key was seen before, Novu returns the cached response without reprocessing the request This mechanism protects against network issues, timeouts, and other scenarios where a request might be retried. ## Using Idempotency Keys To make an idempotent request, include the `Idempotency-Key` header with a unique identifier: ```bash curl -X POST https://api.novu.co/v1/events/trigger \ --header 'Authorization: ApiKey ' \ --header 'Idempotency-Key: unique-request-id-12345' \ --header 'Content-Type: application/json' \ --data '{"name": "workflow-id", "to": {"subscriberId": "subscriber-123"}}' ``` ### Key Requirements * **Maximum length**: 255 characters * **Uniqueness**: Keys should be unique per organization * **Format**: Any string value (UUIDs are recommended) We recommend using UUIDs or other unique identifiers that include context about the operation, such as `order-confirmation-{orderId}-{timestamp}`. ## Supported Methods Idempotency is supported for the following HTTP methods: | Method | Supported | | ------ | --------- | | POST | ✅ Yes | | PATCH | ✅ Yes | | GET | ❌ No | | PUT | ❌ No | | DELETE | ❌ No | GET, PUT, and DELETE methods are inherently idempotent by design and don't require idempotency keys. ## Authentication Requirements Idempotency is only available when authenticating with an API Key: ```bash --header 'Authorization: ApiKey ' ``` Requests using other authentication methods will be processed normally without idempotency support. ## Cache Duration | Scenario | Cache TTL | | ------------------- | --------- | | Successful response | 24 hours | | Error response | 24 hours | | In-progress request | 5 minutes | After the cache expires, the same idempotency key can be reused. ## HTTP Response Headers When making idempotent requests, Novu includes helpful headers in the response: ### Idempotency-Key Confirms the idempotency key that was used for the request: ``` Idempotency-Key: unique-request-id-12345 ``` ### Idempotency-Replay Indicates that the response was served from the cache rather than processing a new request: ``` Idempotency-Replay: true ``` This header is only present when returning a cached response. ## Error Handling ### Request In Progress (409 Conflict) If you send a request with an idempotency key while a previous request with the same key is still being processed: ```json { "statusCode": 409, "message": "Request with key \"unique-request-id-12345\" is currently being processed. Please retry after 1 second" } ``` The response includes a `Retry-After` header indicating when to retry: ``` Retry-After: 1 ``` ### Key Reused with Different Body (422 Unprocessable Entity) If you send a request with an idempotency key that was previously used with a different request body: ```json { "statusCode": 422, "message": "Request with key \"unique-request-id-12345\" is being reused for a different body" } ``` Each idempotency key must be associated with a specific request body. Using the same key with different payloads will result in an error. ### Key Too Long (400 Bad Request) If the idempotency key exceeds 255 characters: ```json { "statusCode": 400, "message": "idempotencyKey \"...\" has exceeded the maximum allowed length of 255 characters" } ``` ## Best Practices 1. **Generate unique keys**: Use UUIDs or combine entity IDs with timestamps to ensure uniqueness 2. **Store keys**: Keep track of idempotency keys you've used in case you need to retry requests 3. **Handle 409 responses**: Implement retry logic with exponential backoff when you receive a conflict response 4. **Don't reuse keys for different operations**: Each unique operation should have its own idempotency key 5. **Include meaningful context**: Consider including the operation type and relevant IDs in your key for easier debugging ## Example: Safe Retry Logic Here's an example of implementing safe retry logic with idempotency: ```javascript import { Novu } from '@novu/api'; import { v4 as uuidv4 } from 'uuid'; const novu = new Novu({ secretKey: "" }); async function triggerNotificationWithRetry(workflowId, subscriberId, payload, maxRetries = 3) { const uniqueId = uuidv4(); const idempotencyKey = `trigger-${workflowId}-${subscriberId}-${uniqueId}`; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const response = await novu.trigger({ workflowId: workflowId, to: { subscriberId: subscriberId }, payload, }, idempotencyKey); return response; } catch (error) { if (error.statusCode === 409 && attempt < maxRetries) { // Request in progress, wait and retry const retryAfter = error.headers?.['retry-after'] || 1; await new Promise(resolve => setTimeout(resolve, retryAfter * 1000)); continue; } throw error; } } } ``` file: ./content/docs/api-reference/overview.mdx # Overview In this page you can learn about how to work with Novu's API It's important to note that our API and backend SDK are intended for use exclusively in server-side applications. **Attempting to use them in a client-side application will result in Cross-Origin Resource Sharing (CORS) errors.** This restriction ensures the security and integrity of our services. ## Authentication Authentication for the Novu API involves the use of an API Key, which is a secure credential that is tied to your Novu account. This key should be included in the header of the request in the Authorization field as a string prefixed with 'ApiKey '. ```bash --header 'Authorization: ApiKey ' ``` For example, when using Novu in a Node.js application, the Novu package should be imported and initialized with the API key, as shown in this snippet: ```javascript import { Novu } from '@novu/api'; const novu = new Novu({ secretKey: "NOVU_SECRET_KEY", }); ``` Replace `` with your actual API Key, available in the API Key section of the Novu Dashboard. It is advised not to hardcode your credentials in a file in production environments. Use environment variables instead. ## API Endpoints Novu provides a multitude of API endpoints that enable a variety of functionalities. the base URL for the Novu API is `https://api.novu.co/v1`. We offer two API options: the US API and the EU API. By default, our API documentation refers to the US API, which can be accessed at: [https://api.novu.co/v1](https://api.novu.co/v1). If you require the EU version, you can access it here: [https://eu.api.novu.co/v1](https://eu.api.novu.co/v1). For instance, to get tenant information, the endpoint to use would include the tenant's identifier and look like this `https://api.novu.co/v1/tenants/{identifier}`. file: ./content/docs/api-reference/payload-limits.mdx # Payload Limits Learn about payload limits when working with Novu's API. Payload size limits are an essential functionality for ensuring optimal performance and reliability of the system. They safeguard system resources by preventing oversized payloads from impacting API performance and ensure efficient processing of event triggers. ## Payload size limit All event trigger operations have a **512KB (524,288 bytes)** payload size limit. This limit applies to the following endpoints: * **Trigger event** - Single event triggers * **Bulk trigger event** - Batch event triggers * **Broadcast event** - Events sent to all subscribers Attachments are excluded from the payload size calculation and have a separate size limit. ## What counts towards the limit The payload size limit includes: * Event payload data * Custom data and properties * All other request body content ## Error messages When your payload exceeds the 512KB limit, you'll receive an error response indicating the exact size and the limit exceeded. ### Single event trigger For single event triggers (`/events/trigger`), the error response will be: ```json { "statusCode": 413, "message": "Payload size (524342 bytes) exceeds maximum allowed size of 524288 bytes (512KB). Note: Attachments are excluded from this limit." } ``` ### Bulk trigger event For bulk event triggers (`/events/trigger/bulk`), the error response includes the index of the event that exceeded the limit: ```json { "statusCode": 413, "message": "Event at index 1 (workflow: \"workflow-name\") has payload size (524342 bytes) exceeds maximum allowed size of 524288 bytes (512KB). Note: Attachments are excluded from this limit." } ``` When using bulk trigger, each event in the array is validated individually against the 512KB limit. Ensure that every event payload stays within the limit. ### Broadcast event For broadcast events (`/events/trigger/broadcast`), the error response will be: ```json { "statusCode": 413, "message": "Payload size (524342 bytes) exceeds maximum allowed size of 524288 bytes (512KB). Note: Attachments are excluded from this limit." } ``` ## Attachment size limit Email attachments have a separate size limit: * **Total attachment size**: 20MB for all attachments included in an email. Attachments do not count towards the 512KB payload limit For more information about sending email attachments, refer to the [Sending Attachments](/platform/integrations/email#sending-attachments) documentation. ## Related documentation * [Rate Limiting](/api-reference/rate-limiting) - Learn about API rate limits * [Trigger Event](/api-reference/events/trigger-event) - Single event trigger API * [Bulk Trigger Event](/api-reference/events/bulk-trigger-event) - Batch event trigger API * [Broadcast Event](/api-reference/events/broadcast-event-to-all) - Broadcast to all subscribers file: ./content/docs/api-reference/rate-limiting.mdx # Rate Limiting In this page you can learn about how rate limiting works with Novu's API Rate limiting is an essential functionality for establishing a robust and resilient system. It safeguards system resources from being misused by malicious actors or being monopolized by one client. A variable-cost token bucket rate limited algorithm has been added to provide the capability for different API controllers and methods to have a varying cost. It also lays a foundation for dynamic costing of resource consumption. ## Limits The following limits apply to each category of the Novu system. Each category has an independent bucket of request tokens to consume from. Standard requests cost 1 request token and bulk requests cost 100 request tokens. Both standard and bulk requests consume from the same token pool. Each category has a different limit of requests per second (RPS), with the endpoints in each category shown below. | Category | Free | Pro | Team | Enterprise | Endpoints | | ------------- | ------ | ------- | ------- | ---------- | -------------------------------------------------------------- | | Events | 60 RPS | 240 RPS | 600 RPS | 6K RPS | Trigger | | Configuration | 20 RPS | 80 RPS | 200 RPS | 2k RPS | Subscribers, Topics, Contexts | | Global | 30 RPS | 120 RPS | 300 RPS | 3K RPS | All other endpoints consume request tokens from this category. | ### HTTP response headers When integrating with Novu API, it’s important to consider the rate limiting HTTP headers included in the response. These headers help you manage your API usage and avoid hitting rate limits. ``` RateLimit-Remaining: 219 RateLimit-Limit: 300 RateLimit-Reset: 2 RateLimit-Policy: 300;w=5;burst=330;comment="token bucket";category="trigger";cost="bulk";serviceLevel="free" ``` * `RateLimit-Remaining` - Indicates the remaining number of request tokens in the current window. * `RateLimit-Limit` - Indicates the total number of request tokens available in the current window. * `RateLimit-Reset` - Indicates the number of seconds until the current window resets and the request token limit is fully replenished. * `RateLimit-Policy` - Defines the details of the applied rate limiting policy. * `Retry-After` - Specifies the number of seconds to wait before making another request. file: ./content/docs/framework/chat-channel.mdx # Chat Learn the process of configuring and using chat providers with Novu Novu brings chat notifications into your development workflow, giving you a unified way to manage messaging across platforms and apps. Whether you're working with tools like Slack or Microsoft Teams or apps like WhatsApp, Telegram, and Discord, Novu lets you integrate, manage, and scale chat notifications without unnecessary complexity. Learn more about the [Chat Channel](/platform/integrations/chat). ```tsx await step.chat('chat', async () => { return { body: 'A new post has been created', }; }); ``` file: ./content/docs/framework/controls.mdx # Controls Learn how to use Controls in your notification workflows import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; Controls are defined using [JSON Schema](/framework/schema/json-schema) or [Zod](https://zod.dev), providing a strong run-time validation system for your workflows. This ensures that you as the developer and your non-technical peers are speaking the same language. Those responsible for styling and copy can edit with confidence, knowing their changes are tested in code. ## Controls vs Payload **Control Schema** - For Non-Technical Peers and Developers. Managed in the Novu Dashboard UI, defined by developers and used by non-technical peers. **Payload Schema** - For Developers. Passed during the `novu.trigger` method, and controlled by the developer. ## Common usecases * **Content** - Modify any static content: email subject, email body, push notification title, etc... * **Styling** - Modify the styling of the content: button color, background color, font size, etc... * **Behaviour** - Modify the behaviour of the content: show/hide a section, show/hide a button, etc... * **Order** - Modify the order of the content: the order of the email sections, the order of the buttons, etc... * **Actions** - Modify the behaviour of actions: digest duration, etc... * **Other** - Any other use case that should be controller without modifying code ## Step Controls Step Control schema defines the control passed during the `step` method. These controls can be modified and persisted in the Novu Dashboard UI. The snippet below shows a configuration for the Step Control schema. If you don't provide a schema, Typescript will infer the data type to `unknown`, reminding you of the best practice to specify your schema. ```tsx import { z } from 'zod'; import { render } from 'react-email'; import { ReactEmailContent } from './ReactEmailContent'; workflow('new-signup', async ({ step, payload }) => { await step.email( 'send-email', async (controls) => { return { subject: controls.subject, body: render( ), }; }, { controlSchema: z.object({ hideBanner: z.boolean().default(false), subject: z.string().default('Hi {{subscriber.firstName | capitalize}}'), components: z.array( z.object({ type: z.enum(['header', 'cta-row', 'footer']), content: z.string(), }) ), }), } ); }); ``` ```tsx import { IsString, IsNotEmpty, IsOptional, IsBoolean } from 'class-validator'; import { Type } from 'class-transformer'; import { render } from 'react-email'; import { ReactEmailContent } from './ReactEmailContent'; class NewSignUpComponent { @IsString() subject: string; @IsString() content: string; } class NewSignUpControlSchema { @IsBoolean() hideBanner: boolean; @IsString() @IsNotEmpty() @IsOptional() subject?: string; // Allowing no code control over the component in the Dashboard UI @Type(() => NewSignUpComponent) @NestedValidation({ each: true }) @IsOptional() components?: NewSignUpComponent[]; } workflow('new-signup', async ({ step, payload }) => { await step.email( 'send-email', async (controls) => { return { subject: controls.subject, body: render( ), }; }, { // Learn about Class-Validator Schema here: https://github.com/typestack/class-validator controlSchema: NewSignUpControlSchema, } ); }); ``` ```tsx workflow("new-signup", async ({ step, payload }) => { await step.email( "send-email", async (controls) => { return { subject: controls.subject, body: render( ), }; }, { // Learn about JSON Schema here: https://json-schema.org/specification controlSchema: { // Always `object` type: "object", // Specify the properties to validate. Supports deep nesting. properties: { hideBanner: { type: "boolean", default: false }, subject: { type: "string", default: 'Hi {{subscriber.firstName | capitalize}}' }, // Allowing no code control over the component in the Dashboard UI components: { type: "array", items: { type: "object", }, properties: { subject: { type: "string" }, content: { type: "string" }, } }, }, // Specify the array of which properties are required. required: ["hideBanner"], // Used to enforce full type strictness, with no rogue properties. additionalProperties: false, // The `as const` is important to let Typescript know that this // type won't change, enabling strong typing on `inputs` via type // inference of the provided JSON Schema. } as const, } ); }); ``` For the full list of parameters, check out the [full SDK reference](/framework/typescript/steps). ## Schema Validation & IDE IntelliSense You can use **Zod, Class-Validator or JSON Schema** based on your needs. * **[Zod](https://zod.dev/)** - A TypeScript-first schema declaration and validation library. *(Novu supports Zod v3)* * **[Class-Validator](https://github.com/typestack/class-validator)** - A TypeScript-first validation library using decorators for OOP-style applications. * **[JSON Schema](/framework/schema/json-schema)** - The most popular schema language for defining JSON data structures. If you only want local IDE IntelliSense, you are able to pass plain JS Classes, which will not provide any Schema Definition useable by Novu Platform. All provided **Zod** and **Class-Transformer** Schemas are compiled into **JSON Schema** which is passed to Novu. This ensures a consistent validation approach and UX by managing Payload and Control Data directly from the Platform. There may be inconsistencies when using Class-Transformer especially with nested schema objects. Please check out the guidelines on converting Class-Transformer classes to JSON Schema before using it here: [class-validator-jsonschema](https://www.npmjs.com/package/class-validator-jsonschema). ## Using Variables To facilitate the use of variables in the control schema, enclose the variable name in double curly braces using the `{{variableName}}` syntax. For example, `{{subscriber.firstName | capitalize}}` will be dynamically replaced with the actual value of the subscriber's first name at runtime. You can use variables in any step control value, whether set by the developer or within the Novu Dashboard UI. To facilitate this, the Novu Dashboard UI offers auto-completion for variables. Simply start typing `{{` to view a list of all available variables. ![Example for variables autocomplete in dashboard](/images/controls-autocomplete.gif) ### Variable Options * **Subscriber Attributes**: Access all [subscriber attributes](/platform/concepts/subscribers#subscriber-attributes). Example: `{{subscriber.firstName}}` * **Payload Variables**: Use all payload variables defined in the `payloadSchema`. Example: `{{payload.userId}}` * **Liquid Filters**: Apply [liquid filters](https://liquidjs.com/filters/overview.html) to format or manipulate variable values. Examples: `{{subscriber.firstName | append: ': ' | append: payload.status | capitalize}}` or `{{payload.invoiceDate | date: '%a, %b %d, %y'}}` will format the date as `Thu, Jan 01, 24` file: ./content/docs/framework/custom.mdx # Custom Step Used to execute any custom code as a step in the workflow. A custom steps allows to execute any custom logic and persist in the durable execution context. The result of this step can be used in subsequent steps. ## Common usecases * Making an API call to 3rd party service * Fetch data from a database to be used in subsequent steps * Execute a custom logic to transform data * Custom provider implementation ## Custom Step Interface ```tsx const stepResult = await step.custom( 'custom-step', async () => { return { item_name: 'A product name', item_price: 100, }; }, { outputSchema: { type: 'object', properties: { item_name: { type: 'string' }, item_price: { type: 'number' }, }, required: ['item_name', 'item_price'], }, } ); ``` ### Output Schema Definition This JSON Schema definition is used to validate the output of the custom step. If the output does not match the schema, the workflow will fail. Novu Framework will infer the Typescript interface from the JSON Schema definition. ### Return Value The Custom Step function should return a valid serializable object. The return value will be persisted in the durable execution context. ## Using the Custom Step Result The result can only be used in the `resolver` of the step/providers/skip functions of subsequent steps. ```tsx workflow('hello-world-workflow', async ({ payload }) => { const task = await step.custom( 'fetch-db-data', async () => { const taskData = db.fetchTask(payload.task_id); return { task_id: taskData.id, task_title: taskData.title, complete: taskData.complete, }; }, { outputSchema: { type: 'object', properties: { task_title: { type: 'string' }, task_id: { type: 'string' }, complete: { type: 'boolean' }, }, required: ['task_id', 'complete'], }, } ); await step.email( 'send-email', () => { return { subject: `Task reminder for ${task.task_title}`, body: 'Task is not yet complete. Please complete the task.', }; }, { // Only send the reminder E-mail if the task is not complete skip: () => !task.complete, } ); }); ``` To read more about the full list of parameters, check out the [full SDK reference](/framework/typescript/steps/custom). file: ./content/docs/framework/delay.mdx # Delay Action Learn how to use Delay steps in your notification workflows import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; The delay action awaits a specified amount of time before moving on to trigger the following steps of the workflow. Learn more about the [Delay](/platform/workflow/delay). ## Common usecases * Waiting for X amount of time before sending the message * Wait for a short period of time before sending a push message in case the user seen the notification in the Inbox Component * Allow the user some time to cancel an action that generated a notification ## Adding a delay step Delay steps can be inserted at any stage of your workflow execution, they can happen after or before any action. The workflow execution will be halted for the given amount of time and then resumed to the next step in the flow. The action can also be skipped using the skip parameter conditionally to allow more complex usecases of when to wait and when to send an email immediately. Here, we are delaying the execution of the next step by 1 day and skipping the delay step if the isCriticalMessage function returns true. ```tsx await step.delay( 'delay', () => { return { type: 'regular', unit: 'days', amount: 1, }; }, { skip: () => isCriticalMessage(), } ); ``` Here, we are delaying the execution of the in-app step by 30 minutes and sending the in-app notification only if the subscriber has `goalReminderInAppAllowed` set to `true` for subscriber. If during 30 minutes delay window, subscriber sets `goalReminderInAppAllowed` to `false`, the in-app step will be skipped. ```tsx export const goalReminderInAppAfterDelay = workflow( 'goal-reminder-in-app-after-delay', async ({ step, subscriber }) => { await step.delay('delay-step', async () => { return { type: 'regular', amount: 30, unit: 'minutes', }; }); await step.inApp( 'in-app-step', async () => { return { subject: `Don't Forget Your Fitness Goals Today!`, body: `Hey ${subscriber.firstName}, it's been a while since you logged your last activity. Keep up the momentum and complete your workout to stay on track with your goals!`, }; }, { skip: () => subscriber.data?.goalReminderInAppAllowed === false, } ); } ); ``` Changing the step content after triggering the workflow with delay step will not affect the existing pending delayed notification content. ## Frequently Asked Questions ### If delay step fails, will the workflow continue to the next step? No, workflow execution will stop immediately if the delay step fails due to an error. file: ./content/docs/framework/digest.mdx # Digest Action Learn how to use the Digest Engine to collect multiple events into a single message You can use the Digest Engine to collect multiple events to a single message. Learn more about the [Digest Engine](/platform/workflow/digest). ## Defining a digest step ```tsx const digestResult = await step.digest('digest', async () => { return { unit: 'days', // 'seconds' | 'minutes' | 'hours' | 'days' | 'weeks' | 'months' amount: 3, // the number of units to digest events for }; }); ``` ## Writing digest content In many cases, you will need to access all the digested events payload in order to show the user all or parts of the events included in this digest. **For example:** "John and 5 others liked your photo." The digest function returns an array of triggers that have been digested. You can use this array to perform any necessary actions on the digested triggers. Like Sending and email, or updating a database. ```tsx const { events } = await step.digest('digest-3-days', async () => { return { unit: 'days', // 'seconds' | 'minutes' | 'hours' | 'days' | 'weeks' | 'months' amount: 3, // the number of units to digest events for }; }); await step.email('send-email', async () => { const eventCount = events.length; return { subject: 'Digest Email', body: `You have ${eventCount} new events`, }; }); ``` ## Cron based digest You can use cron based digest to digest events based on a cron expression. ```tsx const digestedEvents = await step.digest('cron-digest', async () => { return { cron: '0 0 * * *', // every day at midnight }; }); ``` ## Custom Digest Key You can use a custom digest key to digest events based on a custom key. By default, events are digested based on the `subscriberId`. With a custom digest key, events are digested based on the combination of the `subscriberId` and the `digestKey` value. ```tsx export const customDigestKey = workflow( "custom-digest-key", async ({ step, payload }) => { const { events } = await step.digest("digest-step", async () => { return { unit: "hours", amount: 1, digestKey: payload.ticket_id, }; }); console.log("events ==>", events); // use above events to send email / in-app notification }, { payloadSchema: z.object({ ticket_id: z.string(), }), } ); ``` Step controls: At the moment, it is not possible to use digest information in step controls. You can only use it from the code, by creating a custom component for handling digested data. The digest step returns an object with events array. Each event in the array has the following properties: * **id** - The job id of the digested event * **time** - The time when the event was triggered * **payload** - The original payload passed to the event ## Two digest steps in a workflow Currently, Novu does not support two digest steps in a workflow. However, two digest behaviours can be achieved by creating second workflow with digest step, add all other steps after the digest step and trigger the second workflow from the first workflow in [custom step](/framework/custom). **Use Case: LLM-Powered Feedback Digest** In this example, customer requests are collected and digested every 15 minutes. An in-app notification alerts the user about the number of new requests. Every 6 hours, those requests are grouped and passed to a second workflow where an LLM (like OpenAI) categorizes the feedback into bugs, feature requests, and praise. A summary email is then sent with the categorized counts. This setup helps reduce noise while still keeping teams informed with meaningful, AI-powered insights. ```typescript const secondDigestWorkflow = workflow( "llm-request-summary-workflow", async ({ step, payload }) => { const { events } = await step.digest("digest-llm-summary", async () => { return { unit: "hours", amount: 6, }; }); await step.email("send-llm-summary", async () => { const allRequests = events?.map((event) => event.payload.requests); // categorized: { bugs: 2, features: 4, praise: 5 } const categorized = await categorizeUsingLLM(allRequests); const { bugs, features, praise } = categorized; return { subject: `🧠 LLM Feedback Digest - Last 6 Hours`, body: ` 🔧 Bugs reported: ${bugs}\n 🌟 Feature requests: ${features}\n 🙌 Praise received: ${praise}\n\n View full feedback log in your dashboard. `, }; }); } ); const firstDigestWorkflow = workflow( "customer-requests-digest-workflow", async ({ step, subscriber, payload }) => { const { events } = await step.digest("digest-recent-requests", async () => { return { unit: "minutes", amount: 15, }; }); await step.inApp("in-app-summary", async () => { return { subject: `📝 ${events.length} new requests received`, body: `You’ve received ${events.length} customer requests in the last 15 minutes.`, }; }); await step.custom("trigger-llm-categorize-workflow", async () => { return await secondDigestWorkflow.trigger({ to: subscriber?.subscriberId as string, payload: { requests: events.map((event) => event.payload), }, }); }); } ); ``` Changing the step content after triggering the workflow with digest step will not affect the existing digested events. ## Next Steps * [Learn more about the Digest Engine](/framework/typescript/steps/digest) ## Frequently Asked Questions ### If digest step fails, will the workflow continue to the next step? No, workflow execution will stop immediately if the digest step fails due to an error. file: ./content/docs/framework/email-channel.mdx # Email Learn how to configure the Email channel The Email Channel is a critical component for delivering notifications reliably. Whether it's a password reset, an onboarding email, or an alert about account activity, email remains a trusted medium for reaching users. Novu simplifies this process, allowing you to focus on implementation rather than infrastructure. Learn more about the [Email Channel](/platform/integrations/email). ```tsx await step.email('email', async () => { return { subject: 'You received a message', body: 'A new post has been created', }; }); ``` file: ./content/docs/framework/endpoint.mdx # Bridge Endpoint undefined import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; Novu Framework requires a **single** `HTTP` endpoint (`/api/novu` or similar) to be exposed by your application. This endpoint is used to receive events from our **Worker Engine**. You can view the Bridge Endpoint as a webhook endpoint that Novu will call when it needs to retrieve contextual information for a given subscriber and notification. Using the `npx novu init` command creates a Bridge application for you with a Bridge Endpoint ready to go. ## The `serve` function We offer framework specific wrappers in form of an exported `serve` function that abstracts away: * Parsing the incoming request for `GET`, `POST`, `PUT` and `OPTIONS` requests * HMAC header authentication * Framework specific response and error handling Currently, we offer `serve` functions for the following frameworks: * [Next.js](/framework/quickstart/nextjs) * [Express.js](/framework/quickstart/express) * [Nuxt](/framework/quickstart/nuxt) * [h3](/framework/quickstart/h3) * [Remix](/framework/quickstart/remix) * [Sveltekit](/framework/quickstart/svelte) ## Writing a custom `serve` function If we currently don't support your framework, you can write a custom `serve` function like the following example: ```ts import { type Request, type Response } from 'express'; import { NovuRequestHandler, ServeHandlerOptions } from '@novu/framework'; export const serve = (options: ServeHandlerOptions) => { const requestHandler = new NovuRequestHandler({ frameworkName: 'express', ...options, handler: (incomingRequest: Request, response: Response) => ({ method: () => incomingRequest.method, headers: (key) => { const header = incomingRequest.headers[key]; return Array.isArray(header) ? header[0] : header; }, queryString: (key) => { const qs = incomingRequest.query[key]; return Array.isArray(qs) ? qs[0] : qs; }, body: () => incomingRequest.body, url: () => new URL(incomingRequest.url, `https://${incomingRequest.headers.get('host') || ''}`), transformResponse: ({ body, headers, status }) => { Object.entries(headers).forEach(([headerName, headerValue]) => { response.setHeader(headerName, headerValue); }); return response.status(status).send(body); }, }), }); return requestHandler.createHandler(); }; ``` The tunnel url is the url that is generated by the Studio when you run the `npx novu@latest dev` command. It is used to test your notifications by triggering them from the Studio UI. For local development and testing, you can use the tunnel url as bridge url. For production, you should use deployed application url with bridge endpoint as the bridge url. Yes, the bridge url must be publicly accessible. We recommend having https enabled for the bridge url. Yes, you can use any path you want. However, you need to make sure that the endpoint is used in the bridge url. Bridge url is made of the base url and the endpoint path. If your path is /custom-path/novu and deployed application url is [https://my-app.com](https://my-app.com), then the bridge url will be [https://my-app.com/custom-path/novu](https://my-app.com/custom-path/novu). file: ./content/docs/framework/in-app-channel.mdx # In-App Learn how to configure the In-App channel Novu extends beyond traditional notification channels like email, SMS, and push by providing a robust framework for in-app notifications. With Novu, you can build reliable, stateful systems that integrate seamlessly into your applications. Learn more about the [In-App Channel](/platform/inbox/overview). ```tsx await step.inApp('inbox', async () => { return { subject: 'Welcome to Acme!', body: 'We are excited to have you on board.', avatar: 'https://acme.com/avatar.png', redirect: { url: 'https://acme.com/welcome', target: '_blank', }, primaryAction: { label: 'Get Started', redirect: { url: 'https://acme.com/get-started', target: '_self', }, }, secondaryAction: { label: 'Learn More', redirect: { url: 'https://acme.com/learn-more', target: '_self', }, }, data: { customData: 'customValue', text: payload.text, }, }; }); ``` file: ./content/docs/framework/introduction.mdx # Introduction Discover how the Novu Framework empowers you to build, customize, and manage advanced notification workflows with a mix of code and no-code capabilities. The Novu framework allows you to build and manage advanced notification workflows with code, and expose no-code controls for non-technical users to modify. Workflows are the building blocks of your customer notification experience, they will define the what, when, how and where of your notifications. ## Building blocks Each Novu workflow is composed of 3 main components: * **Trigger** - The event that will start the workflow. * **Channel steps** - The delivery method of the notification with the content. * **Action steps** - Actions that will happen before and after a given channel step is executed. Let's take a look at a simple example of a workflow that sends an email after one day: ```tsx import { workflow } from '@novu/framework'; workflow('sample-workflow', async (step) => { await step.delay('delay', async () => { return { unit: 'days', amount: 1, }; }); await step.email('email-step', async () => { return { subject: 'Welcome to Novu', body: 'Hello, welcome to Novu!', }; }); }); ``` ### Trigger The trigger is the event that will start the workflow. In our current example the `sample-workflow` identifier will be used as our trigger id. Workflow identifiers should be unique to your application and should be descriptive of the workflow's purpose. ### Channel steps Channel Steps are the delivery methods of the notification. In our example, we have an email Channel Step that will send an email with the subject `Welcome to Novu` and the body `Hello, welcome to Novu!`. Novu's durable workflow execution engine will select the relevant delivery provider configured for this channel and send the notification with the specified content. Novu supports a variety of common notification channels out-of-the-box, including **email**, **SMS**, **push**, **inbox**, and **chat**. To read more about the full list of parameters, check out the [full SDK reference](/framework/typescript/overview). ### Action steps Action Steps are purpose built functions that help you manage the flow of your workflow. In our example, we have a delay Action Step that will pause the workflow for one day before sending the email. You can also use Action Steps to perform other tasks such as fetching data from an external API, updating a database, or sending a notification to another channel. Novu supported the following Action Steps: **delay**, **custom** and **digest**. ## Create a workflow Here's a bare-bones example of a workflow to send a notification in response to a trigger: ```tsx import { workflow } from '@novu/framework'; const myWorkflow = workflow( 'new-signup', async ({ step, payload }) => { await step.email('send-email', async () => { return { subject: `Welcome to Acme, ${payload.name}`, body: 'We look forward to helping you achieve mission.', }; }); }, { payloadSchema: z.object({ name: z.string() }) } ); ``` We'll build on top of this basic code block in the following examples below. ## Just-in-time data fetching You can add any custom logic needed into your steps. For example, you might want to fetch more information about your new user from a database during the workflow execution. You can achieve this with the following changes: ```tsx import { workflow } from '@novu/framework'; const myWorkflow = workflow( 'new-signup', async ({ step, payload }) => { await step.email('send-email', async () => { const user = await db.getUser(payload.userId); return { subject: `Welcome to Acme ${user.productTier} tier, ${user.name}`, body: 'We look forward to helping you achieve mission.', }; }); }, { payloadSchema: z.object({ userId: z.string() }) } ); ``` We call this **just-in-time** notification data fetching. It allows you pull in data from the relevant sources during the workflow execution, removing the need to store all of your subscriber data in Novu. ## Multi-step workflow What if you want to send another update to the same user in one week? But you don't want to send the follow-up if the user opted out. We can add more steps to the workflow to achieve this. ```tsx import { workflow } from '@novu/framework'; const myWorkflow = workflow( 'new-signup', async ({ step, payload }) => { await step.email('send-email', async () => { const user = await db.getUser(payload.userId); return { subject: `Welcome to Acme ${user.productTier} tier, ${user.name}`, body: 'We look forward to helping you achieve mission.', }; }); await step.delay('onboarding-follow-up', async () => ({ amount: 1, unit: 'weeks', })); await step.inApp( 'onboarding-follow-up', async (controls) => { const user = await db.getUser(payload.userId); return { body: `Hey ${user.name}! How do you like the product? Let us know here if you have any questions.`, }; }, { skip: () => !payload.shouldFollowUp, controlSchema: { type: 'object', properties: { feedbackUrl: { type: 'string', format: 'uri', default: 'https://acme.com/feedback', }, }, required: ['feedbackUrl'], additionalProperties: false, } as const, } ); }, { payloadSchema: { type: 'object', properties: { userId: { type: 'string' }, shouldFollowUp: { type: 'boolean', default: true }, }, required: ['userId', 'shouldFollowUp'], additionalProperties: false, } as const, } ); ``` With this simple workflow, we: * Sent a new signup email * Waited 1 week * Sent a follow-up notification in-app That's the flexibility that Novu Framework offers. Read the section on [Controls](/framework/controls) next to learn how to expose Novu's no-code editing capabilities to your peers. ## Payload schema Payload schema defines the payload passed during the `novu.trigger` method. This is useful for ensuring that the payload is correctly formatted and that the data is valid. ```tsx import { workflow } from '@novu/framework'; import { render } from 'react-email'; import { ReactEmailContent } from './ReactEmailContent'; workflow( 'comment-on-post', async ({ step, payload }) => { await step.email('send-email', async () => { return { subject: `You have a new comment from: ${payload.author_name}`, body: render(), }; }); }, { payloadSchema: { type: 'object', properties: { post_id: { type: 'number' }, author_name: { type: 'string' }, comment: { type: 'string', maxLength: 200 }, }, required: ['post_id', 'comment'], additionalProperties: false, } as const, } ); ``` ## Tags Tags are used to categorize the workflows. They also allow you to filter and group notifications for your [Inbox](/platform/inbox/react/tabs) tabs. ```tsx import { workflow } from '@novu/framework'; workflow( 'acme-login-alert', async ({ step, payload }) => { await step.inApp('inbox', async () => { return { subject: 'New Login Detected', body: "We noticed a login from a new device or location. If this wasn't you, change your password immediately.", }; }); }, { tags: ['security'], } ); ``` file: ./content/docs/framework/overview.mdx # Overview Learn how to extend Novu's capabilities by building custom notification workflows with code using the Novu Framework. import { ExpressjsIcon } from '@/components/icons/expressjs'; import { H3Icon } from '@/components/icons/h3'; import { LambdaIcon } from '@/components/icons/lambda'; import { NestjsIcon } from '@/components/icons/nestjs'; import { NextjsIcon } from '@/components/icons/nextjs'; import { NuxtIcon } from '@/components/icons/nuxt'; import { RemixIcon } from '@/components/icons/remix'; import { SvelteIcon } from '@/components/icons/svelte'; import { Card, Cards } from 'fumadocs-ui/components/card'; Novu Framework enables you to run a part of the core Novu workflow engine from within your network boundary. This also opens up a powerful new capability: **you can create Novu workflows entirely in code**. This is important because: * You can inject custom code that does nearly anything you can imagine as part of a Novu workflow * Your code-based workflow lives alongside your application code in source control * You can hydrate notification content using local data-sources, reducing what you need to sync outside of your IT boundary ### What it is and how it works Novu Framework is an application and SDK that you run locally, and communicates natively with the Novu Cloud Worker Engine via the Novu API. Novu Framework requires a single HTTP webhook-like endpoint (/api/novu or similar) to be exposed by your application. This endpoint is called a **Bridge Endpoint** and is used to receive events from our Worker Engine through an encrypted client-initiated tunnel. When enabled, Novu Cloud will call the Bridge Endpoint when it needs to retrieve contextual information for a given subscriber and notification. Use the `npx novu init` command to create a sample Bridge application with a built-in Bridge Endpoint. ## Quickstart To get started with Novu Framework, pick your preferred technology from the list below: } href="/framework/quickstart/nextjs" /> } href="/framework/quickstart/express" /> } href="/framework/quickstart/remix" /> } href="/framework/quickstart/nestjs" /> } href="/framework/quickstart/svelte" /> } href="/framework/quickstart/nuxt" /> } href="/framework/quickstart/h3" /> } href="/framework/quickstart/lambda" /> file: ./content/docs/framework/payload.mdx # Payload undefined Workflow payload is the data passed during the `novu.trigger` method. This is useful for ensuring that the payload is correctly formatted and that the data is valid. ## Payload Schema Payload schema is defining the payload passed during the `novu.trigger` method. This is useful for ensuring that the payload is correctly formatted and that the data is valid. ```tsx import { render } from '@react-email/components'; import { ReactEmailContent } from './ReactEmailContent'; workflow( 'comment-on-post', async ({ step, payload }) => { await step.email('send-email', async () => { return { subject: `You have a new comment from: ${payload.author_name}.`, body: render(), }; }); }, { payloadSchema: { // Always `object` type: 'object', // Specify the properties to validate. Supports deep nesting. properties: { post_id: { type: 'number' }, author_name: { type: 'string' }, comment: { type: 'string', maxLength: 200 }, }, // Specify the array of which properties are required. required: ['post_id', 'comment'], // Used to enforce full type strictness, with no rogue properties. additionalProperties: false, // The `as const` is important to let Typescript know that this // type won't change, enabling strong typing on `inputs` via type // inference of the provided JSON Schema. } as const, } ); ``` ## Passing Payload Here is an example of the validated payload during trigger: ```typescript novu.trigger('comment-on-post', { to: 'subscriber_id', payload: { post_id: 1234, author_name: 'John Doe', comment: 'Looks good!', }, }); ``` file: ./content/docs/framework/push-channel.mdx # Push Learn how to configure the Push channel Push notifications are a powerful way to deliver real-time updates, reminders, and personalized messages to your users across mobile and web platforms. Whether it's a promotional alert, a system notification, or a critical update, push notifications are key to enhancing engagement and retention. Learn more about the [Push Channel](/platform/integrations/push) ```tsx await step.push('push', async () => { return { subject: 'You received a message', body: 'A new post has been created', }; }); ``` file: ./content/docs/framework/skip.mdx # Skip Function Skip any step of the workflow based on a condition The `skip` action is used to skip the next step in the workflow. `skip` function is available for all the steps in the workflow. ## Common usecases * Skip the step if the user has already seen the notification * Skip the step if the user has not completed the previous step ## Example In this example, we will send an in-app notification for task reminder to the user and then send an email reminder 6 hours later if the user has not read the in-app notification. ```tsx workflow('skip-email-if-in-app-notification-seen', async ({ payload }) => { const inAppNotification = await step.inApp( 'send-in-app-notification', async () => { return { subject: 'Task reminder!', body: 'Task is not yet complete. Please complete the task.', }; }, ); // delay for 6 hrs await step.delay("delay-step-before-email", async () => { return { unit: 'hours', amount: 6, }; }); // send email notification after 6 hrs if the in-app notification has not been read await step.email( 'send-email', () => { return { subject: `Task reminder!`, body: 'Task is not yet complete. Please complete the task.', }; }, { // skip the email step if the in-app notification has been read skip: () => inAppNotification.read === true, } ); }); ``` file: ./content/docs/framework/sms-channel.mdx # SMS Learn how to configure the SMS channel Novu makes SMS notifications simple, scalable, and reliable, enabling seamless integration with your communication stack. Whether you're sending OTPs, updates, or transactional messages, Novu ensures your SMS notifications are delivered efficiently and effectively. Learn more about the [SMS Channel](/platform/integrations/sms). ```tsx await step.sms('sms', async () => { return { body: 'A new post has been created', }; }); ``` file: ./content/docs/framework/studio.mdx # Studio Learn how to use the Local Studio companion app for Novu Framework SDK import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; The Local Studio is a companion app to the Novu Framework SDK. Its goal is to provide a local environment that lives near your code. To launch the Local Studio locally you can run the following command in your terminal: Local Studio currently works only in the **Chrome** browser ```bash npx novu@latest dev ``` Learn how to use the `novu` CLI package and the available [CLI flags](#novu-cli-flags) to use for customization The Dev Studio will be started by default on port 2022, and accessible via: [http://localhost:2022](http://localhost:2022) After successfully connecting the Studio to your local [Bridge Endpoint](/framework/endpoint), you will be able to preview in real time any workflows and content defined in your code. This is ideal for quick prototyping, debugging, styling, and adjusting your workflows before syncing them to Novu Cloud. ## Control and Payload forms You can quickly modify the Step Controls and workflow Payload to preview your workflow's different states. This is helpful to quickly debug how the email will behave in case of a missing control, or iterate more complex content structures. ## Syncing State Syncing state to your Production or Development environment in Novu, is recommended to do via your CI pipeline. However, a sync can be made using the Local Studio for quick experimentation. Click on the Sync button at the top right corner of the navigation bar. This will open the Sync State modal. ## Tunnel URL By default the Novu CLI will automatically create a tunnel URL connected to your local computer. This tunnel will proxy any workflow engine requests on our cloud to your local machine. ## Connect Studio to your local server By default, the Studio will connect to the Novu [Bridge Endpoint](/framework/endpoint) running on your local machine at `http://localhost:4000/api/novu` if your server is running on a different port or the workflows are served from a different endpoint path you can use the following optional parameters to connect: ```bash npx novu@latest dev --port --route ``` * **YOUR\_SERVER\_PORT** - This accepts the port number where your server is running. Defaults to 4000. * **YOUR\_NOVU\_ROUTE\_PATH** - This is the mounted path of the framework `serve` function. Defaults to `/api/novu`. ## Novu CLI flags The Novu CLI command `npx novu@latest dev` supports a number of flags: | Flag | Long form usage example | Description | Default value | | ---- | ----------------------- | --------------------------- | ------------------------------------------------------ | | -p | --port `` | Bridge application port | 4000 | | -r | --route `` | Bridge application route | /api/novu | | -o | --origin `` | Bridge application origin | [http://localhost](http://localhost) | | -d | --dashboard-url `` | Novu Cloud dashboard URL | [https://dashboard.novu.co](https://dashboard.novu.co) | | -sp | --studio-port `` | Local Studio server port | 2022 | | -t | --tunnel `` | Self hosted tunnel url | null | | -H | --headless | Run bridge in headless mode | false | Example: If bridge application is running on port `3002` and Novu account is in `EU` region. ```bash npx novu@latest dev --port 3002 --dashboard-url https://eu.dashboard.novu.co ``` ## FAQ It is possible to run the Studio without generating the default tunnel by passing the `--tunnel` flag with the URL of your application. ```bash npx novu@latest dev -t http://custom-tunnel-url.ngrok.app ``` While the preview will work, you won't be able to test your notifications by the triggering them from the Studio UI. file: ./content/docs/framework/tags.mdx # Tags Learn how to implement and manage notification tags programmatically using the Novu Framework SDK. **Tags** act like labels or categories that help you organize and manage notifications in your app. By grouping notifications under specific tags, you can better control how they're filtered, displayed, or managed by both your app and your users. [Learn more about why and how to use tags](/platform/workflow/tags) ## How to Add Tags to Notifications Adding tags to a notification is simple and can be done directly in the workflow configuration. Here's an example: ```tsx import { workflow } from '@novu/framework'; workflow( 'acme-login-alert', async ({ step, payload }) => { await step.inApp('inbox', async () => { return { subject: 'New Login Detected', body: "We noticed a login from a new device or location. If this wasn't you, change your password immediately.", }; }); }, { tags: ['security'], } ); workflow( 'acme-password-change', async ({ step, payload }) => { await step.inApp('inbox', async () => { return { subject: 'Password Changed', body: "Your password was successfully updated. If you didn't request this, contact support right away.", }; }); }, { tags: ['security'], } ); ``` Let's break it down: In the above workflows, whenever someone logs in from a new device, or changes their password, the notification is tagged as security. **Benefits:** * **Filtered Views**: Users can quickly locate security-related notifications in their inbox. * **Custom Preferences**: Users who only want security alerts can opt in or out of that tag category. file: ./content/docs/guides/inngest.mdx # Inngest This guide walks you through integrating Inngest with Novu notifications import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; import { Callout } from 'fumadocs-ui/components/callout'; import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; import { Step, Steps } from 'fumadocs-ui/components/steps'; ## Why Use Novu with Inngest? By combining these tools, you can: * **Trigger Smart Notifications**: Send notifications across multiple channels (email, SMS, push, in-app) based on complex event triggers * **Build Reliable Workflows**: Create robust notification pipelines with Inngest's state management and retry capabilities * **Scale Effortlessly**: Handle high-volume notification workloads with Inngest's queue management * **Maintain Observability**: Monitor your notification workflows in real-time through Inngest's dashboard ## What You'll Learn This guide teaches you how to: * Configure Novu and Inngest in your project * Manage notification subscribers via Novu's API * Create event-driven notification workflows with Inngest * Use dynamic data to personalize your notifications New to Inngest? We recommend starting with their [quickstart guide](https://www.inngest.com/docs/getting-started) before continuing. 1. Install Novu's SDK in our project ```json npm i @novu/api ``` 2. Create a new task You can also add this trigger to an existing one or multiple tasks Import Novu's SDK and provide the credentials to initialize the Novu instance. ```typescript import { Novu } from "@novu/api"; const novu = new Novu({ secretKey: 'ApiKey ' + process.env['NOVU_SECRET_KEY'] }); ``` For the sake of this guide, we will create a new dedicated functions for 2 common Novu actions: * Trigger Notification Workflow * Add New Subscriber ## Trigger Notification Workflow from an Inngest function You can trigger Novu notification workflows directly from your Ingest function. This allows you to send notifications based on events or completions within your background jobs. To trigger a Novu workflow, you'll use the Novu Node.js SDK within your Inngest's `createFunction` method. ### Core Requirements When calling Novu's `trigger` method, you must provide the following information: 1. `workflowId` **(string):** This identifies the specific notification workflow you want to execute. * **Note:** Ensure this workflow ID corresponds to an existing, active workflow in your Novu dashboard. 2. `to` **(object):** This object specifies the recipient of the notification. At a minimum, it requires: * `subscriberId` **(string):** A unique identifier for the notification recipient within your system (e.g., a user ID). **Note:** If a subscriber with this `subscriberId` doesn't already exist in Novu, Novu will automatically create one when the workflow is triggered. You can also pass other identifiers like `email`, `phone`, etc., within the `to` object, depending on your workflow steps. ### Basic Trigger Example Here's a simple Inngest function that triggers a Novu workflow when the function is being invoked. It assumes the function invoke receives a `userId` and `email` in its payload. ```tsx import { inngest } from "./client"; import { Novu } from "@novu/api"; const novu = new Novu({ secretKey: 'ApiKey ' + process.env['NOVU_SECRET_KEY'] }); export const sendNotification = inngest.createFunction( { id: "send-notification" }, { event: "test/hello.world" }, async ({ event, step }) => { await step.run("send-welcome-email", async () => { await novu.trigger({ workflowId: "welcome-email", to: { subscriberId: event.data.userId, email: event.data.email, }, payload: {}, }); }); return { message: `Welcome email sent to ${event.data.email}!` }; }, ); ``` ### Adding Dynamic Content with `payload` Often, you'll want your notifications to include dynamic data related to the event that triggered them (e.g., a user's name, an order number, job details). This is done using the `payload` property in the `novu.trigger` call. The `payload` is an object containing key-value pairs that you can reference within your Novu notification templates using Handlebars syntax (e.g., `{{ name }}`, `{{ order.id }}`). **Use Case Example:** Imagine you want to send an email confirming a background job's completion: * **Subject:** `Function {{ functionName }} Completed Successfully!` * **Body:** `Hi {{ userName }}, your function '{{ functionName }}' finished processing.` To achieve this, you would pass the `userName` and `functionName` in the `payload` object when triggering the workflow. ### Trigger Example with Payload Let's modify the previous function to accept more data (like a user's `name`) in its own payload and pass relevant parts to the Novu `trigger` payload. ```typescript import { inngest } from "./client"; import { Novu } from "@novu/api"; const novu = new Novu({ secretKey: 'ApiKey ' + process.env['NOVU_SECRET_KEY'] }); export const sendNotification = inngest.createFunction( { id: "send-notification" }, { event: "test/hello.world" }, async ({ event, step }) => { await step.run("send-welcome-email", async () => { try { // Construct the payload specifically for Novu const novuPayload = { userName: event.data.userName, // Map 'name' from task payload to 'userName' for the template functionName: `Data Processing Function #${event.id}`, // Can include static text or transform data }; await novu.trigger({ workflowId: "inbox-test", // Use the appropriate workflow ID to: { subscriberId: event.data.userId, email: event.data.email, }, payload: { ...novuPayload, }, }); console.log("Novu workflow triggered successfully with payload:", novuPayload); return novuPayload; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'Unknown error triggering Novu'; console.error("Failed to trigger Novu workflow with payload:", errorMessage, error); throw new Error(`Novu trigger failed: ${errorMessage}`); } }); }, ); ``` ## Add a New Subscriber to Novu Before sending notifications, Novu needs to know *who* to notify. That's where subscribers come in. A **subscriber** in Novu represents a user (or entity) that can receive notifications through one or more channels (in-app, email, SMS, etc.). You can create a new subscriber programmatically from a Inngest's task using Novu's SDK. *** ### When Should You Create Subscribers? Create or update a subscriber in Novu when: * A new user signs up or is added to your system * A user becomes eligible to receive notifications * You want to ensure subscriber metadata (name, phone, etc.) is up-to-date If you trigger a workflow with a `subscriberId` that doesn't exist, Novu will auto-create the subscriber. However, doing it explicitly ensures full control over subscriber data. ### Create a Subscriber in a Task Below is a task that creates a Novu subscriber using a Inngest task. It also supports optional fields like `phone` and `avatar`. ```typescript import { inngest } from "./client"; import { Novu } from "@novu/api"; const novu = new Novu({ secretKey: 'ApiKey ' + process.env['NOVU_SECRET_KEY'] }); export const createSubscriber = inngest.createFunction( { id: "create-subscriber" }, { event: "app/account.created" }, async ({ event, step }) => { await step.run("create-subscriber", async () => { try { // Construct the payload specifically for Novu const subscriberPayload = Object.fromEntries( Object.entries({ subscriberId: event.data.userId, email: event.data.email, firstName: event.data.firstName, lastName: event.data.lastName, phone: event.data.phone, avatar: event.data.avatar, locale: event.data.locale, data: event.data.userName ? { userName: event.data.userName } : undefined }).filter(([, value]) => value !== undefined) ) as { subscriberId: string; email?: string; firstName?: string; lastName?: string; phone?: string; avatar?: string; locale?: string; data?: { userName: string }; }; await novu.subscribers.create(subscriberPayload); console.log("Novu subscriber created successfully with payload:", subscriberPayload); return subscriberPayload; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'Unknown error creating Novu subscriber'; console.error("Failed to create Novu subscriber with payload:", errorMessage, error); throw new Error(`Novu subscriber creation failed: ${errorMessage}`); } }); }, ); ``` *** ## Implementation Example This example assumes: 1. You have an event named `video/uploaded` that is being sent to Inngest when a user uploads a video. 2. This event's `data` payload includes at least `videoUrl`, `videoId`, `userId`, `email`, and `userName`. 3. You have external services/functions mocked or implemented: `deepgram.transcribe`, `llm.createCompletion`, `createSummaryPrompt`, and `db.videoSummaries.upsert`. 4. You have a Novu account, API Key (`NOVU_SECRET_KEY` environment variable), and a Novu workflow created with the ID `video-processed-notification`. This workflow should expect variables like `userName`, `videoId`, and potentially `summarySnippet` in its payload. ```typescript import { Inngest } from "inngest"; import { Novu } from "@novu/api"; // --- Mock/Placeholder External Services --- // Replace these with your actual implementations const deepgram = { transcribe: async (url: string): Promise => { console.log(`Mock Transcribing: ${url}`); await new Promise(resolve => setTimeout(resolve, 1500)); // Simulate network delay return `This is a mock transcript for the video at ${url}. It contains several sentences explaining the content.`; } }; const llm = { createCompletion: async (params: { model: string; prompt: string }): Promise => { console.log(`Mock Summarizing with ${params.model}`); await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate LLM processing time const transcriptSnippet = params.prompt.substring(params.prompt.indexOf(":") + 1, 70); return `Mock summary of the transcript starting with: ${transcriptSnippet}...`; } }; const createSummaryPrompt = (transcript: string): string => { return `Please summarize the following transcript:\n\n${transcript}`; }; const db = { videoSummaries: { upsert: async (data: { videoId: string; transcript: string; summary: string }): Promise => { console.log(`Mock DB Upsert for videoId: ${data.videoId}`); // In a real scenario, save data.transcript and data.summary linked to data.videoId await new Promise(resolve => setTimeout(resolve, 500)); // Simulate DB write delay } } }; // --- End Mock Services --- // --- Inngest Client Setup --- // Assumes you have your Inngest client initialized elsewhere (e.g., './inngest/client') // For this example, we'll initialize it here. Replace with your actual client setup. export const inngest = new Inngest({ id: "video-processing-app" }); // --- End Inngest Client Setup --- // --- Novu Client Setup --- const novu = new Novu({ secretKey: 'ApiKey ' + process.env['NOVU_SECRET_KEY'] }); // --- End Novu Client Setup --- // --- Combined Inngest Function --- export const processVideoAndNotify = inngest.createFunction( { id: "process-video-and-notify", name: "Process Uploaded Video and Notify User", // Apply concurrency limits per user to avoid overwhelming resources concurrency: { limit: 3, // Allow up to 3 concurrent processing jobs per user key: "event.data.userId" } }, { event: "video/uploaded" }, // Triggered when a video upload event occurs async ({ event, step }) => { const { videoUrl, videoId, userId, email, userName } = event.data; // Validate required data from the event if (!videoUrl || !videoId || !userId || !email || !userName) { throw new Error(`Missing required data in 'video/uploaded' event payload for event ID: ${event.id}`); } // --- Step 1: Transcribe Video --- // step.run ensures this block retries on failure and runs exactly once on success. const transcript = await step.run('transcribe-video', async () => { console.log(`Starting transcription for videoId: ${videoId}, url: ${videoUrl}`); try { const result = await deepgram.transcribe(videoUrl); console.log(`Transcription successful for videoId: ${videoId}`); return result; } catch (error) { console.error(`Transcription failed for videoId: ${videoId}`, error); throw error; // Re-throw to enable Inngest retries } }); // --- Step 2: Summarize Transcript --- // The 'transcript' result from the previous step is automatically available here. const summary = await step.run('summarize-transcript', async () => { console.log(`Starting summarization for videoId: ${videoId}`); try { const result = await llm.createCompletion({ model: "gpt-4o", // Or your preferred model prompt: createSummaryPrompt(transcript), }); console.log(`Summarization successful for videoId: ${videoId}`); return result; } catch (error) { console.error(`Summarization failed for videoId: ${videoId}`, error); throw error; // Re-throw for retries } }); // --- Step 3: Write Results to Database --- await step.run('write-summary-to-db', async () => { console.log(`Writing transcript and summary to DB for videoId: ${videoId}`); try { await db.videoSummaries.upsert({ videoId: videoId, transcript, summary, }); console.log(`DB write successful for videoId: ${videoId}`); } catch (error) { console.error(`DB write failed for videoId: ${videoId}`, error); throw error; // Re-throw for retries } }); // --- Step 4: Send Completion Notification via Novu --- // This step runs only after the previous steps have succeeded. await step.run("send-completion-notification", async () => { if (!novuApiKey) { console.warn(`Skipping notification for videoId ${videoId} as NOVU_SECRET_KEY is not set.`); return { skipped: true, reason: "NOVU_SECRET_KEY not set" }; } console.log(`Attempting to send Novu notification for videoId: ${videoId} to user: ${userId}`); // Construct a relevant payload for your Novu template const novuPayload = { userName: userName, videoId: videoId, summarySnippet: summary.substring(0, 100) + (summary.length > 100 ? "..." : ""), // Send a preview // Add any other data your Novu template needs, e.g., a link to view the full video/summary // videoUrl: `https://yourapp.com/videos/${videoId}` }; try { const triggerResult = await novu.trigger({ // Ensure this workflow ID exists in your Novu dashboard workflowId: "video-processed-notification", to: { subscriberId: userId, // Crucial for identifying the user in Novu email: email, // Can be used by Novu email channel // Add other channels like phone if needed and available }, payload: novuPayload, }); console.log(`Novu workflow 'video-processed-notification' triggered successfully for videoId: ${videoId}`, triggerResult); return { success: true, novuPayload: novuPayload, triggerResult }; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'Unknown error triggering Novu'; console.error(`Failed to trigger Novu workflow for videoId: ${videoId}: ${errorMessage}`, error); // Decide if notification failure should cause the step to fail and retry // For notifications, often you might want to log the error but not fail the whole function // throw new Error(`Novu trigger failed: ${errorMessage}`); // Uncomment to make it retry return { success: false, error: errorMessage }; // Log and continue } }); // --- Function Completion --- // Optional: return a final status or summary return { message: `Video processing and notification steps completed for videoId: ${videoId}`, videoId: videoId, userId: userId, summaryLength: summary.length, }; } ); // --- How to Trigger (Example) --- /* Somewhere else in your application (e.g., after a file upload completes): import { inngest } from "./path/to/inngest/client"; // Your initialized Inngest client async function handleVideoUpload(uploadResult: { url: string, id: string, user: { id: string, email: string, name: string } }) { await inngest.send({ name: "video/uploaded", // The event name our function listens to data: { videoUrl: uploadResult.url, videoId: uploadResult.id, userId: uploadResult.user.id, email: uploadResult.user.email, userName: uploadResult.user.name, }, user: { // Optionally pass user context if needed for event metadata/tracking id: uploadResult.user.id, email: uploadResult.user.email, } }); console.log(`Sent 'video/uploaded' event for videoId: ${uploadResult.id}`); } // Example usage: handleVideoUpload({ url: "https://example.com/videos/123.mp4", id: "vid_12345abc", user: { id: "user_67890def", email: "test@example.com", name: "Alice" } }); */ ``` *** ### Template Usage Example Once you've added custom fields (like `firstName`, `userName`, or `phone`) to a subscriber, you can use them in Novu templates using Handlebars: * Hi `{{subsciber.firstName}}`, welcome! * We'll reach you at `{{subsciber.phone}}` if needed. * Your account is set up under `{{subscriber.data.userName}}`. *** ### Best Practices * Store your Novu `subscriberId` as part of your user model — so you always have a reference. * Create/update subscribers proactively when user data changes (e.g., phone number updates). * Use `data` to store additional info like roles, tags for more personalized notifications. file: ./content/docs/guides/overview.mdx # Overview undefined import { Card, Cards } from 'fumadocs-ui/components/card'; import ClerkIcon from '@/components/icons/home-page/clerk'; import { StripeIcon } from '@/components/icons/stripe'; import { SegmentIcon } from '@/components/icons/segment'; ## Integration Guides Novu provides various ways to integrate with external services to trigger notification workflows. Here are the main integration approaches: ### Webhook Integration Guides Webhooks enable real-time event-driven communication between applications, making integrations more efficient and responsive. In Novu, webhooks trigger notification workflows whenever specific events occur in external applications. This ensures notifications are delivered exactly when they're needed, keeping users informed without delay. For example, when a user signs up via Clerk or completes a payment through Stripe, a webhook delivers the event payload to Novu, which then processes it and triggers the appropriate workflow. This allows for real-time notifications—whether it's a welcome email, payment confirmation SMS, or in-app alert—without the need for constant polling, ensuring efficiency and a seamless user experience. } href="/guides/webhooks/clerk">

Use Clerks webhooks events to trigger authentication related notifications workflows.

} href="/guides/webhooks/stripe">

Use Stripe webhooks events to trigger payment related notifications workflows.

### Analytics & Data Platform Integrations Some integrations use different mechanisms than webhooks to send data to Novu. For example, analytics and data platforms often use custom destinations or functions to forward events. } href="/guides/webhooks/segment">

Use Segment's Destination Functions to forward user events and traits to trigger notification workflows.

### Workflow Automation Platform Integrations Workflow automation platforms help orchestrate complex business processes and event-driven workflows. Integrating Novu with these platforms allows you to trigger notifications as part of your automated workflows.

Use Inngest's event-driven workflows to trigger notifications at specific steps in your automation pipeline.

Leverage Trigger.dev's developer-friendly workflow engine to send notifications based on scheduled or event-driven triggers.

file: ./content/docs/guides/triggerdotdev.mdx # Trigger.dev Learn how to integrate Novu with Trigger.dev to send notifications from background jobs. This guide covers setting up both services, managing subscribers, triggering notifications, and includes practical examples for AI content generation and video processing workflows. import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; import { Card, Cards } from 'fumadocs-ui/components/card'; import { NextjsIcon } from '@/components/icons/nextjs'; import { ExpressjsIcon } from '@/components/icons/expressjs'; import { Callout } from 'fumadocs-ui/components/callout'; import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; import { File, Folder, Files } from 'fumadocs-ui/components/files'; This guide walks you through integrating [Novu](https://novu.co/), the open-source notification infrastructure, with [Trigger.dev](https://trigger.dev/), a powerful open-source background jobs framework for TypeScript. By combining Novu's robust notification workflows with Trigger.Dev's event-driven background job system allows you to send notifications across multiple channels (in-app, email, SMS, push, etc.) in response to events or background tasks — all within a seamless developer experience. Whether you're processing payments, handling user onboarding, or running scheduled tasks, Trigger.dev lets you define workflows with fine-grained control and real-time observability. Novu plugs into those workflows to manage notification content and delivery, ensuring your service / product-to-user communication has a standard. ## What You'll Learn In this guide, you'll learn how to: * Set up both Novu and Trigger.dev in your project * Create and update subscribers via Novu's API * Trigger Novu notification workflows from a Trigger.dev job * Pass dynamic payload data to power your notification templates If you haven't used or are unfamiliar with Trigger.dev, we recommend following [the quickstart guide in their docs](https://trigger.dev/docs/quick-start). This guide assumes you are familiar with the fundamentals. ## Getting Started Install Novu's SDK in your project: ```bash npm i @novu/api ``` Import Novu's SDK and provide the credentials to initialize the Novu instance: ```typescript import { Novu } from "@novu/api"; const novu = new Novu({ secretKey: 'ApiKey ' + process.env['NOVU_SECRET_KEY'] }); ``` For the sake of this guide, we will create a new dedicated task for 2 common Novu actions: * Trigger Notification Workflow * Add New Subscriber ## Triggering Notifications ### Core Requirements When calling Novu's `trigger` method, you must provide the following information: **Workflow ID** `workflowId` **(string):** This identifies the specific notification workflow you want to execute. **Note:** Ensure this workflow ID corresponds to an existing, active workflow in your Novu dashboard. **Recipient Information** `to` **(object):** This object specifies the recipient of the notification. At a minimum, it requires: * `subscriberId` **(string):** A unique identifier for the notification recipient within your system (e.g., a user ID). **Note:** If a subscriber with this `subscriberId` doesn't already exist in Novu, Novu will automatically create one when the workflow is triggered. You can also pass other identifiers like `email`, `phone`, etc., within the `to` object, depending on your workflow steps. ### Basic Trigger Example Here's a simple Trigger.dev task that triggers a Novu workflow when the task runs. ```typescript import { task, retry } from "@trigger.dev/sdk/v3"; import { Novu } from "@novu/api"; // Initialize the Novu client with your API key // It's recommended to store your secret key securely, e.g., in environment variables. const novu = new Novu({ secretKey: 'ApiKey ' + process.env['NOVU_SECRET_KEY'] }); // Add '!' if you're sure it's defined, or handle potential undefined case export const notifyUserTask = task({ id: "notify-user-task", run: async ( payload: { // Payload received by the Trigger.dev task userId: string; email: string; }, { ctx } // Access context if needed, e.g., for logging ) => { const { userId, email } = payload; console.log(`Attempting to trigger Novu workflow for subscriber: ${userId}`); // Use retry logic for resilience against transient network issues or brief API unavailability await retry.onThrow( async () => { try { const triggerResult = await novu.trigger({ // 1. Specify the workflow to trigger workflowId: "your-workflow-id", // Replace with your actual workflow ID // 2. Specify the recipient to: { subscriberId: userId, email: email, // Include other identifiers if needed by your workflow }, // 3. Payload (optional) - see next section payload: {}, // Empty payload for this basic example }); console.log("Novu workflow triggered successfully:", triggerResult.data); return triggerResult.data; // Return data if needed elsewhere } catch (error: unknown) { // Log the specific error for better debugging const errorMessage = error instanceof Error ? error.message : 'Unknown error triggering Novu'; console.error("Failed to trigger Novu workflow:", errorMessage, error); // Throw a new error to ensure retry logic catches it throw new Error(`Novu trigger failed: ${errorMessage}`); } }, { maxAttempts: 3, // Configure retry attempts factor: 2, // Configure backoff strategy minTimeoutInMs: 1000, // Wait at least 1 second before first retry // Add more retry options as needed } ); }, }); ``` ## Working with Dynamic Content ### Using Payloads Often, you'll want your notifications to include dynamic data related to the event that triggered them. This is done using the `payload` property in the `novu.trigger` call. The `payload` is an object containing key-value pairs that you can reference within your Novu notification templates using Handlebars syntax (e.g., `{{ name }}`, `{{ order.id }}`). ### Example Use Case Imagine you want to send an email confirming a background job's completion: * **Subject:** `Job {{ jobName }} Completed Successfully!` * **Body:** `Hi {{ userName }}, your job '{{ jobName }}' finished processing.` To achieve this, you would pass the `userName` and `jobName` in the `payload` object when triggering the workflow. ### Advanced Trigger Example ```typescript import { task, retry } from "@trigger.dev/sdk/v3"; import { Novu } from "@novu/api"; const novu = new Novu({ secretKey: 'ApiKey ' + process.env['NOVU_SECRET_KEY'] }); export const notifyOnJobCompletion = task({ id: "notify-on-job-completion", run: async ( payload: { userId: string; email: string; name: string; jobId: string; } ) => { const { userId, email, name, jobId } = payload; await retry.onThrow( async () => { try { // Construct the payload specifically for Novu // This often uses data from the task's payload, but can include other computed values const novuPayload = { userName: name, // Map 'name' from task payload to 'userName' for the template jobName: `Data Processing Job #${jobId}`, // Can include static text or transform data }; await novu.trigger({ workflowId: "inbox-test", // Use the appropriate workflow ID to: { subscriberId: userId, email: email, }, payload: { // Pass the constructed payload to Novu ...novuPayload, }, }); console.log("Novu workflow triggered successfully with payload:", novuPayload); return novuPayload; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'Unknown error triggering Novu'; console.error("Failed to trigger Novu workflow with payload:", errorMessage, error); throw new Error(`Novu trigger failed: ${errorMessage}`); } }, { maxAttempts: 2 } // Retry the task up to 2 times ); }, }); ``` ### Key Points about Payloads This is the data your Trigger.dev task receives when it's invoked. It contains the context needed for the task to run. This is the data you *specifically send to Novu* via the `payload` property in the `novu.trigger()` call. It's used for populating variables in your notification templates. ## Implementation Examples ### AI Tasks This example demonstrates how to build a reliable AI content generation system that keeps users informed throughout the process using Novu notifications. ```typescript import { task, event, eventTrigger, retry } from "@trigger.dev/sdk/v3"; import { Novu } from "@novu/api"; import OpenAI from "openai"; // Initialize clients const novu = new Novu({ secretKey: 'ApiKey ' + process.env['NOVU_SECRET_KEY'] }); const openai = new OpenAI({ apiKey: process.env['OPENAI_API_KEY'], }); // Helper functions for prompt generation function generateTextPrompt(theme: string, description: string) { return [ { role: "system", content: "You are a creative writer crafting marketing content." }, { role: "user", content: `Write a short, engaging paragraph about ${theme}. Key points: ${description}` } ]; } function generateImagePrompt(theme: string, description: string) { return `Create a professional marketing image that represents ${theme}. Style: modern, clean. Details: ${description}`; } // Event trigger for content generation export const contentGenerationRequested = event({ id: "content-generation-requested", trigger: eventTrigger({ name: "content.generation.requested", }), }); // Notification task for successful completion export const notifyContentReady = task({ id: "notify-content-ready", run: async ({ userId, email, theme, contentId, contentPreview, imageUrl }: { userId: string; email: string; theme: string; contentId: string; contentPreview: string; imageUrl: string; }) => { await retry.onThrow( async () => { try { await novu.trigger({ workflowId: "ai-generation-completed", to: { subscriberId: userId, email: email, }, payload: { userName: email.split('@')[0], theme: theme, contentId: contentId, contentPreview: contentPreview, imageUrl: imageUrl, viewUrl: `https://yourapp.com/content/${contentId}` }, }); console.log("Content ready notification sent successfully"); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; console.error("Failed to send content ready notification:", errorMessage); throw new Error(`Final notification failed: ${errorMessage}`); } }, { maxAttempts: 3, factor: 2, minTimeoutInMs: 1000 } ); }, }); // Error notification task export const notifyError = task({ id: "notify-error", run: async ({ userId, email, theme, errorMessage }: { userId: string; email: string; theme: string; errorMessage: string; }) => { await retry.onThrow( async () => { try { await novu.trigger({ workflowId: "ai-generation-error", to: { subscriberId: userId, email: email, }, payload: { userName: email.split('@')[0], theme: theme, errorMessage: errorMessage, supportEmail: "support@yourapp.com" }, }); console.log("Error notification sent successfully"); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; console.error("Failed to send error notification:", errorMessage); // Not rethrowing here to prevent infinite loops when error notifications fail } }, { maxAttempts: 2 } ); }, }); // Main AI content generation task export const generateContent = task({ id: "generate-content", retry: { maxAttempts: 3, }, run: async ({ userId, email, theme, description }: { userId: string; email: string; theme: string; description: string; }) => { console.log(`Starting AI content generation for theme: ${theme}`); // Generate text content const textResult = await openai.chat.completions.create({ model: "gpt-4", messages: [{ role: "user", content: description }] }); if (!textResult.choices[0]?.message?.content) { throw new Error("No text content generated, retrying..."); } // Generate image content const imageResult = await openai.images.generate({ model: "dall-e-3", prompt: description, size: "1024x1024" }); if (!imageResult.data[0]?.url) { throw new Error("No image generated, retrying..."); } return { text: textResult.choices[0].message.content, imageUrl: imageResult.data[0].url, }; }, }); // Main job that orchestrates the entire workflow export const processContentRequest = task({ id: "process-content-request", events: [contentGenerationRequested], run: async (payload: { userId: string; email: string; theme: string; description: string; }) => { try { // Validate input if (!payload.theme || !payload.description) { throw new Error("Missing required parameters"); } // Generate content const result = await generateContent.run(payload); const contentId = payload.userId + '-' + Date.now(); // Send completion notification await notifyContentReady.run({ userId, email, theme, contentId, contentPreview: result.text.substring(0, 100) + "...", imageUrl: result.imageUrl }); return { success: true, contentId: contentId, ...result }; } catch (error) { // If anything fails, ensure user gets notified const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; console.error("Content generation failed:", errorMessage); await notifyError.run({ userId, email, theme, errorMessage: errorMessage }); return { success: false, error: errorMessage }; } }, }); ``` **The Key Components** 1. **The AI Generation Task** ```typescript export const generateContent = task({ id: "generate-content", retry: { maxAttempts: 3 }, run: async ({ userId, email, theme, description }) => { // Generate text using GPT-4 const textResult = await openai.chat.completions.create({ model: "gpt-4", messages: [{ role: "user", content: description }] }); // Generate image using DALL-E 3 const imageResult = await openai.images.generate({ model: "dall-e-3", prompt: description, size: "1024x1024" }); return { text: textResult.choices[0].message.content, imageUrl: imageResult.data[0].url, }; }, }); ``` This task handles the actual AI content generation: * It creates text content using GPT-4 * It creates an image using DALL-E 3 * It has built-in retry logic (will try up to 3 times if it fails) *** 2. **The Two Notification Tasks** **Completion Notification** ```typescript export const notifyContentReady = task({ id: "notify-content-ready", run: async ({ userId, email, theme, contentId, contentPreview, imageUrl }) => { await novu.trigger({ workflowId: "ai-generation-completed", to: { subscriberId: userId, email: email }, payload: { userName: email.split('@')[0], theme: theme, contentPreview: contentPreview, // Other details... }, }); }, }); ``` **Error Notification** ```typescript export const notifyError = task({ id: "notify-error", run: async ({ userId, email, theme, errorMessage }) => { await novu.trigger({ workflowId: "ai-generation-error", to: { subscriberId: userId, email: email }, payload: { userName: email.split('@')[0], theme: theme, errorMessage: errorMessage, // Other details... }, }); }, }); ``` *** 3. **The Main Workflow Orchestrator** ```typescript export const processContentRequest = task({ id: "process-content-request", events: [contentGenerationRequested], run: async (payload) => { try { // 1. Generate the content const result = await generateContent.run(payload); // 2. Send success notification await notifyContentReady.run({ userId: payload.userId, email: payload.email, theme: payload.theme, contentId: payload.userId + '-' + Date.now(), contentPreview: result.text.substring(0, 100) + "...", imageUrl: result.imageUrl }); return { success: true, ...result }; } catch (error) { // 3. Send error notification if anything fails await notifyError.run({ userId: payload.userId, email: payload.email, theme: payload.theme, errorMessage: error instanceof Error ? error.message : "Unknown error occurred" }); return { success: false, error: error instanceof Error ? error.message : "Unknown error occurred" }; } }, }); ``` This task: * Responds to the content generation event * Tries to generate content * Sends appropriate notifications based on success or failure *** **Notification Workflows You Would Need in Novu** 1. **ai-generation-completed**: Sent when content is successfully generated Workflow `payload` variables: `{{userName}}`, `{{theme}}`, `{{contentPreview}}`, `{{imageUrl}}`, `{{viewUrl}}` 2. **ai-generation-error**: Sent when content generation fails Workflow `payload` variables: `{{userName}}`, `{{theme}}`, `{{errorMessage}}`, `{{supportEmail}}` ### Video processing This example shows how to build a video transcription service that notifies users when their video has been transcribed or if an error occurs during processing. ```typescript import { task, event, eventTrigger, retry } from "@trigger.dev/sdk/v3"; import { Novu } from "@novu/api"; import fs from "fs"; import { Deepgram } from "@deepgram/sdk"; // Initialize clients const novu = new Novu({ secretKey: 'ApiKey ' + process.env['NOVU_SECRET_KEY'] }); const deepgram = new Deepgram(process.env['DEEPGRAM_API_KEY']); // Utility functions for video processing async function downloadFile(url: string): Promise { // Implementation of file download logic console.log(`Downloading file from ${url}`); // This would be the actual implementation to download a file return "/tmp/downloaded-video.mp4"; } async function convertToWav(filePath: string): Promise { // Implementation of conversion logic console.log(`Converting ${filePath} to WAV format`); // This would be the actual implementation to convert video to WAV return "/tmp/audio-extract.wav"; } // Event trigger for transcription request export const transcriptionRequested = event({ id: "transcription-requested", trigger: eventTrigger({ name: "video.transcription.requested", }), }); // Notification task for successful transcription export const notifyTranscriptionComplete = task({ id: "notify-transcription-complete", run: async ({ userId, email, videoName, transcriptionId, wordCount, duration }: { userId: string; email: string; videoName: string; transcriptionId: string; wordCount: number; duration: string; }) => { await retry.onThrow( async () => { try { await novu.trigger({ workflowId: "transcription-completed", to: { subscriberId: userId, email: email, }, payload: { userName: email.split('@')[0], videoName: videoName, transcriptionId: transcriptionId, wordCount: wordCount, duration: duration, viewUrl: `https://yourapp.com/transcriptions/${transcriptionId}` }, }); console.log("Transcription complete notification sent successfully"); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; console.error("Failed to send transcription complete notification:", errorMessage); throw new Error(`Notification failed: ${errorMessage}`); } }, { maxAttempts: 3, factor: 2, minTimeoutInMs: 1000 } ); }, }); // Error notification task export const notifyTranscriptionError = task({ id: "notify-transcription-error", run: async ({ userId, email, videoName, errorMessage }: { userId: string; email: string; videoName: string; errorMessage: string; }) => { await retry.onThrow( async () => { try { await novu.trigger({ workflowId: "transcription-error", to: { subscriberId: userId, email: email, }, payload: { userName: email.split('@')[0], videoName: videoName, errorMessage: errorMessage, supportEmail: "support@yourapp.com" }, }); console.log("Transcription error notification sent successfully"); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; console.error("Failed to send error notification:", errorMessage); // Not rethrowing here to prevent infinite loops when error notifications fail } }, { maxAttempts: 2 } ); }, }); // Main transcription task export const transcribeVideo = task({ id: "transcribe", retry: { maxAttempts: 3, }, machine: { preset: "large-2x" }, // Use larger machine for faster processing run: async (payload: { id: string; url: string; userId?: string; email?: string; videoName?: string; }) => { try { console.log(`Starting transcription for video: ${payload.videoName || payload.id}`); // Download and process the video const downloadedFile = await downloadFile(payload.url); const extractedAudio = await convertToWav(downloadedFile); // Transcribe using Deepgram const { result, error } = await deepgram.listen.prerecorded.transcribeFile( fs.createReadStream(extractedAudio), { model: "nova", language: "en-US", smart_format: true, diarize: true } ); if (error || !result) { throw new Error(error || "Failed to transcribe video"); } // Calculate some metadata from the result const wordCount = result.results?.utterances?.reduce((count, utterance) => count + utterance.words.length, 0) || 0; const duration = result.metadata?.duration ? `${Math.floor(result.metadata.duration / 60)}:${Math.floor(result.metadata.duration % 60).toString().padStart(2, '0')}` : "Unknown"; // Store in database const dbResult = await db.transcriptions.create(payload.id, result.results); return { transcriptionId: dbResult.id, wordCount, duration, transcript: result.results }; } catch (error) { console.error("Error in transcription process:", error); throw error; // Rethrow to trigger retry } }, }); // Main job that orchestrates the entire workflow export const processTranscriptionRequest = task({ id: "process-transcription-request", events: [transcriptionRequested], run: async (payload: { id: string; url: string; userId: string; email: string; videoName: string; }) => { try { // Validate input if (!payload.url) { throw new Error("Missing video URL"); } // Transcribe video const result = await transcribeVideo.run(payload); // Send completion notification await notifyTranscriptionComplete.run({ userId: payload.userId, email: payload.email, videoName: payload.videoName || "Your video", transcriptionId: result.transcriptionId, wordCount: result.wordCount, duration: result.duration }); return { success: true, transcriptionId: result.transcriptionId, transcript: result.transcript }; } catch (error) { // If anything fails, ensure user gets notified const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; console.error("Transcription failed:", errorMessage); await notifyTranscriptionError.run({ userId: payload.userId, email: payload.email, videoName: payload.videoName || "Your video", errorMessage: errorMessage }); return { success: false, error: errorMessage }; } }, }); // Type declaration for database operations (mock) const db = { transcriptions: { create: async (id: string, results: unknown) => { console.log(`Storing transcription results for ${id} in database`); return { id, created: new Date() }; } } }; ``` **How It Works** * A user uploads a video for transcription * The system processes the video using Deepgram's API * The user receives a notification when transcription is complete or if an error occurs **The Key Components** 1. **Transcription Task** The core of this workflow is the `transcribeVideo` task that handles the actual transcription: ```typescript export const transcribeVideo = task({ id: "transcribe", retry: { maxAttempts: 3 }, machine: { preset: "large-2x" }, // Use larger machine for faster processing run: async (payload) => { // Download the video file const downloadedFile = await downloadFile(payload.url); // Extract and convert audio to WAV format const extractedAudio = await convertToWav(downloadedFile); // Transcribe using Deepgram const { result, error } = await deepgram.listen.prerecorded.transcribeFile( fs.createReadStream(extractedAudio), { model: "nova" } ); // Store results in database return await db.transcriptions.create(payload.id, result?.results); }, }); ``` **This task:** * Downloads the video file from a URL * Extracts audio and converts it to WAV format * Sends the audio to Deepgram for transcription * Stores the results in a database *** 2. **Notification Tasks** There are two notification tasks: **Success Notification** ```typescript export const notifyTranscriptionComplete = task({ id: "notify-transcription-complete", run: async ({ userId, email, videoName, transcriptionId, wordCount, duration }) => { await novu.trigger({ workflowId: "transcription-completed", to: { subscriberId: userId, email }, payload: { userName: email.split('@')[0], videoName, wordCount, duration, viewUrl: `https://yourapp.com/transcriptions/${transcriptionId}` }, }); }, }); ``` **Error Notification** ```typescript export const notifyTranscriptionError = task({ id: "notify-transcription-error", run: async ({ userId, email, videoName, errorMessage }) => { await novu.trigger({ workflowId: "transcription-error", to: { subscriberId: userId, email }, payload: { userName: email.split('@')[0], videoName, errorMessage, supportEmail: "support@yourapp.com" }, }); }, }); ``` *** 3. **Main Workflow Orchestrator** ```typescript export const processTranscriptionRequest = task({ id: "process-transcription-request", events: [transcriptionRequested], run: async (payload) => { try { // 1. Validate and transcribe video const result = await transcribeVideo.run(payload); // 2. Send success notification await notifyTranscriptionComplete.run({ userId: payload.userId, email: payload.email, videoName: payload.videoName || "Your video", transcriptionId: result.transcriptionId, wordCount: result.wordCount, duration: result.duration }); return { success: true, transcriptionId: result.transcriptionId }; } catch (error) { // 3. Send error notification if anything fails await notifyTranscriptionError.run({ userId: payload.userId, email: payload.email, videoName: payload.videoName || "Your video", errorMessage: error instanceof Error ? error.message : "Unknown error occurred" }); return { success: false, error: error instanceof Error ? error.message : "Unknown error occurred" }; } }, }); ``` *** **Usage** 1. **Create two notification workflows in Novu:** * transcription-completed with `payload` variables: `{{userName}}`, `{{videoName}}`, `{{wordCount}}`, `{{duration}}`, `{{viewUrl}}` * transcription-error with `payload` variables: `{{userName}}`, `{{videoName}}`, `{{errorMessage}}`, `{{supportEmail}}` 2. **Trigger the workflow by sending an event:** ```typescript // Example of triggering the workflow await client.triggerEvent({ name: "video.transcription.requested", payload: { id: "video-123", url: "https://storage.example.com/videos/meeting-recording.mp4", userId: "user-456", email: "user@example.com", videoName: "Weekly Team Meeting" } }); ``` **Example Notifications** Success notification email: **Subject**: Your video "Weekly Team Meeting" has been transcribed Hello John, Good news! We've finished transcribing your video "Weekly Team Meeting". Details: * Duration: 32:15 * Word count: 4,320 You can view and edit your transcription here: [https://yourapp.com/transcriptions/transcript-789](https://yourapp.com/transcriptions/transcript-789) Thanks for using our service! Error notification email: **Subject**: Issue with transcribing "Weekly Team Meeting" Hello John, We encountered a problem while transcribing your video "Weekly Team Meeting". Error details: The audio quality was too low for accurate transcription. Please try uploading your video again with improved audio, or contact [support@yourapp.com](mailto:support@yourapp.com) if you need assistance. We apologize for the inconvenience. This workflow provides a complete solution for transcribing videos and keeping users informed about the process status, all while handling errors gracefully. ## Managing Subscribers Before triggering notifications, Novu needs to know *who* to notify. That's where subscribers come in. A **subscriber** in Novu represents a user (or entity) that can receive notifications through one or more channels (in-app, email, SMS, etc.). ### When to Create Subscribers Create or update a subscriber in Novu when: **New User Registration** When a new user signs up or is added to your system **Notification Eligibility** When a user becomes eligible to receive notifications **Data Updates** When you want to ensure subscriber metadata (name, phone, etc.) is up-to-date If you trigger a workflow with a `subscriberId` that doesn't exist, Novu will auto-create the subscriber. However, doing it explicitly ensures full control over subscriber data. ### Subscriber Creation Example ```typescript import { task, retry } from "@trigger.dev/sdk/v3"; import { Novu } from "@novu/api"; const novu = new Novu({ secretKey: 'ApiKey ' + process.env["NOVU_SECRET_KEY"] }); export const createSubscriberTask = task({ id: "create-subscriber-task", run: async (payload: { userId: string; email?: string; firtName?: string; lastName?: string; phone?: string; locale?: string; avatar?: string; data?: object; }) => { const { userId, email, name, phone, avatar } = payload; // Split full name into first and last (fallback to empty string) const [firstName, lastName = ""] = name.split(" "); // Create subscriber object for Novu const subscriber = { subscriberId: userId, email, firstName, lastName, phone, locale, avatar, data, }; await retry.onThrow( async () => { console.log("Creating Novu subscriber:", subscriber); const result = await novu.subscribers.create(subscriber); console.log("Subscriber created successfully:", result); return result; }, { maxAttempts: 2, } ); }, }); ``` ### Using Subscriber Data in Templates Once you've added custom fields to a subscriber, you can use them in Novu templates using Handlebars: Hi `{{subscriber.firstName}}`, welcome! We'll reach you at `{{subscriber.phone}}` if needed. Your account is set up under `{{subscriber.data.userName}}`. ## Best Practices As a best practice, maintain consistent subscriber identification: * Use your internal `userId` as the `subscriberId` in Novu * Keep this mapping consistent across all integrations * Store notification preferences and settings in your user model * Consider adding a reference field in your user table: `novuSubscriberId` Example user model: ```typescript interface User { id: string; email: string; novuSubscriberId: string; // Same as user.id notificationPreferences: { marketing: boolean; updates: boolean; // ... other preferences } } ``` This ensures reliable user tracking and simplifies debugging notification issues. Keep subscriber data synchronized by: * Updating Novu whenever user contact info changes * Implementing webhooks to handle notification delivery status * Setting up retry mechanisms for failed updates ```typescript // Example of proactive update async function updateUserProfile(userId: string, newData: UserUpdateData) { // Update your database await db.users.update(userId, newData); // Sync with Novu await novu.subscribers.update(userId, { email: newData.email, phone: newData.phone, data: { lastUpdated: new Date() } }); } ``` Enhance notifications with contextual data: * Store user preferences, roles, and settings * Include relevant business logic flags * Add metadata for analytics and tracking ```typescript // Example of rich subscriber data await novu.subscribers.update(userId, { data: { role: 'admin', subscriptionTier: 'premium', features: ['advanced-analytics', 'priority-support'] } }); ``` file: ./content/docs/platform/how-novu-works.mdx # How Novu Works A high-level overview of Novu’s architecture: workflows, triggers, subscribers, and environments, and how they connect to support in-app and external channels. import { Step, Steps } from '@/components/steps'; Novu is a notification infrastructure built around the concept of workflows, environments, and subscriber-based delivery. It centralizes multi-channel messaging and supports real-time in-app experiences through a customizable {``} component. At its core, Novu uses workflows to orchestrate how notifications are sent, step by step, to different channels like email, SMS, in-app, push, and chat. Each workflow is triggered by your application and executed in the context of a specific environment. ## The Inbox component in the notification lifecycle The {``} component is Novu’s in-app delivery endpoint. This component makes it possible for users to receive and manage notifications directly within your application interface. Unlike external channels that rely on third-party providers, the Inbox is internal to your app. It uses the environment’s `application identifier` to establish a secure connection, scoped to your subscribers. When a workflow includes an in-app step, messages are routed to the subscriber’s inbox in real time. The Inbox component handles rendering, notification management, users' preferences, and message actions without requiring additional backend logic. ## Core concepts overview At a high level, Novu’s architecture is built around modular but connected concepts. Each concept serves a specific role in the delivery pipeline and is scoped to an environment. ![How Novu works](/images/how-novu-works.png) ### Organization and environments Everything starts with your organization, the top-level entity in Novu. Each organization can have multiple environments, such as: * Development for testing * Production for live apps * Any custom environments you need (for example, Staging) Each environment contains: * Independent workflows * Environment-specific subscribers * Topics * Separate integrations and credentials * API keys for secure access (application identifier, secret keys) This isolation allows teams to test workflows and notifications on one environment without affecting the other environments. ### Workflows and Triggers A workflow defines the steps Novu takes to deliver a notification. Steps are either channel steps (email, in-app, push, chat, and SMS) or action steps (Delay and Digest). Workflows are environment-specific and versioned. You trigger them using the REST API by sending an event along with the relevant secret key from the current environment, `subscriberId`, `workflowId`, and `payload`. Each trigger becomes an event that flows through the workflow. Steps in the workflow generate messages, which are delivered via their corresponding channels or routed to the inbox. To learn more about workflows, refer to the [Workflows documentation](/platform/concepts/workflows). ### Subscribers and topics A subscriber represents a user in a given environment. Each subscriber has: * **Channel identifiers**: Such as email address or phone number. * **A unique `subscriberId`**: Used when triggering workflows and resolving the recipient. * **Personal details**: You can pass `firstName` and `lastName` when creating the subscriber. * **Custom metadata**: Stored under a `data` field, which is a flexible JSON object where you can store any additional user-specific information (for example, plan level, preferences, timestamps). * **Environment-specific preferences**: Like preferred channels and opt-in settings. To send notifications to multiple users, you can group subscribers into a topic. A workflow can be triggered for an individual subscriber or for all subscribers within a topic. Subscriber resolution is part of workflow execution. Novu uses the subscriber's metadata to determine how messages are personalized, delivered, and stored. To learn more about subscribers, refer to the [Subscribers documentation](/platform/concepts/subscribers). ### Channels and the Inbox component Each channel represents a delivery medium (email, SMS, push, chat, or in-app). Channels are linked to provider configurations known as integrations, which are defined per environment. In-app messages are delivered to the {``} component, which is located in your frontend and listens for messages scoped to the current environment and subscriber. To learn more about channels and the Inbox component, refer to the [Channels documentation](/platform/integrations/overview) ### Integrations Integrations connect Novu to external delivery providers (for example, SendGrid, Twilio, Firebase Cloud Messaging, Slack). They are configured per environment and mapped to specific channels. During workflow execution, Novu uses these integrations to send messages for steps that require external delivery. The presence of an integration for a given channel is required for a workflow step to succeed. To learn more about Integrations, refer to the [Integrations documentation](/platform/concepts/integrations). ## How it all connects See how the concepts work together: Your application calls Novu’s API and passes in: * The secret API key for your environment. * The `workflowIdentifier` of the workflow that you want to run. * The `subscriberId` of the user that you want to notify (or a topic key for bulk notifications). * An optional `payload` to customize message content for example, `{{firstName}}`, `{{orderId}}` Novu looks up the subscriber using the ID you provided. This includes: * Channel-specific identifiers (email and phone number). * Subscriber preferences, which might affect delivery behavior. * Any metadata tied to that subscriber in the current environment Each step is evaluated in order. These steps include: * **Channel steps**: Deliver a message via email, SMS, push, in-app, or chat. * **Action steps**: Introduce logic such as delays or digest aggregation. If a step fails (for example, due to a provider error), then Novu retries it without re-executing previous steps. This ensures fault tolerance and message-level traceability. Once a step completes, Novu uses the environment’s configured integrations to deliver the message. For example: * An Email step uses a provider like SendGrid. * An In-App step sends the message to the {``} component in your frontend. * An SMS step uses a provider like Twilio. Each triggered workflow becomes an event with a unique `transactionId`. Novu records: * Which steps ran. * Whether each step succeeded or failed. * How long each step took. * What messages were generated and sent. The Novu pipeline is designed to be consistent, observable, and scalable across channels and environments. file: ./content/docs/platform/overview.mdx # Overview undefined import { OverviewPage } from '@/components/pages/overview-page'; file: ./content/docs/platform/what-is-novu.mdx # What is Novu? The open-source notification infrastructure that simplifies in-app, email, chat, and push notifications. import { Rocket } from 'lucide-react'; import { ReactIcon } from '@/components/icons/react'; import { NextjsIcon } from '@/components/icons/nextjs'; import { RemixIcon } from '@/components/icons/remix'; Novu is an open-source notification infrastructure built for anyone who needs an easy way to implement, manage, and deliver notifications across multi-channels, including in-app, email, chat, push, and SMS. Novu is designed to fit and scale with your existing system by providing a unified API, customizable Inbox component that can be integrated with six lines of code, a drag-and-drop workflow builder, and an intuitive dashboard for sending and managing notifications. Trusted by startups and enterprises alike for its ease of use, developer-first design, and cost-effective scalability, Novu ensures that notifications are delivered reliably to users without complexity. } href="/quickstart/nextjs" title="Quickstart">Get started in minutes and focus on building, not maintaining notifications. ## Why Novu? Novu is designed to provide a powerful, flexible, and easy-to-use notification solution that helps teams save time, reduce complexity, and improve user engagement across multiple platforms. Here’s what sets it apart from other notification infrastructure solutions: ### The only fully open-source notification infrastructure * **Backed by a growing open-source community**. Novu is 100% open-source, with a rich community of creative minds constantly contributing to improve its capabilities. * **Built with an API-first approach**. Developers can deeply customize workflows and automate notifications through Novu APIs. * **Easily integrates with existing tools**. Novu works with modern development stacks, enabling smooth adoption without disrupting workflows. ### Best-in-class in-app notification inbox * **Simplifies in-app notifications**. Developers can integrate real-time notifications in any web or mobile app with six lines of code. * **Offers full customization**. Developers can use hooks and headless libraries to modify the look and feel of the inbox UI. * **Empowers users with preferences**. The {``} built-in preference management lets your users control how they receive notifications. ### True omnichannel notification management * **Unifies all notification channels**. Developers can manage email, in-app, push, SMS, and chat notifications from a single API. * **Optimizes delivery strategies**. Novu enables multi-step workflows, ensuring users receive messages in the most effective channel. * **Reduces notification overload**. The digest feature consolidates updates (for example, “10 new likes on your post”), improving user experience. ### Flexible workflow automation: no-code or code-first * **No-code workflow automation**. The drag-and-drop workflow builder makes notification automation accessible to your non-technical teams via the Novu’s dashboard. * **Provides ultimate control with code-first workflows**. [The Novu Framework SDK](/platform/sdks/overview) lets developers define workflows in code, integrate with CI/CD, and add custom automation logic. * **Consistent notification experience across teams**. Standardizes notification patterns, preventing each team from implementing separate, inconsistent solutions. ### Advanced features for power users * **Extends functionality with the Novu Framework SDK**. Developers can integrate custom workflows beyond Novu’s default capabilities. * **Enhances real-time engagement**. Subscriber presence tracking detects if a user is online before sending notifications. * **Supports multi-tenancy**. Ideal for SaaS businesses managing multiple customer accounts. ### Cost-effective with clear, transparent pricing * **Eliminates unpredictable costs**. Fixed pricing ensures startups and enterprises can scale without hidden fees. * **Offers a free tier for easy adoption**. Independent developers and small businesses can get started at no cost. ## Common Use Cases Here is how developers, startups and enterprises are currently using Novu in real-world projects: ### Transactional notifications Order confirmations and shipping updates for e-commerce. Password reset emails and authentication alerts for SaaS platforms. Payment failure or subscription renewal reminders. ### In-app notification feeds Show unread notifications directly in the app using Novu’s Inbox component. Customize the notification UI with hooks and headless components. Deliver updates, such as mentions, comments, or system alerts. ### Digest and summary notifications “10 people liked your post” (similar to Facebook). A weekly report summarizing account activity. A daily task reminder for productivity apps. ### Multi-step notification workflows Step 1: Send an in-app notification → Step 2: If unread in 24 hours, send an email → Step 3: If still unread, send an SMS reminder. Delay and schedule notifications using Novu’s workflow automation. ### Multi-channel notification delivery Notify VIP customers via SMS while sending regular users an email. Route system alerts to Slack or Microsoft Teams for internal teams. Let's users to opt-in to specific channels (e.g., receive marketing updates via email but urgent alerts via push notifications). ### Customizable notification management Let users enable/disable notification channels (email, SMS, push). Offer per-event notification settings (e.g., only notify me when tagged in a comment). Manage user preferences with Novu’s built-in preference center. ## Next steps Get started with Novu in minutes! Choose a quickstart guide to integrate notifications into your application: } title="Next.js" href="/platform/quickstart/nextjs" description="Get started with our pre-built UI component in Next.js" /> } title="React" href="/platform/quickstart/react" description="Get started with our pre-built UI component in React" /> } title="Remix" href="/platform/quickstart/remix" description="Get started with our pre-built UI component in Remix" /> file: ./content/docs/community/self-hosting-novu/data-migrations.mdx # Data Migrations Learn how to update your database data through migrations. On occasion, Novu may introduce features that require changes to the database schema or data. This usually happens when a feature has a hard dependency on some data being available on a database entity. You can use migrations to make these changes to your database. # How to Run Novu Migrations (Manual Process) Novu does **not automatically run database migrations** when deploying new versions. If you're self-hosting or managing deployments outside a CI/CD pipeline, follow these steps to safely run migrations against your MongoDB database. For Novu Cloud, the Novu team have a custom deployment service that runs migrations on AWS and is triggered by our team. By policy any changes are backward compatible and non schema migrations is needed for any deployment. So any migration can be run after the service was already deployed unless specified otherwise. Those migrations are usually for cleanup purposes, and schema alignment. For Novu Self-Hosting, each migration can be run locally against your Mongodb using known connection strings that have credentials. Your connection may require further access via a tunnel. A script has been added at the bottom that help manages the complexity of versions across connections strings. Manually add and run `run-migrations.sh` script from `apps/api` folder. ## Overview * **Migrations are manual.** * They must be run from an environment that has **access to the migration scripts**. * There is no practical way to run those from within the docker container at the moment becuase the migrations scripts are not shipped with the images * Requires **valid MongoDB credentials** and **network access**. * Typically run **during deployment** of new application versions. * **Novu does not use a versioning or idempotent migration strategy.** See [Versioning Note](#migration-versioning-and-idempotency) below. * Migrations once added to source code do not change (by policy) * Refer to the table of releases to know which to run when * There is a shell script at the end that you can use to ease the burden ( see [run-migrations.sh](#run-migrations-script)) * Success of this process requires cloning the git repository and setting up the projects from source before starting migrations * Success may also require the Redis instance to be running during execution for some migrations (here's the kicker, these are running locally otherwise you would need (potentially) another tunnel through to the remote version \[TODO: confirm side effects (April, 2025)]) * **Warning:** some migrations do not exit fully when run in the bash script ( eg \< 0.19.0 ) and require Ctrl-C to finish and continue * **Warning:** not all migrations are fully documented in the release notes for each release * **Warning:** migrations are dependent on the underlying code (eg repositories) and matches the version of the code against the migration at the right point in time is crucial and each version also requires the correct version of `node` and `pnpm` ## Migration Release Map This table maps each migration script to the likely Novu release version, based on the date the script was added and the closest following release tag. | **Release Version** | **Release Date** | **Migrations Introduced** | | ---------------------------------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`v0.17.2`](https://github.com/novuhq/novu/releases/tag/v0.17.2) | 2023-08-13 | `novu-integrations` *(Integration Store)* | | [`v0.18.0`](https://github.com/novuhq/novu/releases/tag/v0.18.0) | 2023-08-14 | `changes-migration` *(Change Promotion)*
`encrypt-credentials` *(Secure Credentials)*
`expire-at` *(Database TTL)*
`fcm-credentials` *(Secure Credentials)*
`in-app-integration` *(In-App Integration)*
`normalize-users-email` *(Organization Invite Fix)*
`secure-to-boolean` *(Secure Flag Fix)*
`seen-read-support` *(Seen/Read Support)* | | [`v0.21.0`](https://github.com/novuhq/novu/releases/tag/v0.19.0) | 2023-09-01 | `integration-scheme-update` *(Multi-Provider)*
`layout-identifier-update` *(Add layout identifier)* | | [`v0.23.0`](https://github.com/novuhq/novu/releases/tag/v0.23.0) | 2024-02-02 | `encrypt-api-keys` *(API keys encryption)* | | [`v0.24.0`](https://github.com/novuhq/novu/releases/tag/v0.24.0) | 2024-03-06 | `normalize-message-template-cta-action` *(Normalize CTA action)*
`topic-subscriber-normalize` *(Normalize topic-subscriber links)* | | [`v2.0.0`](https://github.com/novuhq/novu/releases/tag/v2.0.0) | 2024-10-07 | `subscribers` *(Subscriber record adjustments)* | | [`v2.0.1`](https://github.com/novuhq/novu/releases/tag/v2.0.1) | 2024-11-11 | `preference-centralization` *(Preference model centralization)* — **ensure this is done before v2.1 as repository access has been removed** | | *(future release)* | *sometime 2025* | `deleteLogs` *(Cleanup deleted logs)* | ### Historical releases | Version | Feature | Migration Path(s) | | ------------------------------------------------------------ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | [v0.23](https://github.com/novuhq/novu/releases/tag/v0.23.0) | API keys encryption | `./encrypt-api-keys/encrypt-api-keys-migration.ts` | | [v0.18](https://github.com/novuhq/novu/releases/tag/v0.18.0) | Multi-Provider | `./integration-scheme-update/add-primary-priority-migration.ts`
`./integration-scheme-update/add-integration-identifier-migration.ts` | | | Integration Store | `./novu-integrations/novu-integrations.migration.ts` | | [v0.16](https://github.com/novuhq/novu/releases/tag/v0.16.0) | In-App Integration | `./in-app-integration/in-app-integration.migration.ts` | | | Secure Flag Fix | `./secure-to-boolean/secure-to-boolean-migration.ts` | | [v0.15](https://github.com/novuhq/novu/releases/tag/v0.15.0) | Database TTL | `./expire-at/expire-at.migration.ts` | | [v0.12](https://github.com/novuhq/novu/releases/tag/v0.12.0) | Organization Invite Fix | `./normalize-users-email/normalize-users-email.migration.ts` | | [v0.9](https://github.com/novuhq/novu/releases/tag/v0.9.0) | Seen/Read Support | `./seen-read-support/seen-read-support.migration.ts` | | [v0.8](https://github.com/novuhq/novu/releases/tag/v0.8.0) | Secure Credentials | `./fcm-credentials/fcm-credentials-migration.ts`
`./encrypt-credentials/encrypt-credentials-migration.ts` | | [v0.4](https://github.com/novuhq/novu/releases/tag/v0.4.0) | Change Promotion | `./changes-migration.ts` | ## Step-by-Step Instructions ### Runtime prerequisites * `node` * `npm` * `pnpm` * `npx` * MacOS (or linux) * tunnel (optional `ssh`) Some migration scripts (like `in-app-integration`) require access to Redis during execution. If Redis is not running, you will encounter `ECONNREFUSED 127.0.0.1:6379`. The problem here is that remote environments have their own Redis and currently the assumption is that migrations should only affect the MongoDB collection. To fix this start Redis locally ( or launch a Docker container: `docker run -p 6379:6379 redis`) ### 1. Clone the Novu Repository Ensure you’re using the same version as the one you’re deploying: ```bash git clone https://github.com/novuhq/novu.git cd novu git checkout # e.g. v0.24.0 npm run clean npm run setup:project # really important to run if you change tag cd apps/api ``` **Warning:** migrations are dependent on the underlying code (eg repositories) * match the version of the code against the migration at the right point in time * each version often requires the correct version of `node` and `pnpm` as per the `package.json` **Notes:** * Getting the code the run at a checked out version can be tricky * If you want to know which tags are available: `git tag` * Some migrations may fail because they rely on the underlying code rather than direct mongo queries * Migrations are located in: `apps/api/migrations/` * Checkout by tag stops accidentally running too many migrations that would normally be managed by migrations runner ( see [migration versioning](#migration-versioning-and-idempotency) ) ### 2. Connect to Your MongoDB Database If your database is not directly accessible, you may need to tunnel: ```bash # Example: Tunnel MongoDB via SSH ssh -L 27017:localhost:27017 your-user@your-db-host ``` Have your: * MongoDB **connection string** * **Username and password** (if required) ### 3. Run the Migration Script > use the [script `run-migrations.sh` below](#run-migrations-script) for all the following steps Configure access to the correct database: ```bash npx cross-env \ MONGO_URL=mongodb://127.0.0.1:27017/novu-db \ NEW_RELIC_ENABLED=false \ npm run migration -- ./migrations/normalize-users-email/normalize-users-email.migration.ts ``` Some notes: * under the hood of the migration script `npx ts-node --transpile-only ./apps/api/migrations/.ts` * some migrations require REDIS (ensure already running) * some migrations require NEW\_RELIC (so turn off) * Run each script only once. Monitor logs or database changes for success. ### 5. Repeat for Each Relevant Migration Use a [migration release map](#migration-release-map) to determine which scripts are required for your upgrade. ## Post-Migration Validation After completing migrations: * Verify new fields/collections are present * Check logs for errors * Confirm application starts and behaves as expected ## Migration Versioning and Idempotency Novu **does not implement a versioning system** or record migration history in the database. This means: * Migrations **must be manually tracked**. * There is **no built-in idempotency** for running the migration, so running a script twice may cause duplicate data or errors (but by policy the underlying code does its best) * It is your responsibility to **ensure each script runs only once** and in the correct order. ### What Is an Idempotent Migration? An *idempotent* migration can be safely run multiple times without changing the outcome after the first execution. This is typically achieved through: * Tracking applied migrations in a collection (e.g., `migrations`). * Guard clauses in code to check if a change has already been made. Note: * there are libraries that support idempotent Mongo migrations in TypeScript * if you want to introduce versioned/idempotent migrations in your own deployment process, consider [ `migrate-mongo`](https://github.com/seppevs/migrate-mongo) or [ `mongodb-migrations`](https://github.com/emirotin/mongodb-migrations) ## Tips * Always **back up your database** before running migrations. * Consider scripting or CI/CD automation for repeatability. * **Dockerized deployments do not auto-run migrations**—there is no explicit configuration to turn this on either. * You could implement your own migrations versioning on top #### Run Migrations Script Use this shell to ease the burden, ensure that it is executable. Instructions are in the script comments. ```bash chmod +x run-migrations.sh ``` ```bash run-migrations.sh #!/bin/sh ############################################################################### # 📦 NOVU MIGRATION RUNNER - POSIX SH EDITION (macOS Compatible) # # This script runs one or more database migration scripts for the Novu project. # It supports: # - Selecting a version interactively or via argument # - Providing a custom MongoDB connection string # - Executing version-specific migration files in order # - Compatible with macOS default /bin/sh # # ⚠️ WARNING: some migrations do not exit properly ( eg < 0.19.0 ) and require Ctrl-C to finish and continue # # 🛠 REQUIREMENTS: # - Node.js (with npm and npx) # - ts-node and dependencies installed via your project # # ▶️ USAGE: # ./run-migrations.sh # Interactive version selection # ./run-migrations.sh v0.24.0 # Run migrations for version # ./run-migrations.sh mongodb://... # Use custom Mongo URL (interactive version) # ./run-migrations.sh v0.18.0 mongodb://... # Version + custom Mongo URL # ./run-migrations.sh v0.24.0 --dry-run # Dry run file check only # # Atlas connection strings # - remove all & from query params # eg mongodb+srv://user:password@instance-name.tmah3.mongodb.net/novu-db # # ⚠️ REDIS WARNING: # Some migrations require Redis (on 127.0.0.1:6379) to be running. # If Redis is not available, you may see ECONNREFUSED errors during execution. # # To fix this: # • Start Redis ############################################################################### set -e DEFAULT_MONGO_URL="mongodb://127.0.0.1:27017/novu-db" NEW_RELIC_ENABLED=false VERSION="" MONGO_URL="" DRY_RUN=false # Argument parsing for arg in "$@"; do case "$arg" in v*) VERSION="$arg" ;; mongo*) MONGO_URL="$arg" ;; --dry-run) DRY_RUN=true ;; esac done [ -z "$MONGO_URL" ] && MONGO_URL="$DEFAULT_MONGO_URL" # Define versions and associated keys # versions must match suffix VERSIONS="v0.17.2 v0.18.0 v0.21.0 v0.23.0 v0.24.0 v2.0.0 v2.0.1 Future" MIGRATION_MAP_v0_17_2="novu-integrations" MIGRATION_MAP_v0_18_0="changes-migration encrypt-credentials expire-at fcm-credentials in-app-integration normalize-users-email secure-to-boolean seen-read-support" MIGRATION_MAP_v0_21_0="integration-scheme-update layout-identifier-update" MIGRATION_MAP_v0_23_0="encrypt-api-keys" MIGRATION_MAP_v0_24_0="normalize-message-template-cta-action topic-subscriber-normalize" MIGRATION_MAP_v2_0_0="subscribers" MIGRATION_MAP_v2_0_1="preference-centralization" MIGRATION_MAP_Future="deleteLogs" # Migration file paths get_migration_path() { case "$1" in fcm-credentials) echo "fcm-credentials/fcm-credentials-migration.ts" ;; layout-identifier-update) echo "layout-identifier-update/add-layout-identifier-migration.ts" ;; normalize-message-template-cta-action) echo "normalize-message-template-cta-action/normalize-message-cta-action-migration.ts" ;; changes-migration) echo "changes-migration.ts" ;; seen-read-support) echo "seen-read-support/seen-read-support.migration.ts" ;; in-app-integration) echo "in-app-integration/in-app-integration.migration.ts" ;; novu-integrations) echo "novu-integrations/novu-integrations.migration.ts" ;; expire-at) echo "expire-at/expire-at.migration.ts" ;; secure-to-boolean) echo "secure-to-boolean/secure-to-boolean-migration.ts" ;; encrypt-api-keys) echo "encrypt-api-keys/encrypt-api-keys-migration.ts" ;; encrypt-credentials) echo "encrypt-credentials/encrypt-credentials-migration.ts" ;; topic-subscriber-normalize) echo "topic-subscriber-normalize/topic-subscriber-normalize.migration.ts" ;; subscriber-preferences-level) echo "subscriber-preferences-level/subscriber-preferences-level.migration.ts" ;; integration-scheme-update) echo "integration-scheme-update/add-primary-priority-migration.ts integration-scheme-update/update-primary-for-disabled-novu-integrations.ts integration-scheme-update/add-integration-identifier-migration.ts" ;; normalize-users-email) echo "normalize-users-email/normalize-users-email.migration.ts" ;; subscribers) echo "subscribers/remove-duplicated-subscribers/remove-duplicated-subscribers.migration.ts" ;; preference-centralization) echo "preference-centralization/preference-centralization-migration.ts" ;; deleteLogs) echo "deleteLogs/deleteLogsCollection.ts" ;; *) echo "" ;; esac } # Interactive version selector if [ -z "$VERSION" ]; then echo "📦 No version provided. Please choose one:" i=1 for v in $VERSIONS; do echo " $i) $v" eval "VERSION_INDEX_$i=$v" i=$((i + 1)) done echo printf "Enter the number of the version to run migrations for: " read choice eval "VERSION=\$VERSION_INDEX_$choice" if [ -z "$VERSION" ]; then echo "❌ Invalid selection." exit 1 fi echo "✅ Selected version: $VERSION" fi # Normalize version string to match variable names VERSION_VAR=$(echo "$VERSION" | tr '.' '_' | tr '-' '_') eval "MIGRATION_KEYS=\$MIGRATION_MAP_$VERSION_VAR" echo echo "🌐 Using MongoDB: $MONGO_URL" echo "🚀 Running migrations for version: $VERSION" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" for key in $MIGRATION_KEYS; do paths=$(get_migration_path "$key") for path in $paths; do fullPath="./migrations/$path" if [ "$DRY_RUN" = true ]; then if [ -f "$fullPath" ]; then echo "✅ File exists: $fullPath" else echo "❌ File missing: $fullPath" fi else echo "🔁 Running migration: $fullPath" npx cross-env MONGO_URL="$MONGO_URL" NEW_RELIC_ENABLED="$NEW_RELIC_ENABLED" npm run migration -- "$fullPath" fi done done if [ "$DRY_RUN" = true ]; then echo "✅ Dry-run completed." else echo "✅ All applicable migrations completed." fi ``` ### Reference for Generating Table The migration-to-release mapping is done by comparing the migration creation date to the next release tag chronologically. If multiple migrations appear before a release, they’re grouped under that version. #### Generating Migrations List ```bash for file in *; do echo "$(git log --diff-filter=A --follow --format=%ad --date=short -- $file | head -1) $file" done | sort 2023-07-31 novu-integrations 2023-08-01 changes-migration.ts 2023-08-01 encrypt-credentials ... 2024-03-24 subscribers 2024-11-19 preference-centralization 2024-11-29 deleteLogs ``` #### Generating Releases List ```bash git for-each-ref --sort=creatordate --format '%(creatordate:short) %(refname:short)' refs/tags 2023-08-13 v0.17.2 2023-08-14 v0.18.0 ... 2024-02-07 v0.23.1 2024-03-06 v0.24.0 2024-04-05 v0.24.1 2024-10-07 v2.0.0 2024-11-11 v2.0.1 ``` #### Listing all migrations ```bash find . -type f -name "*.ts" ! -name "*.spec.ts" ./fcm-credentials/fcm-credentials-migration.ts ./layout-identifier-update/add-layout-identifier-migration.ts ./normalize-message-template-cta-action/normalize-message-cta-action-migration.ts ./normalize-message-template-cta-action/normalize-message-template-cta-action-migration.ts ./changes-migration.ts ./seen-read-support/seen-read-support.migration.ts ./in-app-integration/in-app-integration.migration.ts ./novu-integrations/novu-integrations.migration.ts ./expire-at/expire-at.migration.ts ./secure-to-boolean/secure-to-boolean-migration.ts ./encrypt-api-keys/encrypt-api-keys-migration.ts ./encrypt-credentials/encrypt-credentials-migration.ts ./topic-subscriber-normalize/topic-subscriber-normalize.migration.ts ./subscriber-preferences-level/subscriber-preferences-level.migration.ts ./integration-scheme-update/add-primary-priority-migration.ts ./integration-scheme-update/update-primary-for-disabled-novu-integrations.ts ./integration-scheme-update/add-integration-identifier-migration.ts ./normalize-users-email/normalize-users-email.migration.ts ``` file: ./content/docs/community/self-hosting-novu/deploy-with-docker.mdx # Deploy with Docker Learn how to deploy Novu with Docker Docker compose is the easiest way to get started with self-hosted Novu. This guide will walk you through the steps to run all services in single virtual machine using docker compose. This guide uses latest docker images. If you are looking to self host 0.24.x version, checkout [0.24.x docs](https://v0.x-docs.novu.co/self-hosting-novu/deploy-with-docker) ## Prerequisites You need the following installed in your system: * [Docker](https://docs.docker.com/engine/install/) and [docker-compose](https://docs.docker.com/compose/install/) * [Git](https://git-scm.com/downloads) ## Quick Start ### Get the code Clone the Novu repo and enter the docker directory: ```bash # Get the code git clone --depth 1 https://github.com/novuhq/novu # Go to the docker community folder cd novu/docker/community # Copy the example env file cp .env.example .env ``` ### Configure Environment #### Local Deployment To run Novu in local machine, default configuration can be used. Start Novu with: ```bash title="novu/docker/community" docker-compose up ## if above command is not working, use docker compose docker compose up ``` Now visit [http://localhost:4000](http://localhost:4000/) to start using Novu. #### VPS Deployment When deploying to a VPS, update your `.env` file with your server's information: ```bash # Replace with your VPS IP address HOST_NAME=http:// ``` Start Novu on your VPS: ```bash docker-compose -f docker-compose.yml up ``` Access your dashboard at [http://vps-ip-address:4000](http://vps-ip-address:4000/). ## Securing Your Setup While we provide example secrets for getting started, you should NEVER deploy your Novu setup using the defaults provided. Update the `.env` file with your own secrets. ### Required Variables: * `JWT_SECRET`: Used by the API to generate JWT keys. * `STORE_ENCRYPTION_KEY`: Used to encrypt/decrypt the provider credentials. It must be 32 characters long. * `HOST_NAME`: Host name of your installation: * To run in local machine: `http://localhost` * To run in VPS: Your server's IP address (e.g., `http://`) or domain name * `REDIS_CACHE_SERVICE_HOST` and `REDIS_HOST` can have same value for small deployments. For larger deployments, it is recommended to use separate Redis instances for caching and queue management. ## Configuration To keep the setup simple, we made some choices that may not be optimal for production: * the database is in the same machine as the servers * the storage uses localstack instead of S3 We strongly recommend that you decouple your database before deploying. ## Setting Up the Inbox Component This section explains how to integrate the Novu Inbox component into your application when using a self-hosted Novu deployment. ### Install the required packages ```bash npm install @novu/react react-router-dom ``` ### Create the Inbox component Create a component file (e.g., `notification-center.tsx`) in your project: ```tsx import React from 'react'; import { Inbox } from '@novu/react'; import { useNavigate } from 'react-router'; export function NotificationCenter() { const navigate = useNavigate(); return ( navigate(path)} /> ); } ``` ### Configure the environment URLs Adjust the `backendUrl` and `socketUrl` based on your deployment: ### Testing the connection Once your application is running, you should see the bell icon in your navbar. Clicking it will open the notification inbox UI. To test notifications, create and trigger a workflow from your self-hosted Novu dashboard, selecting In-App as the channel. For more information on customizing the Inbox component, refer to the [Inbox documentation](/inbox/overview). ## Initializing the Node SDK When using a self-hosted Novu deployment with your backend services, you need to configure the Node SDK to connect to your Docker-hosted Novu instance. ### Install the package ```bash npm install @novu/api ``` ### Initialize the SDK Configure the SDK with your self-hosted backend URL: ```typescript import { Novu } from '@novu/api'; const novu = new Novu({ secretKey: "", // Your Novu API key from the dashboard serverURL: "http://:3000", // URL of your self-hosted Novu API instance }); ``` ### Configure for different environments Adjust the `backendUrl` based on your deployment: ### Triggering events Once initialized, you can trigger notification events: ```typescript await novu.trigger({ workflowId: 'workflowId', to: { subscriberId: 'subscriberId', }, payload: { // Your custom payload data name: 'John Doe', orderId: 'ORDER_ID_123', }, }); ``` For more information on using the Node SDK, refer to the [Server-Side SDKs documentation](/sdks/overview). ### Setting Up local studio and bridge application #### Setting Up the bridge application The bridge application is application where workflow definition are written using `@novu/framework`. Here's how to set it up: ```bash # Initialize the bridge application npx novu@latest init \ --secret-key= \ --api-url=http://localhost:3000 # Install dependencies npm install # Start the bridge application npm run dev ``` Nextjs based bridge application having one test workflow written using `@novu/framework` will be running on `http://localhost:4000`, Go to [http://localhost:4000/api/novu](http://localhost:4000/api/novu) to see status. #### Setting Up Novu Studio [Novu Local Studio](/framework/studio) is a development environment that allows you to test and manage your workflows. The setup varies based on your deployment: 1. Running in local machine if novu is run using above docker compose command in local machine, use below commmand ```bash npx novu@latest dev -d http://localhost:4000 -p ``` Following actions will occur: * Novu local studio will be started on default port 2002, * Novu will generate a tunnel url that will forward the request to bridge application running on `` * Studio will use `http://localhost:4000` as dashboard url **Using bridge application url as bridge url** To use bridge application url as bridge url, use below command: ```bash npx novu@latest dev -d http://localhost:4000 -p -t http://host.docker.internal: # example: npx novu@latest dev -d http://localhost:4000 -p 4000 -t http://host.docker.internal:4000 ``` In Windows OS, there are some additional steps: * stop the running docker compose process using `ctrl + c` * update the `docker-compose.yml` file and add below config with each service (api, dashboard, worker and ws) ```bash extra_hosts: - "host.docker.internal:host-gateway" ``` * start the docker compose process again using `docker compose up` * now you can use `host.docker.internal` as bridge url hostname inplace of `localhost` 2. Running in VPS ```bash # update the bridge .env file with below variables NOVU_API_URL=http://:3000 # Start Novu Studio with your VPS dashboard URL and bridge application URL npx novu@latest dev -d http://:4000 ``` Check all [available flags](/framework/studio#novu-cli-flags) with `npx novu dev` command ### Synchronizing Workflows 1. For local deployment: ```bash npx novu@latest sync \ --bridge-url /api/novu \ --api-url http://localhost:3000 \ --secret-key ``` 2. For VPS deployment: ```bash npx novu@latest sync \ --bridge-url /api/novu \ --api-url http://:3000 \ --secret-key ``` ### VPS Security Considerations When deploying to a VPS, consider these additional security measures: 1. Use a firewall to restrict access to only necessary ports 2. Set up SSL/TLS certificates for HTTPS access 3. Regularly update your Docker images and host system 4. Use strong, unique secrets in your `.env` file 5. Consider using a reverse proxy like Nginx for additional security layers ### Triggering events with custom installation When self-hosting Novu, in order to trigger an event you must first create a new `Novu` object and configure it with the proper `backendUrl`. ```tsx import { Novu } from '@novu/api'; const novu = new Novu({ secretKey: '', serverURL: '', }); await novu.trigger({ workflowId: 'workflowId', to: { subscriberId: 'subscriberId', }, payload: {}, }); ``` ### Caching We are introducing the first stage of caching in our system to improve performance and efficiency. Caching is turned off by default, but can easily be activated by setting the following environment variables: * REDIS\_CACHE\_SERVICE\_HOST * REDIS\_CACHE\_SERVICE\_PORT Currently, we are caching data in the most heavily loaded areas of the system: the widget requests such as feed and unseen count, as well as common DAL requests during the execution of trigger event flow. These are the most heavily used areas of our system, and we hope that by implementing caching in these areas, we can improve performance in the near future. ### Reverse-Proxy / Load Balancers To implement a reverse-proxy or load balancer in front of Novu, you need to set the GLOBAL\_CONTEXT\_PATH for the base path of the application. This is the path that the application will be served from after the domain. For example: - company.com/novu This is used to set the base path for the application, and is used to set the base path for the API, Dashboard, and WebSocket connections. The following environment variables are used to set the context path for each public service that Novu provides: API\_CONTEXT\_PATH WIDGET\_CONTEXT\_PATH WS\_CONTEXT\_PATH DASHBOARD\_CONTEXT\_PATH These allow you to set the context path for each service independently or dependently of the GLOBAL\_CONTEXT\_PATH. For example, if I was using a reverse proxy to serve Novu from company.com/novu, I would set the GLOBAL\_CONTEXT\_PATH to novu, and then set the API\_CONTEXT\_PATH to api, the WIDGET\_CONTEXT\_PATH to widget, the WS\_CONTEXT\_PATH to ws, and the DASHBOARD\_CONTEXT\_PATH to Dashboard. This would produce the following urls: - API: company.com/novu/api - WIDGET: company.com/novu/widget - WS: company.com/novu/ws - DASHBOARD: company.com/novu/dashboard However the Service context path can be used entirely independently of the GLOBAL\_CONTEXT\_PATH. For example, if I wanted to expose the api as novu-api, I would set the API\_CONTEXT\_PATH to novu-api without setting the GLOBAL\_CONTEXT\_PATH. This would producte the following url: - API: company.com/novu-api These env variables should be present on all services novu provides due to tight coupling. ## FAQs ### Local Tunnel and Self-Hosted Deployments Novu uses a local tunnel as bridge url. It can be used as bridge url with local studio and for testing purpose in development environment. It should not be used in production environment. It is recommended to use deployed application url as bridge url #### When is Local Tunnel Not Required? If the customer's application and the self-hosted Novu deployment are within the same network, there is no need for a local tunnel. In this case, the application can communicate directly with Novu through the internal network. Checkout `Using bridge application url as bridge url` section to learn more. #### When is Local Tunnel Required? If the application and Novu deployment reside on different networks, you can still interact with your self-hosted Novu instance using the Novu CLI. The CLI allows you to specify the Dashboard URL and Bridge Endpoint Origin to enable communication across networks via the Novu Cloud Local Tunnel. For example, you can use the following command: ```bash npx novu@latest dev -d http://my-self-hosted-novu-domain.com:my-port ``` file: ./content/docs/community/self-hosting-novu/overview.mdx # Overview Self-hosting Novu provides full control and flexibility over your notification infrastructure, with specific system requirements for optimal performance across VMs, Redis, MongoDB, FerretDB, and S3 storage. When self-hosting Novu, you take full control over your communication infrastructure by deploying it on your own servers. While this setup allows for customization and greater flexibility, it is important to note that some features exclusive to Novu's cloud-managed solution will not be available in a self-hosted environment. ## System requirements overview ### Hosting Novu services on separate VMs For optimal performance, we recommend hosting Novu's core services across multiple virtual machines (VMs). * **Novu services:** * 3 VMs per service * Each VM: 2 vCPUs and 4GB of RAM * **Redis:** * 2 Redis clusters (one dedicated to queues with Append-Only Log (AOL) enabled) * Minimum: 8GB RAM per cluster * **MongoDB:** * 1 MongoDB cluster (M20 or higher on MongoDB Atlas) * **Storage:** * 10GB of S3 storage ### Hosting all Novu services on a single VM If resources are limited or simplicity is a priority, Novu services can be hosted on a single VM. * **All services:** 36 vCPUs and 64GB of RAM * **Storage:** 10GB of S3 storage ### Redis requirements * **Redis Clusters:** 2 (one for queues with AOL enabled) * **Memory:** 8GB RAM per cluster * **AOL:** Active Append-Only Log (AOL) for data persistence and to prevent job loss during outages. ### MongoDB requirements * **MongoDB cluster:** M20 or higher (recommended) on MongoDB Atlas. ### FerretDB requirements * **[FerretDB instance](https://docs.ferretdb.io/) and PostgreSQL with [DocumentDB extension](https://github.com/FerretDB/documentdb) backend.** ### Storage requirements * **S3 storage:** Minimum 10GB for file storage. The above specifications are general recommendations. Adjust them based on your system load, usage patterns, and scale of operations. Self-hosting Novu does not support GitHub login. To access your account, please use the email and password associated with your Novu account. file: ./content/docs/community/self-hosting-novu/telemetry.mdx # Telemetry Learn about Novu's telemetry data collection and how to configure it Telemetry in Novu encompasses the collection of data regarding user interactions with the platform. This data enables the Novu team to identify usage patterns, troubleshoot issues, and make informed decisions about new features and improvements. ## Data collected by Novu Novu does not capture any data from your APIs, databases, or third-party tools. All information collected from self-hosted instances is completely anonymized to protect user privacy. ### Keep Alive Beacon The Novu server sends a keep-alive ping every hour to confirm its operational status without errors. This data is collected regardless of whether telemetry is enabled or disabled. ```json { "freeMemory": 115703808, "hostname": "somemachine", "instanceId": "ba54fb29-6422-4a83-a0e4-951d767efa73", "ipAddress": "192.168.1.4", "platform": "darwin", "release": "23.6.0", "timestamp": "2024-10-04T11:54:14", "totalMemory": 8589934592 } ``` ### Usage Statistics When telemetry is enabled, the server collects anonymous information regarding users, organizations, events, notifications, and more. This data provides insights into platform usage and helps us improve the Novu Project for all users. ```json { "eventCount": 1, "integrationCount": [ { "count": 1, "providerId": "example" }, { "count": 1, "providerId": "novu" } ], "orgCount": 1, "subscriberCount": 1, "timestamp": "2024-10-04T11:54:30", "topicCount": 3, "totalSteps": 10, "userCount": 3, "workflowCount": 6 } ``` ## Disable telemetry Sharing telemetry data is optional. You can disable telemetry via the Admin Settings or by modifying the relevant environment variable. ### Environment variable To disable telemetry using the `NOVU_TELEMETRY` environment variable, follow these steps: 1. Go to the directory where the .env file is located. 2. Open the file in an editor and search for `NOVU_TELEMETRY`. 3. Change the value of `NOVU_TELEMETRY` to `false`. 4. Restart the Docker container. Once the container restarts, Novu will be running with telemetry disabled. file: ./content/docs/community/self-hosting-novu/v0-to-v2-migration.mdx # Migrating from v0 Web UI to v2 Dashboard A guide to help you migrate from Novu self-hosted v0 to v2 # Migrating from Novu Self-Hosted v0 to v2 This guide will help you migrate your existing Novu self-hosted v0 instance to v2. Due to significant architectural changes between versions, some manual steps are required, particularly for workflow migration. ## Terminology * Web UI: The old Novu UI, that you are currently using. * Dashboard: The new Novu UI, that is powered by the v2 APIs and architecture. ## Prerequisites * Access to your existing v0 instance * New v2 instance ready for deployment * Temporary ability to run both instances simultaneously for migration purposes ## Main Changes in v2 Dashboard The new Dashboard brings a cleaner, and more intuitive UI. We have reconsidered some core principles, let's review the most important ones: ### Subscribers The Subscribers page has been redesigned to provide a more comprehensive view of your subscribers, including preferences management and credentials. ### Topics Brand new topics page, to view and manage your topics in Novu. ### Workflows This area of the product was completely redesigned, and old v0 workflows will not be visible in the new UI. The new Editor includes a block based editor for E-mail, with the ability to provide custom HTML blocks if needed. ### Layouts Layout management is available on the new Dashboard. Checkout the [layouts](/docs/platform/workflow/layouts) documentation for more details. ### Variants (Deprecated) Workflow Variants will not be available in the v2 UI, if you rely on them, you can continue to use the old UI for those workflows. ### Changes (Deprecated) The changes mechanism was removed, in favor of a more simple "Sync" mechanism, that enables you to sync workflow state between environments, Production environment can also be modified directly from the UI, however we still recommend using the Dev environment and syncing the changes to Production. ### User management (Not available) You can still login to your old Novu Users and their passwords with the new Dashboard, However we are not allowing team management flows with the new version. In v2 we have moved our internal cloud implementation to use Clerk as our authentication and authorization provider, and we are not able to maintain 2 separate user management systems. We recommend creating a reusable user credentials and share them between your team. ## API Changes in v2 The v2 API introduces several new endpoints and improvements. Here are the key changes: ### New v2 Routes 1. **Subscribers** (`/v2/subscribers`) * Enhanced subscriber management with new endpoints: * `GET /` - Search subscribers with advanced filtering * `GET /:subscriberId` - Retrieve specific subscriber * `POST /` - Create new subscriber * `PATCH /:subscriberId` - Update subscriber details * `DELETE /:subscriberId` - Remove subscriber * New preferences management: * `GET /:subscriberId/preferences` - Get subscriber preferences * `PATCH /:subscriberId/preferences` - Update preferences 2. **Topics** (`/v2/topics`) 3. **Workflows** (`/v2/workflows`) ## Migrating Workflows Unfortunately, we do not have an automated way to migrate v0 workflows created in the v0 UI. To migrate workflows, you will need to manually recreate them in the new Dashboard UI. The old Web UI dashboard is compatible with the new v2 API, so you don't have to worry about updating your API instance, as it will continue to work as expected. While running the old "Web UI", deploy the new "Dashboard" docker image under another host (we use dashboard.novu.co and dashboard-v0.novu.co for this example). During the transition period, you will have the two dashboards running in the same time, so you can copy the workflows from the old UI to the new one. Workflows created in the new UI will not be visible under the old Web UI, but old workflows create will appear in the new UI however, will not be editable from there. 1. **Access both dashboards**: Open the admin dashboards for both versions 2. **Copy workflow configurations**: For each workflow in v0: * Note all steps, templates, and trigger configurations * Manually recreate them in v2 with the same structure * Verify variables, conditions, and integrations are properly configured ## FAQs ### If I currently use the v2 docker images, what should I do? If you already have the v2 docker images running, you will just need to run the new dashboard image in parallel to the existing "web" image, and migrate your workflows. ### What breaking changes are in the new v2 images (API) The new v2 images are backward compatible with the old v0 API, so you don't have to worry about updating your instances. However, to enjoy the new Workflow editor experience, you will need to migrate your workflows to the new v2 engine. ### What is the difference between Novu Cloud, Enterprise and Self-hosted? You can learn more about the differences between the different editions [here](https://docs.novu.co/community/project-differences). ## Need Help? If you encounter challenges during migration, our community is here to help: * Join our [Discord community](https://discord.gg/novu) * Open issues on [GitHub](https://github.com/novuhq/novu) * Check the [documentation](https://docs.novu.co) for the latest updates file: ./content/docs/api-reference/activity/track-activity-and-engagement-events.mdx # Track activity and engagement events undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Track activity and engagement events for a specific delivery provider file: ./content/docs/api-reference/channel-connections/create-a-channel-connection.mdx # Create a channel connection undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Create a new channel connection for a resource for given integration. Only one channel connection is allowed per resource and integration. file: ./content/docs/api-reference/channel-connections/delete-a-channel-connection.mdx # Delete a channel connection undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Delete a specific channel connection by its unique identifier. file: ./content/docs/api-reference/channel-connections/list-all-channel-connections.mdx # List all channel connections undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} List all channel connections for a resource. file: ./content/docs/api-reference/channel-connections/retrieve-a-channel-connection.mdx # Retrieve a channel connection undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve a specific channel connection by its unique identifier. file: ./content/docs/api-reference/channel-connections/update-a-channel-connection.mdx # Update a channel connection undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update an existing channel connection by its unique identifier. file: ./content/docs/api-reference/channel-endpoints/create-a-channel-endpoint.mdx # Create a channel endpoint undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Create a new channel endpoint for a resource. file: ./content/docs/api-reference/channel-endpoints/delete-a-channel-endpoint.mdx # Delete a channel endpoint undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Delete a specific channel endpoint by its unique identifier. file: ./content/docs/api-reference/channel-endpoints/list-all-channel-endpoints.mdx # List all channel endpoints undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} List all channel endpoints for a resource based on query filters. file: ./content/docs/api-reference/channel-endpoints/retrieve-a-channel-endpoint.mdx # Retrieve a channel endpoint undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve a specific channel endpoint by its unique identifier. file: ./content/docs/api-reference/channel-endpoints/update-a-channel-endpoint.mdx # Update a channel endpoint undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update an existing channel endpoint by its unique identifier. file: ./content/docs/api-reference/contexts/context-schema.mdx # Context schema undefined ### Context **Context** is a reusable, user-defined data object that stores metadata (like tenant, region, or app details) to organize and personalize notifications. Unlike payloads that exist only for a single workflow execution, contexts are persistent and can be shared across multiple workflows and API calls. This makes them particularly useful for multi-tenant applications, dynamic branding, and scenarios where notifications need to reference common, reusable data. Read more about how to use contexts in workflows in the [contexts](/platform/workflow/contexts) section. file: ./content/docs/api-reference/contexts/context-schema.model.mdx # Context schema undefined ### Context **Context** is a reusable, user-defined data object that stores metadata (like tenant, region, or app details) to organize and personalize notifications. Unlike payloads that exist only for a single workflow execution, contexts are persistent and can be shared across multiple workflows and API calls. This makes them particularly useful for multi-tenant applications, dynamic branding, and scenarios where notifications need to reference common, reusable data. Read more about how to use contexts in workflows in the [contexts](/platform/workflow/contexts) section. file: ./content/docs/api-reference/contexts/create-a-context.mdx # Create a context undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Create a new context with the specified type, id, and data. Returns 409 if context already exists. **type** and **id** are required fields, **data** is optional, if the context already exists, it returns the 409 response file: ./content/docs/api-reference/contexts/delete-a-context.mdx # Delete a context undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Delete a context by its type and id. **type** and **id** are required fields, if the context does not exist, it returns the 404 response file: ./content/docs/api-reference/contexts/list-all-contexts.mdx # List all contexts undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve a paginated list of all contexts, optionally filtered by type and key pattern. **type** and **id** are optional fields, if provided, only contexts with the matching type and id will be returned. **search** is an optional field, if provided, only contexts with the matching key pattern will be returned. Checkout all possible parameters in the query section below for more details file: ./content/docs/api-reference/contexts/retrieve-a-context.mdx # Retrieve a context undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve a specific context by its type and id. **type** and **id** are required fields, if the context does not exist, it returns the 404 response file: ./content/docs/api-reference/contexts/update-a-context.mdx # Update a context undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update the data of an existing context. **type** and **id** are required fields, **data** is required. Only the data field is updated, the rest of the context is not affected. If the context does not exist, it returns the 404 response file: ./content/docs/api-reference/environments/create-an-environment.mdx # Create an environment undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Creates a new environment within the current organization. Environments allow you to manage different stages of your application development lifecycle. Each environment has its own set of API keys and configurations. file: ./content/docs/api-reference/environments/delete-an-environment.mdx # Delete an environment undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Delete an environment by its unique identifier **environmentId**. This action is irreversible and will remove the environment and all its associated data. file: ./content/docs/api-reference/environments/environment-schema.mdx # Environment schema undefined ### Environment Environment is a collection of resources that are used to send notifications. For example, an environment can have a set of integrations, workflows, and subscribers. Read more about environments on [environments concept page](/platform/concepts/environments). file: ./content/docs/api-reference/environments/environment-schema.model.mdx # Environment schema undefined ### Environment Environment is a collection of resources that are used to send notifications. For example, an environment can have a set of integrations, workflows, and subscribers. Read more about environments on [environments concept page](/platform/concepts/environments). \---type-table--- ../api-types.ts#Environment \---end--- file: ./content/docs/api-reference/environments/get-environment-tags.mdx # Get environment tags undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve all unique tags used in workflows within the specified environment. These tags can be used for filtering workflows. file: ./content/docs/api-reference/environments/list-all-environments.mdx # List all environments undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} This API returns a list of environments for the current organization. Each environment contains its configuration, API keys (if user has access), and metadata. file: ./content/docs/api-reference/environments/update-an-environment.mdx # Update an environment undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update an environment by its unique identifier **environmentId**. You can modify the environment name, identifier, color, and other configuration settings. file: ./content/docs/api-reference/events/broadcast-event-to-all.mdx # Broadcast event to all undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Trigger a broadcast event to all existing subscribers, could be used to send announcements, etc. In the future could be used to trigger events to a subset of subscribers based on defined filters. file: ./content/docs/api-reference/events/bulk-trigger-event.mdx # Bulk trigger event undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Using this endpoint you can trigger multiple events at once, to avoid multiple calls to the API. The bulk API is limited to 100 events per request. file: ./content/docs/api-reference/events/cancel-triggered-event.mdx # Cancel triggered event undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Using a previously generated transactionId during the event trigger, will cancel any active or pending workflows. This is useful to cancel active digests, delays etc... file: ./content/docs/api-reference/events/trigger-event.mdx # Trigger event undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Trigger event is the main (and only) way to send notifications to subscribers. The trigger identifier is used to match the particular workflow associated with it. Maximum number of recipients can be 100. Additional information can be passed according the body interface below. To prevent duplicate triggers, you can optionally pass a **transactionId** in the request body. If the same **transactionId** is used again, the trigger will be ignored. The retention period depends on your billing tier. file: ./content/docs/api-reference/integrations/auto-configure-an-integration-for-inbound-webhooks.mdx # Auto-configure an integration for inbound webhooks undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Auto-configure an integration by its unique key identifier **integrationId** for inbound webhook support. This will automatically generate required webhook signing keys and configure webhook endpoints. file: ./content/docs/api-reference/integrations/create-an-integration.mdx # Create an integration undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Create an integration for the current environment the user is based on the API key provided. Each provider supports different credentials, check the provider documentation for more details. file: ./content/docs/api-reference/integrations/delete-an-integration.mdx # Delete an integration undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Delete an integration by its unique key identifier **integrationId**. This action is irreversible. file: ./content/docs/api-reference/integrations/generate-chat-oauth-url.mdx # Generate chat OAuth URL undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Generate an OAuth URL for chat integrations like Slack and MS Teams. This URL allows subscribers to authorize the integration, enabling the system to send messages through their chat workspace. The generated URL expires after 5 minutes. file: ./content/docs/api-reference/integrations/integration-schema.mdx # Integration schema undefined ### Integration Integration is third party service used by Novu to send notification for a specific channel. For example, [sengrid](/platform/integrations/email/sendgrid) is a email integration and [twilio](/platform/integrations/sms/twilio) is a sms integration. Read more about integrations on [integrations concept page](/platform/concepts/integrations). ### Credentials Each integration has a set of credentials that are used to authenticate with the third party service. Checkout the provider documentation for more details on what credentials are required for each integration. ### ChannelTypeEnum ChannelTypeEnum is used to specify the type of channel that the integration is used for. For example, if the integration is used for email, the channel type will be `email`. ```typescript ChannelTypeEnum { IN_APP = "in_app", EMAIL = "email", SMS = "sms", CHAT = "chat", PUSH = "push" } ``` file: ./content/docs/api-reference/integrations/integration-schema.model.mdx # Integration schema undefined ### Integration Integration is third party service used by Novu to send notification for a specific channel. For example, [sengrid](/platform/integrations/email/sendgrid) is a email integration and [twilio](/platform/integrations/sms/twilio) is a sms integration. Read more about integrations on [integrations concept page](/platform/concepts/integrations). \---type-table--- ../api-types.ts#Integration \---end--- ### Credentials Each integration has a set of credentials that are used to authenticate with the third party service. Checkout the provider documentation for more details on what credentials are required for each integration. \---type-table--- ../api-types.ts#Credentials \---end--- ### ChannelTypeEnum ChannelTypeEnum is used to specify the type of channel that the integration is used for. For example, if the integration is used for email, the channel type will be `email`. ```typescript ChannelTypeEnum { IN_APP = "in_app", EMAIL = "email", SMS = "sms", CHAT = "chat", PUSH = "push" } ``` file: ./content/docs/api-reference/integrations/list-active-integrations.mdx # List active integrations undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} List all the active integrations created in the organization file: ./content/docs/api-reference/integrations/list-all-integrations.mdx # List all integrations undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} List all the channels integrations created in the organization file: ./content/docs/api-reference/integrations/update-an-integration.mdx # Update an integration undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update an integration by its unique key identifier **integrationId**. Each provider supports different credentials, check the provider documentation for more details. file: ./content/docs/api-reference/integrations/update-integration-as-primary.mdx # Update integration as primary undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update an integration as **primary** by its unique key identifier **integrationId**. This API will set the integration as primary for that channel in the current environment. Primary integration is used to deliver notification for sms and email channels in the workflow. file: ./content/docs/api-reference/layouts/create-a-layout.mdx # Create a layout undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Creates a new layout in the Novu Cloud environment file: ./content/docs/api-reference/layouts/delete-a-layout.mdx # Delete a layout undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Removes a specific layout by its unique identifier **layoutId** file: ./content/docs/api-reference/layouts/duplicate-a-layout.mdx # Duplicate a layout undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Duplicates a layout by its unique identifier **layoutId**. This will create a new layout with the content of the original layout. file: ./content/docs/api-reference/layouts/generate-layout-preview.mdx # Generate layout preview undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Generates a preview for a layout by its unique identifier **layoutId** file: ./content/docs/api-reference/layouts/get-layout-usage.mdx # Get layout usage undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieves information about workflows that use the specified layout by its unique identifier **layoutId** file: ./content/docs/api-reference/layouts/list-all-layouts.mdx # List all layouts undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieves a list of layouts with optional filtering and pagination file: ./content/docs/api-reference/layouts/retrieve-a-layout.mdx # Retrieve a layout undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Fetches details of a specific layout by its unique identifier **layoutId** file: ./content/docs/api-reference/layouts/update-a-layout.mdx # Update a layout undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Updates the details of an existing layout, here **layoutId** is the identifier of the layout file: ./content/docs/api-reference/messages/delete-a-message.mdx # Delete a message undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Delete a message entity from the Novu platform by **messageId**. This action is irreversible. **messageId** is required and of mongodbId type. file: ./content/docs/api-reference/messages/delete-messages-by-transactionid.mdx # Delete messages by transactionId undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Delete multiple messages from the Novu platform using **transactionId** of triggered event. This API supports filtering by **channel** and delete all messages associated with the **transactionId**. file: ./content/docs/api-reference/messages/list-all-messages.mdx # List all messages undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} List all messages for the current environment. This API supports filtering by **channel**, **subscriberId**, and **transactionId**. This API returns a paginated list of messages. file: ./content/docs/api-reference/messages/message-schema.mdx # Message schema undefined ### Message Message is a single notification that is sent to a subscriber. Each channel step in the workflow generates a message. ### ChannelTypeEnum ```typescript ChannelTypeEnum { IN_APP = "in_app", EMAIL = "email", SMS = "sms", CHAT = "chat", PUSH = "push" } ``` ### Workflow Workflow is a collection of steps that are executed in order to send a notification. ### Actor Actor is the user who is skipped from sending the notification when workflow is triggered to a [topic](/platform/concepts/topics). string) | (() => string)" }, "valueOf": { "description": "", "type": "(() => string) | (() => Object)" } }} /> ### MessageCTA MessageCTA is a call to action that is displayed in the [Inbox](/platform/inbox/overview) message. It can be used to redirect the user to a specific URL when the message is clicked. file: ./content/docs/api-reference/messages/message-schema.model.mdx # Message schema undefined ### Message Message is a single notification that is sent to a subscriber. Each channel step in the workflow generates a message. \---type-table--- ../api-types.ts#Message \---end--- ### ChannelTypeEnum ```typescript ChannelTypeEnum { IN_APP = "in_app", EMAIL = "email", SMS = "sms", CHAT = "chat", PUSH = "push" } ``` ### Workflow Workflow is a collection of steps that are executed in order to send a notification. \---type-table--- ../api-types.ts#Workflow \---end--- ### Actor Actor is the user who is skipped from sending the notification when workflow is triggered to a [topic](/platform/concepts/topics). \---type-table--- ../api-types.ts#Actor \---end--- ### MessageCTA MessageCTA is a call to action that is displayed in the [Inbox](/platform/inbox/overview) message. It can be used to redirect the user to a specific URL when the message is clicked. \---type-table--- ../api-types.ts#MessageCTA \---end--- file: ./content/docs/api-reference/notifications/list-all-events.mdx # List all events undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} List all notification events (triggered events) for the current environment. This API supports filtering by **channels**, **templates**, **emails**, **subscriberIds**, **transactionId**, **topicKey**, **severity**, **contextKeys**. Checkout all available filters in the query section. This API returns event triggers, to list each channel notifications, check messages APIs. file: ./content/docs/api-reference/notifications/notification-event-schema.mdx # Notification event schema undefined ### Notification event Notification event is the event that is generated when a workflow is triggered to subscribers. It contains workflow details, subscriber details payload sent during trigger, execution details of each step in the workflow and the result of the execution of each step. ### Workflow Workflow contains the details of the workflow that was triggered. ### ChannelTypeEnum ChannelTypeEnum is the type of the channel that the notification was sent to. ```typescript ChannelTypeEnum { IN_APP = "in_app", EMAIL = "email", SMS = "sms", CHAT = "chat", PUSH = "push" } ``` file: ./content/docs/api-reference/notifications/notification-event-schema.model.mdx # Notification event schema undefined ### Notification event Notification event is the event that is generated when a workflow is triggered to subscribers. It contains workflow details, subscriber details payload sent during trigger, execution details of each step in the workflow and the result of the execution of each step. \---type-table--- ../api-types.ts#Notification \---end--- ### Workflow Workflow contains the details of the workflow that was triggered. \---type-table--- ../api-types.ts#Workflow \---end--- ### ChannelTypeEnum ChannelTypeEnum is the type of the channel that the notification was sent to. ```typescript ChannelTypeEnum { IN_APP = "in_app", EMAIL = "email", SMS = "sms", CHAT = "chat", PUSH = "push" } ``` file: ./content/docs/api-reference/notifications/retrieve-an-event.mdx # Retrieve an event undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve an event by its unique key identifier **notificationId**. Here **notificationId** is of mongodbId type. This API returns the event details - execution logs, status, actual notification (message) generated by each workflow step. file: ./content/docs/api-reference/subscribers/bulk-create-subscribers.mdx # Bulk create subscribers undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Using this endpoint multiple subscribers can be created at once. The bulk API is limited to 500 subscribers per request. file: ./content/docs/api-reference/subscribers/bulk-update-subscriber-preferences.mdx # Bulk update subscriber preferences undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Bulk update subscriber preferences by its unique key identifier **subscriberId**. This API allows updating multiple workflow preferences in a single request. file: ./content/docs/api-reference/subscribers/create-a-subscriber.mdx # Create a subscriber undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Create a subscriber with the subscriber attributes. **subscriberId** is a required field, rest other fields are optional, if the subscriber already exists, it will be updated file: ./content/docs/api-reference/subscribers/delete-a-subscriber.mdx # Delete a subscriber undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Deletes a subscriber entity from the Novu platform along with associated messages, preferences, and topic subscriptions. **subscriberId** is a required field. file: ./content/docs/api-reference/subscribers/delete-provider-credentials.mdx # Delete provider credentials undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Delete subscriber credentials for a provider such as **slack** and **FCM** by **providerId**. This action is irreversible and will remove the credentials for the provider for particular **subscriberId**. file: ./content/docs/api-reference/subscribers/retrieve-a-subscriber.mdx # Retrieve a subscriber undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve a subscriber by its unique key identifier **subscriberId**. **subscriberId** field is required. file: ./content/docs/api-reference/subscribers/retrieve-subscriber-notifications.mdx # Retrieve subscriber notifications undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve subscriber in-app (inbox) notifications by its unique key identifier **subscriberId**. file: ./content/docs/api-reference/subscribers/retrieve-subscriber-preferences.mdx # Retrieve subscriber preferences undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve subscriber channel preferences by its unique key identifier **subscriberId**. This API returns all five channels preferences for all workflows and global preferences. file: ./content/docs/api-reference/subscribers/retrieve-subscriber-subscriptions.mdx # Retrieve subscriber subscriptions undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve subscriber's topic subscriptions by its unique key identifier **subscriberId**. Checkout all available filters in the query section. file: ./content/docs/api-reference/subscribers/retrieve-unseen-notifications-count.mdx # Retrieve unseen notifications count undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve unseen in-app (inbox) notifications count for a subscriber by its unique key identifier **subscriberId**. file: ./content/docs/api-reference/subscribers/search-subscribers.mdx # Search subscribers undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Search subscribers by their **email**, **phone**, **subscriberId** and **name**. The search is case sensitive and supports pagination.Checkout all available filters in the query section. file: ./content/docs/api-reference/subscribers/subscriber-schema.mdx # Subscriber schema undefined ### Subscriber Subscriber is the end user that receives notifications. Subscriber has subscriber attributes like firstName, lastName, email, phone, etc, `data` field to store any custom attributes in key value pairs and channel credentials for push and chat channel provider's integrations. Read more about subscribers on [subscribers concept page](/platform/concepts/subscribers). ### ChannelSettingsDto ChannelSettings are credentials for push and chat channel provider's integrations. One subscriber can have credentials for multiple integrations of same provider of one channel type ### Credentials Credentials like deviceTokens, webhookUrl, etc for a specific integration. `providerId` could be chat channel providerId or push channel providerId. file: ./content/docs/api-reference/subscribers/subscriber-schema.model.mdx # Subscriber schema undefined ### Subscriber Subscriber is the end user that receives notifications. Subscriber has subscriber attributes like firstName, lastName, email, phone, etc, `data` field to store any custom attributes in key value pairs and channel credentials for push and chat channel provider's integrations. Read more about subscribers on [subscribers concept page](/platform/concepts/subscribers). \---type-table--- ../api-types.ts#Subscriber \---end--- ### ChannelSettingsDto ChannelSettings are credentials for push and chat channel provider's integrations. One subscriber can have credentials for multiple integrations of same provider of one channel type \---type-table--- ../api-types.ts#ChannelSettings \---end--- ### Credentials Credentials like deviceTokens, webhookUrl, etc for a specific integration. `providerId` could be chat channel providerId or push channel providerId. \---type-table--- ../api-types.ts#Credentials \---end--- file: ./content/docs/api-reference/subscribers/update-a-subscriber.mdx # Update a subscriber undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update a subscriber by its unique key identifier **subscriberId**. **subscriberId** is a required field, rest other fields are optional file: ./content/docs/api-reference/subscribers/update-all-notifications-state.mdx # Update all notifications state undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update all subscriber in-app (inbox) notifications state such as read, unread, seen or unseen by **subscriberId**. file: ./content/docs/api-reference/subscribers/update-notification-action-status.mdx # Update notification action status undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update in-app (inbox) notification's action status by its unique key identifier **messageId** and type field **type**. **type** field can be **primary** or **secondary** file: ./content/docs/api-reference/subscribers/update-notifications-state.mdx # Update notifications state undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update subscriber's multiple in-app (inbox) notifications state such as seen, read, unseen or unread by **subscriberId**. **messageId** is of type mongodbId of notifications file: ./content/docs/api-reference/subscribers/update-provider-credentials.mdx # Update provider credentials undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update credentials for a provider such as **slack** and **FCM**. **providerId** is required field. This API creates the **deviceTokens** or replaces the existing ones. file: ./content/docs/api-reference/subscribers/update-subscriber-online-status.mdx # Update subscriber online status undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update the subscriber online status by its unique key identifier **subscriberId** file: ./content/docs/api-reference/subscribers/update-subscriber-preferences.mdx # Update subscriber preferences undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update subscriber preferences by its unique key identifier **subscriberId**. **workflowId** is optional field, if provided, this API will update that workflow preference, otherwise it will update global preferences file: ./content/docs/api-reference/subscribers/upsert-provider-credentials.mdx # Upsert provider credentials undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Upsert credentials for a provider such as **slack** and **FCM**. **providerId** is required field. This API creates **deviceTokens** or appends to the existing ones. file: ./content/docs/api-reference/topics/check-topic-subscriber.mdx # Check topic subscriber undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Check if a subscriber belongs to a certain topic file: ./content/docs/api-reference/topics/create-a-topic.mdx # Create a topic undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Creates a new topic if it does not exist, or updates an existing topic if it already exists. Use ?failIfExists=true to prevent updates. file: ./content/docs/api-reference/topics/create-topic-subscriptions.mdx # Create topic subscriptions undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} This api will create subscription for subscriberIds for a topic. Its like subscribing to a common interest group. if topic does not exist, it will be created. file: ./content/docs/api-reference/topics/delete-a-topic.mdx # Delete a topic undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Delete a topic by its unique key identifier **topicKey**. This action is irreversible and will remove all subscriptions to the topic. file: ./content/docs/api-reference/topics/delete-topic-subscriptions.mdx # Delete topic subscriptions undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Delete subscriptions for subscriberIds for a topic. file: ./content/docs/api-reference/topics/get-a-topic-subscription.mdx # Get a topic subscription undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Get a subscription by its unique identifier for a topic. file: ./content/docs/api-reference/topics/list-all-topics.mdx # List all topics undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} This api returns a paginated list of topics. Topics can be filtered by **key**, **name**, or **includeCursor** to paginate through the list. Checkout all available filters in the query section. file: ./content/docs/api-reference/topics/list-topic-subscriptions.mdx # List topic subscriptions undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} List all subscriptions of subscribers for a topic. Checkout all available filters in the query section. file: ./content/docs/api-reference/topics/retrieve-a-topic.mdx # Retrieve a topic undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve a topic by its unique key identifier **topicKey** file: ./content/docs/api-reference/topics/topic-schema.mdx # Topic schema undefined ### Topic Topic is a collection of subscribers that share a common interest. Subscriber can subscribe to multiple topics. When a subscriber is subscribed to a topic, they will receive notifications generated by workflows triggered to that topic. ### TopicSubscription TopicSubscription is a relationship between a subscriber and a topic. It is used to track which subscribers are subscribed to which topics and when they subscribed. `createdAt` is the date and time the subscription was created. ### Subscriber Subscriber is a user who can receive notifications. Read more about subscribers on [subscribers concept page](/platform/concepts/subscribers). file: ./content/docs/api-reference/topics/topic-schema.model.mdx # Topic schema undefined ### Topic Topic is a collection of subscribers that share a common interest. Subscriber can subscribe to multiple topics. When a subscriber is subscribed to a topic, they will receive notifications generated by workflows triggered to that topic. \---type-table--- ../api-types.ts#Topic \---end--- ### TopicSubscription TopicSubscription is a relationship between a subscriber and a topic. It is used to track which subscribers are subscribed to which topics and when they subscribed. `createdAt` is the date and time the subscription was created. \---type-table--- ../api-types.ts#TopicSubscription \---end--- ### Subscriber Subscriber is a user who can receive notifications. Read more about subscribers on [subscribers concept page](/platform/concepts/subscribers). \---type-table--- ../api-types.ts#Subscriber \---end--- file: ./content/docs/api-reference/topics/update-a-topic-subscription.mdx # Update a topic subscription undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update a subscription by its unique identifier for a topic. You can update the preferences and name associated with the subscription. file: ./content/docs/api-reference/topics/update-a-topic.mdx # Update a topic undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Update a topic name by its unique key identifier **topicKey** file: ./content/docs/api-reference/translations/create-a-translation.mdx # Create a translation undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Create a translation for a specific workflow and locale, if the translation already exists, it will be updated file: ./content/docs/api-reference/translations/delete-a-translation-group.mdx # Delete a translation group undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Delete an entire translation group and all its translations file: ./content/docs/api-reference/translations/delete-a-translation.mdx # Delete a translation undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Delete a specific translation by resource type, resource ID and locale file: ./content/docs/api-reference/translations/import-master-translations-json.mdx # Import master translations JSON undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Import translations for multiple workflows from master JSON format for a specific locale file: ./content/docs/api-reference/translations/retrieve-a-translation-group.mdx # Retrieve a translation group undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieves a single translation group by resource type (workflow, layout) and resource ID (workflowId, layoutId) file: ./content/docs/api-reference/translations/retrieve-a-translation.mdx # Retrieve a translation undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve a specific translation by resource type, resource ID and locale file: ./content/docs/api-reference/translations/retrieve-master-translations-json.mdx # Retrieve master translations JSON undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieve all translations for a locale in master JSON format organized by resourceId (workflowId) file: ./content/docs/api-reference/translations/upload-master-translations-json-file.mdx # Upload master translations JSON file undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Upload a master JSON file containing translations for multiple workflows. Locale is automatically detected from filename (e.g., en\_US.json) file: ./content/docs/api-reference/translations/upload-translation-files.mdx # Upload translation files undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Upload one or more JSON translation files for a specific workflow. Files name must match the locale, e.g. en\_US.json. Supports both "files" and "files\[]" field names for backwards compatibility. file: ./content/docs/api-reference/workflows/create-a-workflow.mdx # Create a workflow undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Creates a new workflow in the Novu Cloud environment file: ./content/docs/api-reference/workflows/delete-a-workflow.mdx # Delete a workflow undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Removes a specific workflow by its unique identifier **workflowId** file: ./content/docs/api-reference/workflows/list-all-workflows.mdx # List all workflows undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieves a list of workflows with optional filtering and pagination file: ./content/docs/api-reference/workflows/retrieve-a-workflow.mdx # Retrieve a workflow undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Fetches details of a specific workflow by its unique identifier **workflowId** file: ./content/docs/api-reference/workflows/retrieve-workflow-step.mdx # Retrieve workflow step undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Retrieves data for a specific step in a workflow file: ./content/docs/api-reference/workflows/sync-a-workflow.mdx # Sync a workflow undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Synchronizes a workflow to the target environment file: ./content/docs/api-reference/workflows/update-a-workflow.mdx # Update a workflow undefined {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} Updates the details of an existing workflow, here **workflowId** is the identifier of the workflow file: ./content/docs/api-reference/workflows/workflow-schema.mdx # Workflow schema undefined ### Workflow In Novu, a workflow defines the logic for delivering notifications based on specific events. It consists of configurable steps that control timing, conditions, and which channels to use, such as email, SMS, or in-app messages. Workflows can be built visually in the dashboard or programmatically using the Novu Framework. Each workflow has a unique identifier, supports environment syncing, and provides real-time visibility through the Activity Feed for monitoring and debugging.. Read more about workflows on [workflows concept page](/platform/concepts/workflows). file: ./content/docs/api-reference/workflows/workflow-schema.model.mdx # Workflow schema undefined ### Workflow In Novu, a workflow defines the logic for delivering notifications based on specific events. It consists of configurable steps that control timing, conditions, and which channels to use, such as email, SMS, or in-app messages. Workflows can be built visually in the dashboard or programmatically using the Novu Framework. Each workflow has a unique identifier, supports environment syncing, and provides real-time visibility through the Activity Feed for monitoring and debugging.. Read more about workflows on [workflows concept page](/platform/concepts/workflows). \---type-table--- ../api-types.ts#Workflow \---end--- file: ./content/docs/framework/content/react-email.mdx # React Email Learn how to use React Email to build beautiful email templates React Email is a collection of high-quality, unstyled components for creating beautiful emails using React and TypeScript. It's a great way to build email templates that are consistent with your brand and easy to maintain. ## Getting Started ```bash npm install @react-email/components react-email ``` ```tsx import { Body, Container, Head, Html, render, } from '@react-email/components'; import * as React from "react"; interface TestEmailProps { name: string } export const TestEmailTemplate = ({ name }: TestEmailProps) => { return ( Hello {name} welcome to your first React E-mail template! ); }; export default TestEmailTemplate; export function renderEmail(name: string) { return render(); } ``` ```tsx import { workflow } from '@novu/framework'; import { renderEmail } from './emails/test-email'; import { z } from 'zod'; export const testWorkflow = workflow('test-workflow', async ({ step, payload }) => { await step.email('send-email', async (controls) => { return { subject: controls.subject, body: renderEmail(payload.userName), }; }, { controlSchema: z.object({ subject: z.string().default('A Successful Test on Novu from {{userName}}'), }), }); }, { payloadSchema: z.object({ userName: z.string().default('John Doe'), }), }); ``` file: ./content/docs/framework/content/remix-react-email.mdx # Remix & React Email Learn how to integrate React Email with Novu Framework in a Remix application Integrating Novu Framework with [React email](https://react.email/) for your Remix application can be done in three steps. If you don't have an app, you can [clone our Remix example](https://github.com/novuhq/novu-framework-remix-example). Install the required React email components. ```bash npm i @react-email/components react-email ``` Create an `emails` folder in the `app` directory of your Remix app. Create a new `sample-email.tsx` file for your email template. ```ts import { Button, Html } from "@react-email/components"; function Email(props) { return ( ); } export function renderEmail(inputs) { return render(); } ``` Define your workflow using the above template ```tsx import { renderEmail } from './sample-email.tsx'; import { workflow } from '@novu/framework'; workflow('new-signup', async ({ step, payload }) => { await step.email('send-email', async (inputs) => { return { subject: `Welcome to Remix and React E-mail`, body: renderEmail(inputs), } }); }); ``` file: ./content/docs/framework/content/svelte-email.mdx # Svelte Email Learn how to use Svelte Email to build beautiful email templates Integrating Novu Framework with [Svelte email](https://react.email/) for your Svelte application can be done in three steps. If you don't have an app, you can [clone our Svelte example](https://github.com/novuhq/novu-svelte-email). Install the required Svelte email components. ```bash npm install svelte-email ``` Create a new folder called `emails` in your `src` folder. Create a new file called `test-email.svelte` in your `emails` folder. ```svelte Welcome to Svelte Email

Welcome, {name}!

Thanks for trying Svelte Email. We're thrilled to have you on board.

``` Create a new file called `test-email.ts` in your `emails` folder. ```typescript import { render } from 'svelte-email'; import TestEmail from './test-email.svelte'; export function renderEmail(name: string) { return render({ template: TestEmail, props: { name, }, }); } ```
Define your workflow using the above template ```typescript import { workflow } from '@novu/framework'; import { renderEmail } from './emails/test-email'; import { z } from 'zod'; export const testWorkflow = workflow('test-workflow', async ({ step, payload }) => { await step.email('send-email', async (controls) => { return { subject: controls.subject, body: renderEmail(payload.userName), }; }, { controlSchema: z.object({ subject: z.string().default('A Successful Test on Novu from {{userName}}'), }), }); }, { payloadSchema: z.object({ userName: z.string().default('John Doe'), }), }); ```
file: ./content/docs/framework/content/vue-email.mdx # Vue Email Learn how to use Vue Email to build beautiful email templates You can integrate Novu Framework with [Vue Email](https://vuemail.net/) in a few simple steps. This guide will walk you through the process of creating a new email template using Vue Email and Nuxt. For a Quickstart Boilerplate project using Nuxt.js, and Vue Email, check out the [Vue Email Starter repository](https://github.com/novuhq/novu-framework-nuxt-example/) ## Quickstart ```bash npm install @vue-email/components ``` Create a new folder called `emails` in your `src` folder. ```typescript export default defineNuxtConfig({ build: { transpile: ['@vue/email'], }, nitro: { esbuild: { options: { target: 'esnext', }, }, }, }); ``` ```vue ``` ```typescript import { workflow } from '@novu/framework'; import { renderEmail } from './emails/test-email'; import { z } from 'zod'; export const testWorkflow = workflow('test-workflow', async ({ step, payload }) => { await step.email('send-email', async (controls) => { return { subject: controls.subject, body: renderEmail(payload.userName), }; }, { controlSchema: z.object({ subject: z.string().default('A Successful Test on Novu from {{userName}}'), }), }); }, { payloadSchema: z.object({ userName: z.string().default('John Doe'), }), }); ``` ## Learn More To learn more, refer to [Vue Email documentation](https://vuemail.net/). file: ./content/docs/framework/deployment/actions.mdx # GitHub Actions undefined Learn how to deploy your Novu workflows with our built-in GitHub Action command: ```yaml name: Deploy workflow State to Novu on: workflow_dispatch: push: branches: - main jobs: deploy: runs-on: ubuntu-latest steps: # https://github.com/novuhq/actions-novu-sync - name: Sync State to Novu uses: novuhq/actions-novu-sync@v2 with: # The secret key used to authenticate with Novu Cloud # To get the secret key, go to https://web.novu.co/api-keys. # Required. secret-key: ${{ secrets.NOVU_SECRET_KEY }} # The publicly available endpoint hosting the bridge application # where notification entities (eg. workflows, topics) are defined. # Required. bridge-url: ${{ secrets.NOVU_BRIDGE_URL }} # The Novu Cloud API URL to sync with. # Optional. # Defaults to https://api.novu.co api-url: https://api.novu.co ``` file: ./content/docs/framework/deployment/cli.mdx # CLI undefined import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; The Novu CLI provides a mechanism for you to synchronize your workflows into Novu Cloud so that non-technical Novu users can control the workflow and Step-level controls and enable your workflows to be triggered via Novu API. ```bash npx novu@latest sync \ --bridge-url \ --secret-key \ --api-url https://api.novu.co ``` ```bash npx novu@latest sync \ --bridge-url \ --secret-key \ --api-url https://eu.api.novu.co ``` * If your application api server is running at URL **[https://api.domain.com](https://api.domain.com)** and **/api/novu** endpoint is serving Novu workflows created using Novu Framework, then ``in above command will be `https://api.domain.com/api/novu` * If your application is running in local machine with a local studio server, the tunnel URL can be used as ``. The tunnel URL follows the format of `https://.novu.sh/api/novu`, and example is `https://041e553c-0dbf-47e0-8ffa-c4536f390145.novu.sh/api/novu`. In this example `041e553c-0dbf-47e0-8ffa-c4536f390145` is the unique tunnel identifier which is generated for each Novu user when starting a tunnel via `npx novu@latest dev`. The tunnel identifier is persisted to your local machine to guarantee the same tunnel URL each time you invoke `npx novu@latest dev`. ## Using vercel preview url In free tier, vercel preview urls for non production deployments are not publicly accessible. You will need to enable [Protection Bypass for Automation](https://vercel.com/docs/security/deployment-protection/methods-to-bypass-deployment-protection/protection-bypass-automation#protection-bypass-for-automation) from settings to make the preview url publicly accessible. Use vercel generated secret key in query params with bridge url to make the bridge url accessible to novu. Example: `https://my-app-preview-url.vercel.app/api/novu?x-vercel-protection-bypass=BYPASS_SECRET_KEY` file: ./content/docs/framework/deployment/production.mdx # Production Deployment Guide Learn how to deploy your Novu Framework application to production including networking, security, and HMAC verification setup. ## Networking Novu Cloud workers will need to be able to communicate with your [Bridge Endpoint](/framework/endpoint). You will need to ensure that your firewall rules allow traffic from the internet. Due to the autoscaling nature of Novu Cloud, we don't have a set of IP Addresses that you can whitelist. ## Security Novu Cloud workers are GDPR, SOC2 type II and ISO 27001 compliant. We take security very seriously and have implemented a number of security measures to ensure that your data is safe. Novu Framework has a builtin security mechanism that ensures that the requests are authentic from Novu Cloud using an HMAC signature. HMAC Verification is turned on by default for all "production" NODE\_ENV environments For `NODE_ENV=development` the HMAC validation is turned off, for the Studio to be able to reach your endpoint. The `Novu-Signature` header included in each signed event contains a timestamp and one or more signatures that we verify. The timestamp is prefixed by `t=`, and each signature is prefixed by a scheme. Schemes start with `v`, followed by an integer. Currently, the only valid live signature scheme is v1. {/* todo add an example of the x-novu-signature header here */} Handling the signature verification is done by the Novu Framework, so you don't need to perform any action. file: ./content/docs/framework/deployment/syncing.mdx # Syncing undefined Novu operates in a multi environment setup, with the currently available environments: * **Local Studio** - Running against your local machine, this is where you can create, edit, and preview workflows. * **Development** - Acts as a Staging environment, where your non-technical peers can view and modify controls. * **Production** - For triggering workflows to your customers. ## Sync changes to Novu Cloud Novu Framework operates in a GitOps model. This means that the source of truth for your workflows and configurations are located in your Git as Code. The general workflow for pushing changes to Novu Cloud is as follows: * Create a feature branch * Develop workflows locally in your bridge application * Sync changes to the Development environment to test e2e * Merge the feature branch to your `dev` branch * This will trigger a CI/CD pipeline that will deploy the changes to the Development environment * Test the changes in the Development environment * Merge the `dev` branch to the `main` branch * This will trigger a CI/CD pipeline that will deploy the changes to the Production environment ## CI/CD Integrations Novu currently supports the following CI integrations: * **GitHub Actions** - [Direct Integration](/framework/deployment/actions) * **GitLab CI** - Using our [CLI command](/framework/deployment/cli) * **Jenkins** - Using our [CLI command](/framework/deployment/cli) * **CircleCI** - Using our [CLI command](/framework/deployment/cli) * **Bitbucket Pipelines** - Using our [CLI command](/framework/deployment/cli) * **Azure DevOps** - Using our [CLI command](/framework/deployment/cli) * **Travis CI** - Using our [CLI command](/framework/deployment/cli) * **Other** - For any other CI/CD tool, you can use our [CLI command](/framework/deployment/cli) Direct integration with other CI/CD tools is on our roadmap. If you would like to see a specific CI/CD tool integrated, please reach out to us. file: ./content/docs/framework/quickstart/express.mdx # Express Get started with Novu Framework in an Express application import DeployApp from '@/snippets/quickstart/deploy.mdx'; import NextStepsStep from '@/snippets/quickstart/next-steps.mdx'; import { PackagesStep } from '@/snippets/quickstart/packages.tsx'; import { SecretStep } from '@/snippets/quickstart/secret.tsx'; import { StudioStep } from '@/snippets/quickstart/studio.tsx'; import { TestStep } from '@/snippets/quickstart/test.tsx'; import { WorkflowStep } from '@/snippets/quickstart/workflow.tsx'; In this guide, we will add a Novu [Bridge Endpoint](/platform/concepts/endpoint) to a Express.js application and send our first test workflow. ### Set up your local environment ### Install packages ### Add a Novu API Endpoint ```typescript app/server/api/novu.ts import { serve } from "@novu/framework/express"; import { testWorkflow } from "../novu/workflows"; app.use(express.json()); // Required for Novu POST requests app.use( "/api/novu", serve({ workflows: [testWorkflow] }) ); ``` ### Configure your secret key ### Create your workflow definition Add a `novu` folder in your app folder as such `novu/workflows.ts` that will contain your workflow definitions. ### Start your application Start your Express server with the Novu Endpoint configured. If your Express application is running on other than `4000` port, restart the `npx novu dev` command with the port: ```tsx npx novu@latest dev --port ``` ### Test your endpoint ### Deploy your application file: ./content/docs/framework/quickstart/h3.mdx # H3 Get started with Novu Framework in an H3 application import DeployApp from '@/snippets/quickstart/deploy.mdx'; import NextStepsStep from '@/snippets/quickstart/next-steps.mdx'; import { PackagesStep } from '@/snippets/quickstart/packages.tsx'; import { SecretStep } from '@/snippets/quickstart/secret.tsx'; import { StudioStep } from '@/snippets/quickstart/studio.tsx'; import { TestStep } from '@/snippets/quickstart/test.tsx'; import { WorkflowStep } from '@/snippets/quickstart/workflow.tsx'; In this guide, we will add a Novu [Bridge Endpoint](/platform/concepts/endpoint) to a H3 application and send our first test workflow. ### Set up your local environment ### Install packages ### Add a Novu API Endpoint ```typescript app/server/api/novu.ts import { createApp, eventHandler, toNodeListener } from "h3"; import { serve } from "@novu/framework/h3"; import { createServer } from "node:http"; import { testWorkflow } from "./novu/workflows"; const app = createApp(); app.use("/api/novu", eventHandler(serve({ workflows: [testWorkflow] }) )); createServer(toNodeListener(app)).listen(4000); ``` ### Configure your secret key ### Create your workflow definition Add a `novu` folder in your app folder as such `novu/workflows.ts` that will contain your workflow definitions. ### Start your application Start your H3 server with the Novu Endpoint configured. If your H3 application is running on other than `4000` port, restart the `npx novu dev` command with the port: ```tsx npx novu@latest dev --port ``` ### Test your endpoint ### Deploy your application file: ./content/docs/framework/quickstart/lambda.mdx # AWS Lambda Get started with Novu Framework in an AWS Lambda function import DeployApp from '@/snippets/quickstart/deploy.mdx'; import NextStepsStep from '@/snippets/quickstart/next-steps.mdx'; import { PackagesStep } from '@/snippets/quickstart/packages.tsx'; import { SecretStep } from '@/snippets/quickstart/secret.tsx'; import { StudioStep } from '@/snippets/quickstart/studio.tsx'; import { TestStep } from '@/snippets/quickstart/test.tsx'; import { WorkflowStep } from '@/snippets/quickstart/workflow.tsx'; In this guide, we will add a Novu [Bridge Endpoint](/platform/concepts/endpoint) to a AWS Lambda application and send our first test workflow. ### Set up your local environment ### Install packages ### Add a Novu API Endpoint ```typescript src/functions/api/novu.ts import { serve } from "@novu/framework/lambda"; import { workflow } from "@novu/framework"; import { testWorkflow } from "../novu/workflows"; module.exports.novu = serve({ workflows: [testWorkflow], }); ``` ### Configure your secret key ### Create your workflow definition Add a `novu` folder in your app folder as such `novu/workflows.ts` that will contain your workflow definitions. ### Start your application Start your AWS Lambda server with the Novu Endpoint configured. If your Local Lambda application is running on other than `4000` port, restart the `npx novu dev` command with the port: ```tsx npx novu@latest dev --port ``` ### Test your endpoint ### Deploy your application file: ./content/docs/framework/quickstart/nestjs.mdx # NestJS Get started with Novu Framework in a NestJS application import DeployApp from '@/snippets/quickstart/deploy.mdx'; import NextStepsStep from '@/snippets/quickstart/next-steps.mdx'; import { PackagesStep } from '@/snippets/quickstart/packages.tsx'; import { SecretStep } from '@/snippets/quickstart/secret.tsx'; import { StudioStep } from '@/snippets/quickstart/studio.tsx'; import { TestStep } from '@/snippets/quickstart/test.tsx'; import { WorkflowStep } from '@/snippets/quickstart/workflow.tsx'; In this guide, we will add a Novu [Bridge Endpoint](/platform/concepts/endpoint) to a NestJS application and send our first test workflow. ### Set up your local environment ### Install packages The `NovuModule` is a NestJS module that registers the Novu Endpoint in your application. The following example does not support NestJS dependency injection. If you need to `@Injectable` dependencies in your workflow definition, see [Advanced Usage](#advanced-usage-dependency-injection). ```typescript src/app.module.ts import { Module } from '@nestjs/common'; import { NovuModule } from '@novu/framework/nest'; import { testWorkflow } from './novu/workflows'; @Module({ imports: [ NovuModule.register({ apiPath: '/api/novu', workflows: [testWorkflow], }), ], }) export class AppModule {} ``` ### Configure your secret key ### Create your workflow definition Add a `novu` folder in your `src` folder as such `src/novu/workflows.ts` that will contain your workflow definitions. ### Start your application Start your NestJS application with the Novu Endpoint configured. If your NestJS application is running on other than `4000` port, restart the `npx novu dev` command with the port: ```tsx npx novu@latest dev --port ``` ### Test your endpoint ### Deploy your application ## Advanced Usage (Dependency Injection) If you need to inject dependencies into your workflow definition, you can use the `registerAsync` method. Add the `NovuModule` using the `registerAsync` method to your `AppModule`. ```typescript src/app.module.ts import { Module } from '@nestjs/common'; import { NovuModule } from '@novu/framework/nest'; import { NotificationService } from './notification.service'; import { UserService } from './user.service'; @Module({ imports: [ NovuModule.registerAsync({ imports: [AppModule], useFactory: (notificationService: NotificationService) => ({ apiPath: '/api/novu', workflows: [notificationService.welcomeWorkflow()], }), inject: [NotificationService], }), ], providers: [NotificationService, UserService], exports: [NotificationService], }) export class AppModule {} ``` For example, you might need to inject a service that fetches the user's name from a database. This is useful when you need to fetch data in realtime during the execution of your workflow. An example `UserService` is available below with hardcoded values, but in a real-world application you might use a database or an external API to fetch the user's name. ```typescript src/user.service.ts import { Injectable } from '@nestjs/common'; @Injectable() export class UserService { getUser(id: string) { return { name: 'John Doe', email: `john.doe.${id}@example.com`, }; } } ``` Finally, configure your `NotificationService` to use the injected `UserService`. ```typescript src/notification.service.ts import { Injectable } from '@nestjs/common'; import { workflow } from '@novu/framework'; import { z } from 'zod'; import { UserService } from './user.service'; @Injectable() export class NotificationService { constructor(private readonly userService: UserService) {} public welcomeWorkflow() { return workflow( 'welcome-email', async ({ step, payload }) => { await step.email('send-email', async () => { const user = this.userService.getUser(payload.userId); return { subject: `Hello, ${user.name}`, body: `We are glad you are here!`, }; }); }, { payloadSchema: z.object({ userId: z.string(), }), } ); } } ``` A full example NestJS application demonstrating dependency injection is available [here](https://github.com/novuhq/novu/tree/next/playground/nestjs). file: ./content/docs/framework/quickstart/nextjs.mdx # Next.js Get started with Novu Framework in a Next.js application import DeployApp from '@/snippets/quickstart/deploy.mdx'; import NextStepsStep from '@/snippets/quickstart/next-steps.mdx'; import { PackagesStep } from '@/snippets/quickstart/packages.tsx'; import { SecretStep } from '@/snippets/quickstart/secret.tsx'; import { StudioStep } from '@/snippets/quickstart/studio.tsx'; import { TestStep } from '@/snippets/quickstart/test.tsx'; import { WorkflowStep } from '@/snippets/quickstart/workflow.tsx'; import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; In this guide, we will add a Novu [Bridge Endpoint](/platform/concepts/endpoint) to a Next.js application and send our first test workflow. This link can be copied right from the onboarding guide on the Novu Studio or can always be copied from the [API Keys](https://dashboard.novu.co/api-keys) page on the Novu Dashboard. ```bash npx novu init --secret-key= ``` The sample application will create an `.env` file containing the `NOVU_SECRET_KEY` environment variable required for securing your endpoint. And a sample workflow demonstrating the flexibility of Novu using Step Controls. **Install required packages** ```bash npm install @novu/framework @react-email/components react-email zod zod-to-json-schema ``` This will install * **`@novu/framework`** SDK Package * **React Email** (Recommended) - For writing your email templates with React * **Zod** (Recommended) - For end-to-end type safety for your Payload and Step Controls **Add a Novu API Endpointt** ```typescript App Router (app/api/novu/route.ts) import { serve } from "@novu/framework/next"; import { myWorkflow } from "../../novu/workflows"; export const { GET, POST, OPTIONS } = serve({ workflows: [myWorkflow] }); ``` ```typescript Pages Router (pages/api/novu.ts) import { serve } from '@novu/framework/next'; import { testWorkflow } from '../../novu/workflows'; export default serve({ workflows: [testWorkflow] }); ``` **Add a Novu Secret Key Environment Variable** Add `NOVU_SECRET_KEY` environment variable to your `.env` ```bash NOVU_SECRET_KEY= ``` **Create your workflow definition** Add a `novu` folder that will contain your workflow definitions ```tsx app/novu/workflows.ts import { workflow } from '@novu/framework'; import { renderEmail } from './emails/test-email'; import { z } from 'zod'; export const testWorkflow = workflow('test-workflow', async ({ step, payload }) => { await step.email('send-email', async (controls) => { return { subject: controls.subject, body: renderEmail(payload.userName), }; }, { controlSchema: z.object({ subject: z.string().default('A Successful Test on Novu from {{userName}}'), }), }); }, { payloadSchema: z.object({ userName: z.string().default('John Doe'), }), }); ``` **Create your React Email Template (Optional)** Add a new email template ```typescript app/novu/emails/test-email.tsx import { Body, Container, Head, Html, render, } from '@react-email/components'; import * as React from "react"; interface TestEmailProps { name: string } export const TestEmailTemplate = ({ name }: TestEmailProps) => { return ( Hello {name} welcome to your first React E-mail template! ); }; export default TestEmailTemplate; export function renderEmail(name: string) { return render(); } ``` ### Start your application To start your boilerplate Next.js server with the Novu Endpoint configured, run the following command: ```tsx cd my-novu-app && npm run dev ``` The sample application will start on [`https://localhost:4000`](https://localhost:4000) and your novu endpoint will be exposed at `/api/novu` served by the Next.js API. If your Next.js application is running on other than `4000` port, restart the `novu dev` command with the port: ```tsx npx novu@latest dev --port ``` ### Test your endpoint ### Deploy your application file: ./content/docs/framework/quickstart/nuxt.mdx # Nuxt Get started with Novu Framework in a Nuxt application import DeployApp from '@/snippets/quickstart/deploy.mdx'; import NextStepsStep from '@/snippets/quickstart/next-steps.mdx'; import { PackagesStep } from '@/snippets/quickstart/packages.tsx'; import { SecretStep } from '@/snippets/quickstart/secret.tsx'; import { StudioStep } from '@/snippets/quickstart/studio.tsx'; import { TestStep } from '@/snippets/quickstart/test.tsx'; import { WorkflowStep } from '@/snippets/quickstart/workflow.tsx'; In this guide, we will add a Novu [Bridge Endpoint](/platform/concepts/endpoint) to a Nuxt application and send our first test workflow. ### Set up your local environment ### Install packages ### Add a Novu API Endpoint ```typescript app/server/api/novu.ts import { serve } from '@novu/framework/nuxt'; import { testWorkflow } from "../novu/workflows"; export default defineEventHandler(serve({ workflows: [myWorkflow] })); ``` ### Configure your secret key ### Create your workflow definition Add a `novu` folder in your app folder as such `app/server/api/novu.ts` that will contain your workflow definitions. ### Start your application Start your Nuxt application with the Novu Endpoint configured. ```bash cd my-novu-app && npm run dev ``` If your Nuxt application is running on other than `4000` port, restart the `npx novu dev` command with the port: ```bash npx novu@latest dev --port ``` ### Test your endpoint ### Deploy your application file: ./content/docs/framework/quickstart/remix.mdx # Remix Get started with Novu Framework in a Remix application import DeployApp from '@/snippets/quickstart/deploy.mdx'; import NextStepsStep from '@/snippets/quickstart/next-steps.mdx'; import { PackagesStep } from '@/snippets/quickstart/packages.tsx'; import { SecretStep } from '@/snippets/quickstart/secret.tsx'; import { StudioStep } from '@/snippets/quickstart/studio.tsx'; import { TestStep } from '@/snippets/quickstart/test.tsx'; import { WorkflowStep } from '@/snippets/quickstart/workflow.tsx'; In this guide, we will add a Novu [Bridge Endpoint](/platform/concepts/endpoint) to a Remix application and send our first test workflow. ### Set up your local environment ### Install packages ### Add a Novu API Endpoint This guide is based on Remix Offical [Quick Start](https://remix.run/docs/en/main/start/quickstart). ```typescript app/routes/api.novu.ts import { serve } from "@novu/framework/remix"; import { testWorkflow } from "../novu/workflows"; const handler = serve({ workflows: [testWorkflow] }); export { handler as action, handler as loader }; ``` ### Configure your secret key ### Create your workflow definition Add a `novu` folder in your app folder as such `app/novu/workflows.ts` that will contain your workflow definitions. ### Start your application To start your Remix server with the Novu Endpoint configured, run the following command: ```bash cd my-novu-app && npm run dev ``` Remix application default port is 5173. For that to work, restart Novu Studio and point it to the right port: ```bash npx novu@latest dev --port ``` ### Test your endpoint ### Deploy your application file: ./content/docs/framework/quickstart/svelte.mdx # SvelteKit Get started with Novu Framework in a SvelteKit application import DeployApp from '@/snippets/quickstart/deploy.mdx'; import NextStepsStep from '@/snippets/quickstart/next-steps.mdx'; import { PackagesStep } from '@/snippets/quickstart/packages.tsx'; import { SecretStep } from '@/snippets/quickstart/secret.tsx'; import { StudioStep } from '@/snippets/quickstart/studio.tsx'; import { TestStep } from '@/snippets/quickstart/test.tsx'; import { WorkflowStep } from '@/snippets/quickstart/workflow.tsx'; In this guide, we will add a Novu [Bridge Endpoint](/platform/concepts/endpoint) to a Svelte application and send our first test workflow. ### Set up your local environment ### Install packages ### Add a Novu API Endpoint ```typescript src/routes/api/novu/+server.ts import { testWorkflow } from '$lib/novu/workflows'; import { serve } from '@novu/framework/sveltekit'; export const { GET, POST, OPTIONS } = serve({ workflows: [testWorkflow] }); ``` ### Configure your secret key ### Create your workflow definition Add a `novu` folder in your lib folder as such `src/lib/novu/workflows.ts` that will contain your workflow definitions. ### Start your application To start your Svelte server with the Novu Endpoint configured, run the following command: ```tsx cd my-novu-app && npm run dev ``` Svelte application default port is 5173. For that to work, restart Novu Studio and point it to the right port: ```tsx npx novu@latest dev --port ``` ### Test your endpoint ### Deploy your application file: ./content/docs/framework/schema/class-validator.mdx # Class Validator Integrate Class Validator with your notification workflows Novu Framework allows you to use [Class Validator](https://www.npmjs.com/package/class-validator) to define the [Control](/framework/controls) and [Payload](/framework/payload) schemas for your workflows. ### Add class validator to your project ```bash npm install class-validator class-validator-jsonschema reflect-metadata ``` Novu requires the `class-validator-jsonschema` package to generate JSON schemas from your DTOs. You may also need the `reflect-metadata` package. After installation, the Class Validator DTOs can be used interchangeably with the `controlSchema` and `payloadSchema` options in your workflow definitions. ```tsx import { workflow } from '@novu/framework'; import { IsString, IsBoolean, IsNotEmpty, IsOptional, Type, NestedValidation } from "class-validator"; class TestComponent { @IsString() subject: string; @IsString() content: string; } class TestControlSchema { @IsBoolean() hideBanner: boolean; @IsString() @IsNotEmpty() @IsOptional() subject?: string; // Allowing no code control over the component in the Dashboard UI @Type(() => NewSignUpComponent) @NestedValidation({ each: true }) @IsOptional() components?: NewSignUpComponent[]; } class TestPayloadSchema { @IsString() username: string; } export const testWorkflow = workflow('test-workflow', async ({ step, payload }) => { await step.email('send-email', async (controls) => { return { subject: controls.subject, body: 'Hello, World!', }; }, { controlSchema: TestControlSchema, }); }, { payloadSchema: TestPayloadSchema, }); ``` ### Controls and Payload UI When you define a `controlSchema` for a step, Novu will automatically generate a UI for the controls in the workflow editor. * **Form Input Title** - Will be derived from the key of the Class Validator schema. Unfortunately Class Validator does not support custom titles at this point. * **Form Input Type** - Will be derived from the Class Validator schema type, with support for `string`, `number`, `boolean`, and `enum` and `array` types. * **Default Value** - Unfortunately Class Validator does not support default values at this point. * **Validation** - Will be derived from the Class Validator schema validation decorators, including `@Min`, `@Max`, `@IsEmail`, `@IsUrl` and etc... file: ./content/docs/framework/schema/json-schema.mdx # JSON Schema Learn how to use JSON Schema to define the workflow payload and step inputs JSON Schema can be used to define the [workflow payload](/framework/payload) and [step inputs](/framework/steps). It provides a strongly-typed way to define the structure of the data that is expected by the workflow or Step. And also as a contract for changing the workflow behaviour using the Platform User Interface. Learn more about JSON schema at [json-schema.org](https://json-schema.org/). ## Examples ### Simple ```json { "type": "object", "required": ["firstName", "lastName"], "properties": { "firstName": { "type": "string", "title": "First name", "default": "Chuck" }, "lastName": { "type": "string", "title": "Last name" }, "age": { "type": "integer", "title": "Age" } } } ``` ### Nested array structure ```json { "type": "object", "required": ["title"], "properties": { "title": { "type": "string", "title": "Task list title" }, "tasks": { "type": "array", "title": "Tasks", "items": { "type": "object", "required": ["title"], "properties": { "title": { "type": "string", "title": "Title", "description": "A sample title" }, "details": { "type": "string", "title": "Task details", "description": "Enter the task details" }, "done": { "type": "boolean", "title": "Done?", "default": false } } } } } } ``` ### Reference and reuse blocks ```json { "definitions": { "address": { "type": "object", "properties": { "street_address": { "type": "string" }, "city": { "type": "string" }, "state": { "type": "string" } }, "required": ["street_address", "city", "state"] }, "node": { "type": "object", "properties": { "name": { "type": "string" }, "children": { "type": "array", "items": { "$ref": "#/definitions/node" } } } } }, "type": "object", "properties": { "billing_address": { "title": "Billing address", "$ref": "#/definitions/address" }, "shipping_address": { "title": "Shipping address", "$ref": "#/definitions/address" }, "tree": { "title": "Recursive references", "$ref": "#/definitions/node" } } } ``` ### Any of schemas ```json { "type": "object", "properties": { "age": { "type": "integer", "title": "Age" }, "items": { "type": "array", "items": { "type": "object", "anyOf": [ { "properties": { "foo": { "type": "string" } } }, { "properties": { "bar": { "type": "string" } } } ] } } }, "anyOf": [ { "title": "First method of identification", "properties": { "firstName": { "type": "string", "title": "First name", "default": "Chuck" }, "lastName": { "type": "string", "title": "Last name" } } }, { "title": "Second method of identification", "properties": { "idCode": { "type": "string", "title": "ID code" } } } ] } ``` ### One of schema ```json { "type": "object", "oneOf": [ { "properties": { "lorem": { "type": "string" } }, "required": ["lorem"] }, { "properties": { "ipsum": { "type": "string" } }, "required": ["ipsum"] } ] } ``` ### If then else ```json { "type": "object", "properties": { "animal": { "enum": ["Cat", "Fish"] } }, "allOf": [ { "if": { "properties": { "animal": { "const": "Cat" } } }, "then": { "properties": { "food": { "type": "string", "enum": ["meat", "grass", "fish"] } }, "required": ["food"] } }, { "if": { "properties": { "animal": { "const": "Fish" } } }, "then": { "properties": { "food": { "type": "string", "enum": ["insect", "worms"] }, "water": { "type": "string", "enum": ["lake", "sea"] } }, "required": ["food", "water"] } }, { "required": ["animal"] } ] } ``` ### Enum objects ```json { "definitions": { "locations": { "enumNames": ["New York", "Amsterdam", "Hong Kong"], "enum": [ { "name": "New York", "lat": 40, "lon": 74 }, { "name": "Amsterdam", "lat": 52, "lon": 5 }, { "name": "Hong Kong", "lat": 22, "lon": 114 } ] } }, "type": "object", "properties": { "location": { "title": "Location", "$ref": "#/definitions/locations" }, "locationRadio": { "title": "Location Radio", "$ref": "#/definitions/locations" }, "multiSelect": { "title": "Locations", "type": "array", "uniqueItems": true, "items": { "$ref": "#/definitions/locations" } }, "checkboxes": { "title": "Locations Checkboxes", "type": "array", "uniqueItems": true, "items": { "$ref": "#/definitions/locations" } } } } ``` ### Regex validation The following example matches a simple North American telephone number with an optional area code: ```json { "type": "object", "properties": { "phone": { "type": "string", "pattern": "^(\\([0-9]{3}\\))?[0-9]{3}-[0-9]{4}$" } } } ``` ## Other resources * [Examples](https://json-schema.org/learn/miscellaneous-examples) * [React JSON schema](https://rjsf-team.github.io/react-jsonschema-form/) * [JSON schema validator](https://www.jsonschemavalidator.net/) * [JSON schema lint](https://jsonschemalint.com/) file: ./content/docs/framework/schema/zod.mdx # Zod Learn how to integrate Zod with Novu Framework Novu Framework allows you to use [Zod](https://zod.dev/) to define the [Control](/framework/controls) and [Payload](/framework/payload) schemas for your workflows. *(Supports Zod v3)* ## Add Zod to your project ```bash npm install zod ``` Novu Framework supports Zod v3. Make sure you're using this version for optimal performance and feature support. ```typescript import { workflow } from '@novu/framework'; import { z } from 'zod'; export const testWorkflow = workflow('test-workflow', async ({ step, payload }) => { await step.email('send-email', async (controls) => { return { subject: controls.subject, body: 'Hello World', }; }, { controlSchema: z.object({ subject: z.string().default('A Successful Test on Novu from {{userName}}'), }), }); }, { payloadSchema: z.object({ userName: z.string().default('John Doe'), }), }); ``` ## Controls and Payload UI When you define a `controlSchema` for a step, Novu will automatically generate a UI for the controls in the workflow editor. * **Form Input Title** - Will be derived from the key of the Zod schema. Unfortunately Zod does not support custom titles at this point. * **Form Input Type** - Will be derived from the Zod schema type, with support for `string`, `number`, `boolean`, and `enum` and `array` types. * **Default Value** - Will be derived from the Zod schema default value. * **Validation** - Will be derived from the Zod schema validation rules, including `min`, `max`, `email`, `url`, `regex` and etc... file: ./content/docs/framework/typescript/client.mdx # Client Learn how to configure and use the Novu Framework Client for managing global settings The `Client` is an optional Class you can pass to the `serve` function to override some global settings. By default, we will inject a new instance of the `Client` class in your `serve` method with the following defaults: ## Client Interface ### secretKey * **Type**: `string` * **Default**: `process.env.NOVU_SECRET_KEY` * **Description**: Your Novu Secret Key, used to sign the HMAC header to guarantee the authenticity of our requests. ### strictAuthentication * **Type**: `boolean` * **Default**: `process.env.NODE_ENV !== 'development'` * **Description**: This bypasses the HMAC signature verification, required for local development and testing against [Local Studio](/framework/studio). ## Environment Variables Unless specified in the `Client` constructor the `Client` class will look for the following environment variables: * `NOVU_SECRET_KEY` - Your Novu Secret Key * `NOVU_API_URL` - Defaults to `https://api.novu.co`. For EU customers, this should be set to `https://eu.api.novu.co`. ## Development Environment When your service is running in development mode `process.env.NODE_ENV=development`, the following rules will auto apply: * `strictAuthentication` will be set to `false`. ## Code Example ```tsx import { Client as NovuFrameworkClient } from '@novu/framework'; import { serve } from '@novu/framework/next'; import { passwordResetWorkflow } from './workflows'; export const { GET, POST, OPTIONS } = serve({ client: new NovuFrameworkClient({ secretKey: process.env.NOVU_SECRET_KEY, strictAuthentication: false, }), workflows: [ /* all workflows */ passwordResetWorkflow, ], }); ``` file: ./content/docs/framework/typescript/overview.mdx # Overview Learn how to use Novu's TypeScript SDK to build type-safe notification workflows with advanced features like payload validation and step controls. Although you can trigger Novu workflows from any programming language using our Rest API SDKs. We believe that the best way to build your notification strategy is to treat your templates and workflows as your Notification Design System. Building reusable components to be consumed and embedded by your non-technical peers in any combination. Typescript SDKs enable the creation of stunning channel content like E-mails using modern technologies like React/Vue/etc... Treating your emails as a front-end concern opens up a world of possibilities to reuse design tokens, components, and even entire templates across your applications for consistent branding and a cohesive user experience. Novu Framework was built and optimized with extreme focus on Developer Experience. Our `@novu/framework` SDK is written in Typescript, and we recommend using Typescript for your own projects as well. ## Type-safe workflow payloads When defining a [workflow payload](/framework/payload) schema, our SDK will automatically infer it to a Typescript interface. ```tsx import { workflow } from '@novu/framework'; const myWorkflow = workflow( 'new-signup', async ({ step, payload }) => { await step.email('send-email', () => { return { subject: 'Hello World', // The payload object here is type-safe body: `Hi ${payload.name}, welcome to our platform!`, }; }); }, { payloadSchema: { properties: { name: { type: 'string' } } }, } ); ``` ## Type safe steps Similarly, when defining a [step](/framework/typescript/steps) schema, our SDK will automatically infer it to a Typescript interface. ## Step controls Build and define type safe controls to expose no-code editing capabilities to your teammates. ## Explore the SDK * [Client](/framework/typescript/client) * [Workflow](/framework/typescript/workflow) * [Steps](/framework/typescript/steps) The `@novu/framework` SDK is compatible with Node.js version 20.0.0 and above. file: ./content/docs/framework/typescript/workflow.mdx # Workflow Learn about the Novu Framework workflow interface and its configuration options import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; ## Example Usage ```tsx import { workflow } from '@novu/framework'; workflow( 'my-workflow', async ({ step, payload, subscriber }) => { await step.inApp('send-in-app', async () => { return { body: 'Hello there', }; }); }, { payloadSchema: z.object({ body: z.string(), }), name: 'My Workflow', description: 'This is a workflow', tags: ['business', 'critical'], preferences: { channels: { inApp: { enabled: true }, }, }, } ); ``` ## Interface ```typescript import { workflow } from '@novu/framework'; workflow(workflowId, handler, options); ``` ### Parameters #### workflowId * **Type**: `string` * **Required**: Yes * **Description**: This id should be unique within your organization. #### handler * **Type**: `(context: WorkflowContext) => Promise` * **Required**: Yes * **Description**: The definition function of the workflow. #### options * **Type**: `WorkflowOptions` * **Required**: No * **Description**: An optional options object for workflow level configurations ##### options.payloadSchema * **Type**: `JsonSchema | ZodSchema` * **Description**: The schema to validate the event payload against, can be used to provide default values. ##### options.name * **Type**: `string` * **Description**: The name of the workflow. This is used to display a human-friendly name for the workflow in the Dashboard and `` component. If no value is specified, the `workflowId` will be used as the name. ##### options.description * **Type**: `string` * **Description**: The description of the workflow. This is used to provide a brief overview of the workflow in the Dashboard. ##### options.tags * **Type**: `string[]` * **Description**: The tags assigned to the workflow. Tags can be used to filter workflows in the dashboard, and can be used by channels such as Inbox to sort Notifications into different categories for a better user experience. ##### options.preferences * **Type**: `WorkflowPreferences` * **Description**: The preferences for the workflow. Read more about [Workflow Channel Preferences](/platform/concepts/preferences#workflow-channel-preferences). ###### preferences.all * **Type**: `WorkflowPreference` * **Properties**: * `enabled`: `boolean` (default: `true`) - A flag specifying if notification delivery is enabled for the workflow. * `readOnly`: `boolean` (default: `false`) - A flag specifying if the preferences are read-only. ###### preferences.channels * **Type**: `ChannelPreferences` * **Description**: The preferences for each channel. Read more about [Workflow Channel Preferences](/platform/concepts/preferences). * **Properties**: * `inApp`: `{ enabled: boolean }` - In-app channel preferences * `email`: `{ enabled: boolean }` - Email channel preferences * `sms`: `{ enabled: boolean }` - SMS channel preferences * `chat`: `{ enabled: boolean }` - Chat channel preferences * `push`: `{ enabled: boolean }` - Push channel preferences ## Workflow Context This context is passed by the workflow engine to provide contextual information about current workflow execution. ### subscriber * **Type**: `Subscriber` * **Properties**: * `subscriberId`: `string` (required) - The id of the subscriber, as passed during `/events/trigger` request. * `firstName`: `string` (nullable) - The first name of the subscriber. * `lastName`: `string` (nullable) - The last name of the subscriber. ### payload * **Type**: `InferProperties` * **Description**: The payload of the event that triggered the workflow, will be validated against the `payloadSchema` if provided. ### step * **Type**: `object` * **Description**: The object that contains all the step functions, read more at [Step Functions](/platform/framework/typescript/steps). ## Workflow Channel Preferences With Workflow channel preferences, you can control the default delivery preference for a channel and whether a subscriber can change it. Novu will show the subscriber preferences in `` component. Subscribers can enable and disable any active channel in the workflow. In the `all` object, you can specify default preferences for all channels. The `enabled` field on the `all` object is used as fallback value if a channel is not specified explicitly in `channels`. The `readOnly` field controls whether subscribers can change the delivery preference for a channel. Critical workflows are defined with `{ readOnly: true }`. In the `channels` object, you can specify In-App, SMS, Email, Chat, and Push channel preferences. Each channel takes an object with an optional `enabled` flag that controls whether a notification delivery channel is enabled or disabled by default for subscribers. ### Default values By default, `enabled` is `true` for all channels. The `readOnly` flag is `false`. These preferences can also be controlled from the Novu Dashboard per workflow. To do so, click on the cog icon at the top right of your screen, and then select the "Preferences" tab. ```tsx const newWorkflow = workflow( 'default-preferences', async ({ step }) => { await step.inApp('send-in-app', () => ({ body: 'Hello there', })); }, { preferences: { all: { enabled: true, readOnly: false }, channels: { inApp: { enabled: true }, email: { enabled: true }, sms: { enabled: true }, chat: { enabled: true }, push: { enabled: true }, }, }, } ); ``` ```tsx const newWorkflow = workflow( 'only-in-app-channel', async ({ step }) => { await step.inApp('send-in-app', () => ({ body: 'Hello there', })); }, { preferences: { all: { enabled: false }, channels: { inApp: { enabled: true }, }, }, } ); ``` ```tsx const newWorkflow = workflow( 'all-enabled-editable', async ({ step }) => { await step.inApp('send-in-app', () => ({ body: 'Hello there', })); }, { preferences: { all: { enabled: true }, }, } ); ``` file: ./content/docs/guides/webhooks/clerk.mdx # Clerk This guide walks you through integrating Clerk webhooks with Novu notifications in a Next.js application. import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; import { Card, Cards } from 'fumadocs-ui/components/card'; import { NextjsIcon } from '@/components/icons/nextjs'; import { ExpressjsIcon } from '@/components/icons/expressjs'; import { Callout } from 'fumadocs-ui/components/callout'; import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; import { File, Folder, Files } from 'fumadocs-ui/components/files'; You'll learn how to automatically trigger notification workflows when **any Clerk event** occurs, such as **user creation, email events, or password changes**. ## Overview When specific events happen in Clerk (e.g., user signup, password changes, email verification), this integration will: 1. Receive the webhook event from Clerk. 2. Verify the webhook signature. 3. Process the event data. 4. Trigger the corresponding **Novu notification workflow**. You can also clone this repository: [https://github.com/novuhq/clerk-to-novu-webhooks](https://github.com/novuhq/clerk-to-novu-webhooks) ## Prerequisites Before proceeding, ensure you have: * A **Clerk + Next.js app** ([Set up Clerk](https://clerk.com/docs/quickstarts/nextjs)). * A **Novu account** ([Sign up here](https://novu.com/signup)). ## Install Dependencies Run the following command to install the required packages: ``` npm install svix @novu/api @clerk/nextjs ``` ## Configure Environment Variables Add the following variables to your `.env.local` file: ``` NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_... CLERK_SECRET_KEY=sk_test_... CLERK_SIGNING_SECRET=whsec_... NOVU_SECRET_KEY=novu_secret_... ``` ## Expose Your Local Server To test webhooks locally, you need to expose your **local server** to the internet. There are two common options: ### localtunnel **localtunnel** is a simple and free way to expose your local server without requiring an account. 1. Start a localtunnel listener ```bash npx localtunnel 3000 ``` 2. Copy and save the generated **public URL** (e.g., `https://your-localtunnel-url.loca.lt`). Learn more about **localtunnel** [here](https://www.npmjs.com/package/localtunnel). **localtunnel** links may expire quickly and sometimes face reliability issues. ### ngrok For a more stable and configurable tunnel, use **ngrok**: 1. Create an account at [ngrok dashboard](https://dashboard.ngrok.com/). 2. Follow the [setup guide](https://dashboard.ngrok.com/get-started/setup). 3. Run the command: ```bash ngrok http 3000 ``` 4. Copy and save the **Forwarding URL** (e.g., `https://your-ngrok-url.ngrok.io`). Learn more about **ngrok** [here](https://dashboard.ngrok.com/get-started/setup). ## Set Up Clerk Webhook Endpoint 1. Go to the **Clerk Webhooks** page ([link](https://dashboard.clerk.com/last-active?path=webhooks)). 2. Click **Add Endpoint**. 3. Set the **Endpoint URL** as: ``` https://your-forwarding-URL/api/webhooks/clerk ``` 4. Subscribe to the **relevant Clerk events** (e.g., `user.created`, `email.created` etc.). You can find the list of all supported Clerk events [here](https://clerk.com/docs/reference/webhooks/events), or proceed to the section which going over [Identify the Triggering Event(s).](#identify-the-triggering-events) 5. Click **Create** and keep the settings page open. ## Add Signing Secret to Environment Variables 1. Copy the **Signing Secret** from Clerk's **Webhook Endpoint Settings**. 2. Add it to your `.env.local` file: ``` CLERK_SIGNING_SECRET=your_signing_secret_here ``` ## Make Webhook Route Public Ensure the webhook route is public by updating `middleware.ts` : ```jsx import { clerkMiddleware } from '@clerk/nextjs/server'; export default clerkMiddleware({ publicRoutes: ['/api/webhooks'], }); ``` ## Create Webhook Endpoint for Clerk in Next.js Create `app/api/webhooks/clerk/route.ts`: The following snippet is the complete code of how to create a webhook endpoint for Clerk in Next.js: ```jsx import { Webhook } from 'svix' import { headers } from 'next/headers' import { WebhookEvent, UserJSON } from '@clerk/nextjs/server' import { triggerWorkflow } from '@/app/utils/novu' // Single source of truth for all supported Clerk events and their corresponding Novu workflows const EVENT_TO_WORKFLOW_MAPPINGS = { // Session events 'session.created': 'recent-login-v2', // User events 'user.created': 'user-created', // Email events 'email.created': { 'magic_link_sign_in': 'auth-magic-link-login', 'magic_link_sign_up': 'auth-magic-link-registration', 'magic_link_user_profile': 'profile-magic-link-update', 'organization_invitation': 'organization-invitation-v2', 'organization_invitation_accepted': 'org-member-joined', 'passkey_added': 'security-passkey-created', 'passkey_removed': 'security-passkey-deleted', 'password_changed': 'security-password-updated', 'password_removed': 'security-password-deleted', 'primary_email_address_changed': 'profile-email-updated', 'reset_password_code': 'reset-password-code-v2', 'verification_code': 'verification-code-v2', 'waitlist_confirmation': 'waitlist-signup-confirmed', 'waitlist_invitation': 'waitlist-access-granted', 'invitation': 'user-invitation' } } as const; export async function POST(request: Request) { try { const SIGNING_SECRET = process.env.SIGNING_SECRET if (!SIGNING_SECRET) { throw new Error('Please add SIGNING_SECRET from Clerk Dashboard to .env') } const webhook = new Webhook(SIGNING_SECRET) const headerPayload = await headers() const validatedHeaders = validateHeaders(headerPayload) const payload = await request.json() const body = JSON.stringify(payload) const event = await verifyWebhook(webhook, body, { 'svix-id': validatedHeaders.svix_id, 'svix-timestamp': validatedHeaders.svix_timestamp, 'svix-signature': validatedHeaders.svix_signature, }) await handleWebhookEvent(event) return new Response('Webhook received', { status: 200 }) } catch (error) { console.error('Webhook processing error:', error) return new Response(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`, { status: 400 }) } } const handleWebhookEvent = async (event: WebhookEvent) => { const workflow = await workflowBuilder(event) if (!workflow) { console.log(`Unsupported event type: ${event.type}`) return } const subscriber = await subscriberBuilder(event) const payload = await payloadBuilder(event) await triggerWorkflow(workflow, subscriber, payload) } async function workflowBuilder(event: WebhookEvent): Promise { if (!EVENT_TO_WORKFLOW_MAPPINGS[event.type as keyof typeof EVENT_TO_WORKFLOW_MAPPINGS]) { return undefined; } if (event.type === 'email.created' && event.data.slug) { const emailMappings = EVENT_TO_WORKFLOW_MAPPINGS['email.created']; const emailSlug = event.data.slug as keyof typeof emailMappings; return emailMappings[emailSlug] || `email-${String(emailSlug).replace(/_/g, '-')}`; } return EVENT_TO_WORKFLOW_MAPPINGS[event.type as keyof typeof EVENT_TO_WORKFLOW_MAPPINGS] as string; } async function subscriberBuilder(response: WebhookEvent) { const userData = response.data as UserJSON; if (!userData.id) { throw new Error('Missing subscriber ID from webhook data'); } return { subscriberId: (userData as any).user_id ?? userData.id, firstName: userData.first_name ?? undefined, lastName: userData.last_name ?? undefined, email: (userData.email_addresses?.[0]?.email_address ?? (userData as any).to_email_address) ?? undefined, phone: userData.phone_numbers?.[0]?.phone_number ?? undefined, locale: 'en_US', avatar: userData.image_url ?? undefined, data: { clerkUserId: (userData as any).user_id ?? userData.id, username: userData.username ?? '', }, } } async function payloadBuilder(response: WebhookEvent) { return response.data; } const validateHeaders = (headerPayload: Headers) => { const svix_id = headerPayload.get('svix-id') const svix_timestamp = headerPayload.get('svix-timestamp') const svix_signature = headerPayload.get('svix-signature') if (!svix_id || !svix_timestamp || !svix_signature) { throw new Error('Missing Svix headers') } return { svix_id, svix_timestamp, svix_signature } } const verifyWebhook = async (webhook: Webhook, body: string, headers: any): Promise => { try { return webhook.verify(body, headers) as WebhookEvent } catch (err) { console.error('Error: Could not verify webhook:', err) throw new Error('Verification error') } } ``` *** **Imports and Dependencies** ```jsx import { Webhook } from 'svix' import { headers } from 'next/headers' import { WebhookEvent, UserJSON } from '@clerk/nextjs/server' import { triggerWorkflow } from '@/app/utils/novu' ``` * `Webhook` from `svix`: This is a library used to verify the authenticity of incoming webhooks by checking their signatures. Webhooks often use signatures to ensure the payload hasn’t been tampered with. * `headers` from `next/headers`: A Next.js utility to access HTTP headers from the incoming request in the App Router. * `WebhookEvent` from `@clerk/nextjs/server`: A type definition for webhook events, likely provided by Clerk (a user authentication and management service). This ensures type safety when handling events. * `triggerWorkflow`: A custom function (imported from another file) that triggers a workflow. This is likely where notifications or other business logic is executed. *** **Event Mapping** ```jsx const EVENT_TO_WORKFLOW_MAPPINGS = { // Clerk webhook event type -> Novu workflowId // Session events 'session.created': 'session-created', // User events 'user.created': 'user-created', // Email events 'email.created': { 'magic_link_sign_in': 'auth-magic-link-login', 'magic_link_sign_up': 'auth-magic-link-registration', 'magic_link_user_profile': 'profile-magic-link-update', 'organization_invitation': 'organization-invitation', 'organization_invitation_accepted': 'org-member-joined', 'passkey_added': 'security-passkey-created', 'passkey_removed': 'security-passkey-deleted', 'password_changed': 'security-password-updated', 'password_removed': 'security-password-deleted', 'primary_email_address_changed': 'profile-email-updated', 'reset_password_code': 'reset-password-code', 'verification_code': 'verification-code', 'waitlist_confirmation': 'waitlist-signup-confirmed', 'waitlist_invitation': 'waitlist-access-granted', 'invitation': 'user-invitation' } } as const; ``` This mapping defines how Clerk webhook events are associated with Novu workflows. *** **Main Entry Point: `POST` Handler** ```jsx export async function POST(request: Request) { try { const SIGNING_SECRET = process.env.SIGNING_SECRET if (!SIGNING_SECRET) { throw new Error('Please add SIGNING_SECRET from Clerk Dashboard to .env') } const webhook = new Webhook(SIGNING_SECRET) const headerPayload = await headers() const validatedHeaders = validateHeaders(headerPayload) const payload = await request.json() const body = JSON.stringify(payload) const event = await verifyWebhook(webhook, body, { 'svix-id': validatedHeaders.svix_id, 'svix-timestamp': validatedHeaders.svix_timestamp, 'svix-signature': validatedHeaders.svix_signature, }) await handleWebhookEvent(event) return new Response('Webhook received', { status: 200 }) } catch (error) { console.error('Webhook processing error:', error) return new Response(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`, { status: 400 }) } } ``` This is the main function that handles incoming HTTP POST requests (webhook events). *** **Handling the Webhook Event: `handleWebhookEvent`** ```jsx const handleWebhookEvent = async (event: WebhookEvent) => { const workflow = await workflowBuilder(event) if (!workflow) { console.log(`Unsupported event type: ${event.type}`) return } const subscriber = await subscriberBuilder(event) const payload = await payloadBuilder(event) await triggerWorkflow(workflow, subscriber, payload) } ``` This function processes the verified webhook event. *** **Identify the WorkflowID based on the event type: `workflowBuilder`** ```jsx async function workflowBuilder(event: WebhookEvent): Promise { if (!EVENT_TO_WORKFLOW_MAPPINGS[event.type as keyof typeof EVENT_TO_WORKFLOW_MAPPINGS]) { return undefined; } if (event.type === 'email.created' && event.data.slug) { const emailMappings = EVENT_TO_WORKFLOW_MAPPINGS['email.created']; const emailSlug = event.data.slug as keyof typeof emailMappings; return emailMappings[emailSlug] || `email-${String(emailSlug).replace(/_/g, '-')}`; } return EVENT_TO_WORKFLOW_MAPPINGS[event.type as keyof typeof EVENT_TO_WORKFLOW_MAPPINGS] as string; } ``` This function determines the workflow ID by mapping the Clerk webhook event type to the Novu workflow ID. *** **Building the Subscriber: `subscriberBuilder`** ```jsx async function subscriberBuilder(response: WebhookEvent) { const userData = response.data as UserJSON; if (!userData.id) { throw new Error('Missing subscriber ID from webhook data'); } return { subscriberId: (userData as any).user_id ?? userData.id, firstName: userData.first_name ?? undefined, lastName: userData.last_name ?? undefined, email: (userData.email_addresses?.[0]?.email_address ?? (userData as any).to_email_address) ?? undefined, phone: userData.phone_numbers?.[0]?.phone_number ?? undefined, locale: 'en_US', avatar: userData.image_url ?? undefined, data: { clerkUserId: (userData as any).user_id ?? userData.id, username: userData.username ?? '', }, } } ``` This function builds the subscriber data based on the webhook event data. *** **Building the Payload: `payloadBuilder`** ```jsx async function payloadBuilder(response: WebhookEvent) { return response.data; } ``` This function constructs (extracts from the webhook event) the payload object data that will be used within workflow trigger call. *** **Validating the Headers: `validateHeaders`** ```jsx const validateHeaders = (headerPayload: Headers) => { const svix_id = headerPayload.get('svix-id') const svix_timestamp = headerPayload.get('svix-timestamp') const svix_signature = headerPayload.get('svix-signature') if (!svix_id || !svix_timestamp || !svix_signature) { throw new Error('Missing Svix headers') } return { svix_id, svix_timestamp, svix_signature } } ``` This function extracts and validates the Svix headers from the request. *** **Verifying the Webhook: `verifyWebhook`** ```jsx const verifyWebhook = async (webhook: Webhook, body: string, headers: any): Promise => { try { return webhook.verify(body, headers) as WebhookEvent } catch (err) { console.error('Error: Could not verify webhook:', err) throw new Error('Verification error') } } ``` This function verifies the authenticity of the webhook using the `svix` library. *** **How It All Fits Together** Here’s a high-level flow of how the code works: 1. **Receive a Webhook:** The `POST` handler receives an `HTTP POST` request with a webhook payload. 2. **Validate and Verify:** The `validateHeaders` function ensures the required Svix headers are present. The `verifyWebhook` function uses the `svix` library to verify the webhook’s authenticity. 3. **Process the Event:** The `handleWebhookEvent` function filters events and processes only `user.created` or `email.created` events. It calls helper functions (`workflowBuilder`, `subscriberBuilder`, `payloadBuilder`) to construct the necessary data. 4. **Trigger a Workflow:** The `triggerWorkflow` function is called with the constructed data, executing the desired business logic (e.g., sending notifications). *** ## Add Novu Workflow Notification Trigger Function Create `app/utils/novu.ts` : ```jsx import { Novu } from '@novu/api'; import { SubscriberPayloadDto } from '@novu/api/models/components/subscriberpayloaddto'; const novu = new Novu({ secretKey: process.env['NOVU_SECRET_KEY'] }); export async function triggerWorkflow(workflowId: string, subscriber: SubscriberPayloadDto, payload: object) { try { console.log("Payload:", payload ,"Triggering workflow:", workflowId, "Subscriber:", subscriber) await novu.trigger({ workflowId, to: subscriber, payload }); return new Response('Notification triggered', { status: 200 }); } catch (error) { return new Response('Error triggering notification', { status: 500 }); } } ``` ## Add or create Novu workflows in your Novu dashboard In Novu, a webhook event—such as a user being created or updated—can trigger one or more workflows, depending on how you want to handle these events in your application. A workflow defines a sequence of actions (e.g., sending notifications, updating records) that execute when triggered by a webhook. The Novu dashboard allows you to either create a custom workflow from scratch or choose from pre-built templates to streamline the process. **Steps to Create a Workflow** Follow these steps to set up your workflow(s) in the Novu dashboard: ### Identify the Triggering Event(s) Determine which webhook events will activate your workflow (e.g., "user created," "user updated"). Check your webhook configuration to understand the event data being sent. To find a list of all the events Clerk supports: 1. In the Clerk Dashboard, navigate to the Webhooks page. 2. Select the Event Catalog tab. The payload of a webhook is a JSON object that contains the following properties: * `data`: contains the actual payload sent by Clerk. The payload can be a different object depending on the event type. For example, for `user.*` events, the payload will always be the [User object](https://clerk.com/docs/references/javascript/user). * `object`: always set to `event`. * `type`: the type of event that triggered the webhook. * `timestamp`: timestamp in milliseconds of when the event occurred. * `instance_id`: the identifier of your Clerk instance. The following example shows the payload of a `user.created` event: ```json { "data": { "birthday": "", "created_at": 1654012591514, "email_addresses": [ { "email_address": "exaple@example.org", "id": "idn_29w83yL7CwVlJXylYLxcslromF1", "linked_to": [], "object": "email_address", "verification": { "status": "verified", "strategy": "ticket" } } ], "external_accounts": [], "external_id": "567772", "first_name": "Example", "gender": "", "id": "user_29w83sxmDNGwOuEthce5gg56FcC", "image_url": "https://img.clerk.com/xxxxxx", "last_name": "Example", "last_sign_in_at": 1654012591514, "object": "user", "password_enabled": true, "phone_numbers": [], "primary_email_address_id": "idn_29w83yL7CwVlJXylYLxcslromF1", "primary_phone_number_id": null, "primary_web3_wallet_id": null, "private_metadata": {}, "profile_image_url": "https://www.gravatar.com/avatar?d=mp", "public_metadata": {}, "two_factor_enabled": false, "unsafe_metadata": {}, "updated_at": 1654012591835, "username": null, "web3_wallets": [] }, "instance_id": "ins_123", "object": "event", "timestamp": 1654012591835, "type": "user.created" } ``` ### Choose Your Starting Point Browse the workflow template store in the Novu dashboard. If a template matches your use case (e.g., "User Onboarding"), select it and proceed to customize it. ![Dashboard Template Store](./media-assets/clerk/workflow-fromTemplate.gif) If no template fits or you need full control, start with a blank workflow and define every step yourself. ![Dashboard Blank Workflow](./media-assets/clerk/blankWorkflow.gif) If you prefer a more code-based approach, you can create a workflow using the Novu Framework. } href="/framework/overview">

The Novu framework allows you to build and manage advanced notification workflows with code, and expose no-code controls for non-technical users to modify.

### Configure the Workflow * For a template, tweak the existing steps to align with your requirements. * For a blank workflow, add actions like sending emails, sending in-app notifications, Push notifications, or other actions. * For a code-first workflow, you can use the Novu Framework to build your workflow right within your code base. ### Set Trigger Conditions * Link the workflow to the correct webhook event(s). * Ensure the trigger matches the event data (e.g., event type or payload) sent by your application. - **Start Simple:** Use templates for common tasks and switch to blank workflows for unique needs. - **Test Thoroughly:** Simulate webhook events to ensure your workflows behave as expected. - **Plan for Growth:** Organize workflows logically (separate or combined) to make future updates easier.
## Disable Email **Delivered by Clerk** By default, Clerk sends email notifications whenever necessary, such as Magic Links for email verification, Invitations, Password Resets, and more. To prevent users from receiving duplicate emails, we need to disable email delivery by Clerk for the notifications handled by Novu. 1. Navigate to the **Emails** section in the Clerk Dashboard. ![Clerk's Dashboard 1](/images/guides/clerk/clerk-email-dashboard.png) 2. Select any **email.created** event that you want Novu to handle. 3. Toggle **off** email delivery for the selected event. ![Clerk's Dashboard 2](/images/guides/clerk/clerk-email-dashboard2.png) This ensures that Clerk does not send duplicate emails, allowing Novu to manage the notifications seamlessly. ## Test the Webhook 1. Start your Next.js server. 2. Go to **Clerk Webhooks → Testing**. 3. Select an event (e.g., `user.created`, `email.created`). 4. Click **Send Example**. 5. Verify logs in **your terminal**.
file: ./content/docs/guides/webhooks/segment.mdx # Segment Learn how to set up Segment as a data source for Novu using Destination Functions. Send user events from Segment to trigger notifications in Novu. import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; import { Card, Cards } from 'fumadocs-ui/components/card'; import { NextjsIcon } from '@/components/icons/nextjs'; import { ExpressjsIcon } from '@/components/icons/expressjs'; import { Callout } from 'fumadocs-ui/components/callout'; import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; import { File, Folder, Files } from 'fumadocs-ui/components/files'; This guide demonstrates how to use Segment's Destination Functions to send user events and traits to Novu. You'll learn how to: * Create a custom Segment destination for Novu * Map Segment identify calls to Novu subscribers * Trigger notification workflows from Segment track events * Handle errors and retry logic for reliable delivery By the end, you'll have a working integration that creates subscribers and triggers notification workflows in Novu based on Segment events. **Prerequisites** Before you start, ensure you have: * A **Segment account** with access to **Functions** (check your workspace permissions) * A **Novu account** with an **API key** (find this in your Novu dashboard under Settings > API Keys) ## Create a Destination Function in Segment 1. Log in to your Segment account 2. Navigate to **Connections** > **Functions** in the left sidebar 3. Click **New Function** and select **Destination** 4. Name your function (e.g., Novu Destination) and click **Create Function** ## Configure the Destination Function The Destination Function will handle two key Segment event types: * **identify**: Creates or updates a subscriber in Novu * **track**: Triggers a notification workflow in Novu Paste the following complete code into the Segment Function editor: ```jsx /** * Handles identify events: Creates or updates a subscriber in Novu * @param {SegmentIdentifyEvent} event - The Segment identify event * @param {FunctionSettings} settings - Function settings including API key */ async function onIdentify(event, settings) { const endpoint = 'https://api.novu.co/v2/subscribers'; const apiKey = settings.apiKey; if (!apiKey) throw new Error('Novu API key is missing in settings'); if (!event.userId) throw new Error('userId is required in identify event'); const subscriberData = { subscriberId: event.userId, firstName: event.traits?.firstName || null, lastName: event.traits?.lastName || null, email: event.traits?.email || null, phone: event.traits?.phone || null, avatar: event.traits?.avatar || null, }; try { const response = await fetch(endpoint, { method: 'POST', headers: { 'Authorization': `ApiKey ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify(subscriberData) }); const responseBody = await response.json(); if (!response.ok) { if (response.status >= 500 || response.status === 429) { throw new RetryError(`Server error: ${response.status}`); } throw new Error(`API error: ${response.status} - ${responseBody.message || 'Unknown error'}`); } } catch (error) { throw error instanceof RetryError ? error : new RetryError(error.message); } } // Mapping of Segment track events to Novu workflows const EVENT_TO_WORKFLOW_MAPPINGS = { 'User Registered': 'welcome' // Add more mappings: 'Event Name': 'novu-workflow-name' }; /** * Handles track events: Triggers a notification workflow in Novu * @param {SegmentTrackEvent} event - The Segment track event * @param {FunctionSettings} settings - Function settings including API key */ async function onTrack(event, settings) { const endpoint = 'https://api.novu.co/v1/events/trigger'; const apiKey = settings.apiKey; if (!apiKey) throw new Error('Novu API key is missing in settings'); if (!event.userId) throw new Error('userId is required in track event'); const workflow = EVENT_TO_WORKFLOW_MAPPINGS[event.event]; if (!workflow) throw new Error(`No workflow mapped for event: ${event.event}`); const triggerEvent = { name: workflow, to: { subscriberId: event.userId }, payload: event.properties || {} }; try { const response = await fetch(endpoint, { method: 'POST', headers: { 'Authorization': `ApiKey ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify(triggerEvent) }); const responseBody = await response.json(); if (!response.ok) { if (response.status >= 500 || response.status === 429) { throw new RetryError(`Server error: ${response.status}`); } throw new Error(`API error: ${response.status} - ${responseBody.message || 'Unknown error'}`); } } catch (error) { throw error instanceof RetryError ? error : new RetryError(error.message); } } ``` * **`onIdentify`**: * Maps Segment traits (e.g., firstName, lastName, email) to Novu subscriber fields * Uses Novu's `/v2/subscribers` endpoint * Creates or updates subscribers (Novu's API is idempotent for existing `subscriberIds`) * **`onTrack`**: * Maps Segment `track` events to Novu workflows using `EVENT_TO_WORKFLOW_MAPPINGS` * Sends the event properties as the payload to trigger a workflow via `/v1/events/trigger` * **Error Handling**: Retries on server errors (5xx) or rate limits (429), fails fast on other errors **Customize the Mapping**: Update `EVENT_TO_WORKFLOW_MAPPINGS` with your Segment event names and corresponding Novu workflow names. ## Deploy the Function 1. Click **Save** in the Function editor 2. Enable the function by toggling it to **Active** ## Connect the Function to a Source 1. Go to **Connections** > Select your **Source** (e.g., website, app) 2. In the **Destinations** tab, click **Add Destination** 3. Choose your **Novu Destination Function** from the list 4. Click **Connect**. When prompted, enter your **Novu API key** in the apiKey field 5. Save the configuration ## Testing the Integration Verify everything works: Example: ```json { "type": "identify", "traits": { "name": "Peter Gibbons", "email": "peter@example.com", "plan": "premium", "logins": 5 }, "userId": "97980cfea0067" } ``` Check Novu's **Subscribers** list to confirm the subscriber appears. Example: ```json { "type": "track", "event": "User Registered", "properties": { "plan": "Pro Annual", "accountType" : "Facebook" } } ``` Verify the `welcome` workflow triggers in Novu's activity feed. Use Segment's **Debugger** to monitor function calls and catch any errors. * **401 Unauthorized**: Double-check your Novu API key in the function settings * **Subscriber Not Created**: Ensure userId is included in the identify event * **Workflow Not Triggering**: Confirm the event name matches a key in EVENT\_TO\_WORKFLOW\_MAPPINGS and the workflow exists in Novu * **API Errors**: Check Segment's logs for detailed error messages * **Subscriber Updates**: Novu's /v1/subscribers endpoint updates existing subscribers if the subscriberId matches, keeping data current with each identify event * **Expanding Functionality**: Add more event types (e.g., group, page) by defining additional handlers like onGroup in the code With this setup, your Segment events will seamlessly flow into Novu, enabling powerful notification workflows tailored to your users' actions. file: ./content/docs/guides/webhooks/stripe.mdx # Stripe This guide walks you through integrating Stripe webhooks with Novu notifications in a Next.js application. import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; import { Card, Cards } from 'fumadocs-ui/components/card'; import { NextjsIcon } from '@/components/icons/nextjs'; import { ExpressjsIcon } from '@/components/icons/expressjs'; import { Callout } from 'fumadocs-ui/components/callout'; import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; import { File, Folder, Files } from 'fumadocs-ui/components/files'; You'll learn how to automatically trigger notification workflows when **any Stripe event** occurs, such as **payment, subscription, or customer events**. ## Overview When specific events happen in Stripe (e.g., payment, subscription, or customer events), this integration will: 1. Receive the webhook event from Stripe. 2. Verify the webhook signature. 3. Process the event data. 4. Trigger the corresponding **Novu notification workflow**. You can also clone this repository: [https://github.com/novuhq/stripe-to-novu-webhooks](https://github.com/novuhq/stripe-to-novu-webhooks) ## Prerequisites Before proceeding, ensure you have: * A **Stripe account** ([Sign up here](https://dashboard.stripe.com/signup)). * A **Novu account** ([Sign up here](https://novu.com/signup)). ## Install Dependencies Run the following command to install the required packages: ``` npm install @novu/api @clerk/nextjs @stripe ``` ## Configure Environment Variables Add the following variables to your `.env.local` file: ``` NOVU_SECRET_KEY=novu_secret_... STRIPE_SECRET_KEY=sk_test_... STRIPE_WEBHOOK_SECRET=whsec_... ``` ## Expose Your Local Server To test webhooks locally, you need to expose your **local server** to the internet. There are two common options: ### localtunnel **localtunnel** is a simple and free way to expose your local server without requiring an account. 1. Start a localtunnel listener ```bash npx localtunnel 3000 ``` 2. Copy and save the generated **public URL** (e.g., `https://your-localtunnel-url.loca.lt`). Learn more about **localtunnel** [here](https://www.npmjs.com/package/localtunnel). **localtunnel** links may expire quickly and sometimes face reliability issues. ### ngrok For a more stable and configurable tunnel, use **ngrok**: 1. Create an account at [ngrok dashboard](https://dashboard.ngrok.com/). 2. Follow the [setup guide](https://dashboard.ngrok.com/get-started/setup). 3. Run the command: ```bash ngrok http 3000 ``` 4. Copy and save the **Forwarding URL** (e.g., `https://your-ngrok-url.ngrok.io`). Learn more about **ngrok** [here](https://dashboard.ngrok.com/get-started/setup). ## Set Up Stripe Webhook Endpoint Stripe supports two endpoint types: Account and Connect. Create an endpoint for Account unless you’ve created a Connect application. You can register up to 16 webhook endpoints on each Stripe account. When you create an endpoint in the Dashboard, you can choose between your Account’s API version or the latest API version. You can test other API versions in Workbench using stripe webhook\_endpoints create, but you must create a webhook endpoint using the API to use other API versions in production. Use the following steps to register a webhook endpoint in the Developers Dashboard. 1. Navigate to the [**Webhooks page**](https://dashboard.stripe.com/webhooks). 2. Click **Add Endpoint**. 3. Add your webhook endpoint’s HTTPS URL in **Endpoint URL**. ``` https://your-forwarding-URL/api/webhooks/stripe ``` 4. If you have a **Stripe Connect account**, enter a description, then click **Listen to events** on **Connected accounts**. 5. Select the [event types](https://docs.stripe.com/api#event_types) you’re currently receiving in your local webhook endpoint in **Select events**. 6. Click **Add endpoint**. ## Add Signing Secret to Environment Variables 1. Copy the **Signing Secret** from Stripe's **Webhook Endpoint Settings**. 2. Add it to your `.env.local` file: ``` STRIPE_WEBHOOK_SECRET=your_signing_secret_here ``` ## Make Webhook Route Public Ensure the webhook route is public by updating `middleware.ts` : ```jsx import { clerkMiddleware } from '@clerk/nextjs/server'; export default clerkMiddleware({ publicRoutes: ['/api/webhooks'], }); ``` ## Create Webhook Endpoint for Clerk in Next.js Create `app/api/webhooks/stripe/route.ts`: The following snippet is the complete code of how to create a webhook endpoint for Clerk in Next.js: ```jsx import Stripe from "stripe"; import { NextResponse, NextRequest } from "next/server"; import { triggerWorkflow } from "@/app/utils/novu"; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); const supportedEvents = [ "customer.subscription.created", "customer.subscription.updated", ]; export async function POST(request: NextRequest) { const webhookPayload = await request.text(); const response = JSON.parse(webhookPayload); const signature = request.headers.get("Stripe-Signature"); try { let event = stripe.webhooks.constructEvent( webhookPayload, signature!, process.env.STRIPE_WEBHOOK_SECRET! ); if (supportedEvents.includes(event.type)) { const workflow = event.type.replaceAll(".", "-"); const subscriber = await buildSubscriberData(response); const payload = await payloadBuilder(response); console.log( "Triggering workflow:", workflow, "Subscriber:", subscriber, "Payload:", payload ); return await triggerWorkflow(workflow, subscriber, payload); } return NextResponse.json({ status: "sucess", event: event.type, response: response }); } catch (error) { return NextResponse.json({ status: "Failed", error }); } } async function buildSubscriberData(response: any) { const customer = await stripe.customers.retrieve(response.data.object.customer); console.log("Customer", customer); if ('deleted' in customer) { throw new Error('Customer has been deleted'); } // Split the full name into first and last name const [firstName = '', lastName = ''] = (customer.name || '').split(' '); return { subscriberId: customer.id, email: customer.email || 'test2@test.com', firstName: firstName || '', lastName: lastName || '', phone: customer?.phone || '', locale: customer?.preferred_locales?.[0] || 'en', // Use first preferred locale or default to 'en' avatar: '', // Stripe customer doesn't have avatar data: { stripeCustomerId: customer.id, }, }; } async function payloadBuilder(response: any) { const webhookData = JSON.parse(response); return webhookData; } ``` ## Add Novu Workflow Notification Trigger Function Create `app/utils/novu.ts` : ```jsx import { Novu } from '@novu/api'; import { SubscriberPayloadDto } from '@novu/api/models/components/subscriberpayloaddto'; const novu = new Novu({ secretKey: process.env['NOVU_SECRET_KEY'] }); export async function triggerWorkflow(workflowId: string, subscriber: SubscriberPayloadDto, payload: object) { try { console.log("Payload:", payload ,"Triggering workflow:", workflowId, "Subscriber:", subscriber) await novu.trigger({ workflowId, to: subscriber, payload }); return new Response('Notification triggered', { status: 200 }); } catch (error) { return new Response('Error triggering notification', { status: 500 }); } } ``` ## Add or create Novu workflows in your Novu dashboard In Novu, a webhook event—such as a user being created or updated—can trigger one or more workflows, depending on how you want to handle these events in your application. A workflow defines a sequence of actions (e.g., sending notifications, updating records) that execute when triggered by a webhook. The Novu dashboard allows you to either create a custom workflow from scratch or choose from pre-built templates to streamline the process. *** Follow these steps to set up your workflow(s) in the Novu dashboard: ### Identify the Triggering Event(s) Determine which webhook events will activate your workflow (e.g., "user created," "user updated"). Check your webhook configuration to understand the event data being sent. To find a list of all the events Stripe supports and learn more about them, visit the [Stripe documentation](https://docs.stripe.com/event-destinations). The following example shows the payload of a `customer.subscription.created` event: ```json { "object": { "id": "sub_1Qy9WoR7RyRE3Uxrj6iaIAHV", "object": "subscription", "application": null, "application_fee_percent": null, "automatic_tax": { "disabled_reason": null, "enabled": false, "liability": null }, "billing_cycle_anchor": 1740910426, "billing_cycle_anchor_config": null, "billing_thresholds": null, "cancel_at": null, "cancel_at_period_end": false, "canceled_at": null, "cancellation_details": { "comment": null, "feedback": null, "reason": null }, "collection_method": "charge_automatically", "created": 1740910426, "currency": "usd", "current_period_end": 1743588826, "current_period_start": 1740910426, "customer": "cus_RrtJuJIveFMpmq", "days_until_due": null, "default_payment_method": null, "default_source": null, "default_tax_rates": [], "description": null, "discount": null, "discounts": [], "ended_at": null, "invoice_settings": { "account_tax_ids": null, "issuer": { "type": "self" } }, "items": { "object": "list", "data": [ { "id": "si_sdfsFwsthbHIUHJY", "object": "subscription_item", "billing_thresholds": null, "created": 1740910426, "discounts": [], "metadata": {}, "plan": { "id": "price_1Qy9WnR7RyRE3UxrRi33EJNc", "object": "plan", "active": true, "aggregate_usage": null, "amount": 1500, "amount_decimal": "1500", "billing_scheme": "per_unit", "created": 1740910425, "currency": "usd", "interval": "month", "interval_count": 1, "livemode": false, "metadata": {}, "meter": null, "nickname": null, "product": "prod_RrtJKUBhMKqoHb", "tiers_mode": null, "transform_usage": null, "trial_period_days": null, "usage_type": "licensed" }, "price": { "id": "price_1Qy9WnR7RyRE3UxrRi33EJNc", "object": "price", "active": true, "billing_scheme": "per_unit", "created": 1740910425, "currency": "usd", "custom_unit_amount": null, "livemode": false, "lookup_key": null, "metadata": {}, "nickname": null, "product": "prod_RrtJKUBhMKqoHb", "recurring": { "aggregate_usage": null, "interval": "month", "interval_count": 1, "meter": null, "trial_period_days": null, "usage_type": "licensed" }, "tax_behavior": "unspecified", "tiers_mode": null, "transform_quantity": null, "type": "recurring", "unit_amount": 1500, "unit_amount_decimal": "1500" }, "quantity": 1, "subscription": "sub_1Qy9WoR7RyRE3Uxrj6iaIAHV", "tax_rates": [] } ], "has_more": false, "total_count": 1, "url": "/v1/subscription_items?subscription=sub_1Qy9WoR7RyRE3Uxrj6iaIAHV" }, "latest_invoice": "in_1Qy9WoR7RyRE3UxrNBjqvFxM", "livemode": false, "metadata": {}, "next_pending_invoice_item_invoice": null, "on_behalf_of": null, "pause_collection": null, "payment_settings": { "payment_method_options": null, "payment_method_types": null, "save_default_payment_method": "off" }, "pending_invoice_item_interval": null, "pending_setup_intent": null, "pending_update": null, "plan": { "id": "price_1Qy9WnR7RyRE3UxrRi33EJNc", "object": "plan", "active": true, "aggregate_usage": null, "amount": 1500, "amount_decimal": "1500", "billing_scheme": "per_unit", "created": 1740910425, "currency": "usd", "interval": "month", "interval_count": 1, "livemode": false, "metadata": {}, "meter": null, "nickname": null, "product": "prod_RrtJKUBhMKqoHb", "tiers_mode": null, "transform_usage": null, "trial_period_days": null, "usage_type": "licensed" }, "quantity": 1, "schedule": null, "start_date": 1740910426, "status": "active", "test_clock": null, "transfer_data": null, "trial_end": null, "trial_settings": { "end_behavior": { "missing_payment_method": "create_invoice" } }, "trial_start": null }, "previous_attributes": null } ``` ### Choose Your Starting Point Browse the workflow template store in the Novu dashboard. If a template matches your use case (e.g., "User Onboarding"), select it and proceed to customize it. ![Dashboard Template Store](./media-assets/clerk/workflow-fromTemplate.gif) If no template fits or you need full control, start with a blank workflow and define every step yourself. ![Dashboard Blank Workflow](./media-assets/clerk/blankWorkflow.gif) If you prefer a more code-based approach, you can create a workflow using the Novu Framework. } href="/framework/overview">

The Novu framework allows you to build and manage advanced notification workflows with code, and expose no-code controls for non-technical users to modify.

### Configure the Workflow * For a template, tweak the existing steps to align with your requirements. * For a blank workflow, add actions like sending emails, sending in-app notifications, Push notifications, or other actions. * For a code-first workflow, you can use the Novu Framework to build your workflow right within your code base. ### Set Trigger Conditions * Link the workflow to the correct webhook event(s). * Ensure the trigger matches the event data (e.g., event type or payload) sent by your application. - **Start Simple:** Use templates for common tasks and switch to blank workflows for unique needs. - **Test Thoroughly:** Simulate webhook events to ensure your workflows behave as expected. - **Plan for Growth:** Organize workflows logically (separate or combined) to make future updates easier.
## Disable Email Delivery by Stripe By default, Stripe sends email notifications whenever necessary, such as subscription created, updated, and more. To prevent users from receiving duplicate emails, we need to disable email delivery by Stripe for the notifications handled by Novu. 1. In your Stripe Dashboard, navigate to the **Settings**. 2. Under the **Product Settings** section, navigate to the **Billing** tab. 3. Toggle **off** delivery of the events you want to handle with Novu. This ensures that Stripe does not send duplicate emails, allowing Novu to manage the notifications seamlessly. ## Test the Webhook Learn how you can test the webhook events using the Stripe CLI.
file: ./content/docs/platform/account/authentication.mdx # Authentication Novu supports advanced user authentication capabilities like OAuth, SSO, MFA and more, providing enterprise customers with a robust, secure, and user-friendly authentication solution. import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; import { Card, Cards } from 'fumadocs-ui/components/card'; This feature is only available to Enterprise customers on our [Cloud platform](https://dashboard.novu.co/?utm_campaign=docs_account_authentication). For more information, please see our [Enterprise plan](https://novu.co/pricing) in the pricing page. ## Key capabilities ### Out-of-the-box OAuth providers Currently providing authentication via Github, with the plan to expand to a number of other providers such as: * Google * Facebook * Apple * Microsoft * LinkedIn ### SSO, SAML and OpenID connect Novu provides a streamlined implementation of SAML and OpenID Connect protocols, allowing integration with a wide range of enterprise IdPs. Arrange setup of known providers like Okta, Microsoft Entra ID, Google workspace or a custom SSO provider. Novu adheres to industry-standard security protocols to ensure secure and reliable user authentication. ### Advanced security features Robust MFA options including SMS passcodes, authenticator apps, hardware keys, and recovery codes, enhancing security and reducing the risk of unauthorized access. Comprehensive session management features including active device monitoring, session revocation, and adaptive session durations to balance security and user convenience. ### User management and customization Detailed control over user roles and permissions to tailor access levels and ensure appropriate access control. Smooth onboarding processes with customizable invitation workflows, ensuring a positive initial user experience. Intuitive profile management for users to update their information and authentication methods easily. file: ./content/docs/platform/account/billing.mdx # Billing Manage your billing and payment information, view invoices, and upgrade your subscription plan. import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; ## Frequently Asked Questions Paid plans can be purchased from billing settings in the Novu dashboard. Click on the avatar icon in the top right corner of the dashboard, then click on **Billing Plans** in the left side menu to access the billing settings. From there, you can select the desired subscription tier and complete the purchase. Upgrade subscription from billing settings Past invoices can be accessed from the billing settings in the Novu dashboard. Click on the avatar icon in the top right corner of the dashboard, then click on **Billing Plans** on the side menu to access the billing settings. From there, click on the **Manage Subscription** option in your subscribed tier to view past invoices. Access past invoices using manage subscription option file: ./content/docs/platform/account/manage-members.mdx # Manage organization members How to invite, manage, update permissions for, and remove members from your Novu organization You can manage the members of your organization from the **Settings** page in your Novu dashboard. From there, you can assign responsibilities, manage permissions, and control access to workflows and other features. Only members with the **Owner** role can invite or manage members. ![Managing organization members](/images/account/managing-members-overview.png) ## Invite a new member to your organization 1. In the dashboard, go to **Settings** > **Team** 2. Click **Invite**. You can do this from the **Members**, **Invitations**, or **Requests** tab. 3. Enter the member’s email address into the email field provided. 4. (Optional) Assign a role to the new member. If no role is selected, then the member is assigned the **Viewer** role by default. 5. Click **Send invitation**. The invited user appears under the **Invitations** tab until they accept. Once accepted, they automatically move to the **Members** tab. ![Managing organization members](/images/account/managing-members-invites.png) ## Add a verified domain for automatic or request-based onboarding You can simplify member onboarding by allowing users with your organization's email domain to join automatically or request to join your Novu organization. 1. In the dashboard, go to **Settings** > **Organization** 2. Scroll to the **Verified Domains** section. 3. Click **Add Domain**. 4. Enter the email domain you want to verify (for example, `novu.co`) into the email field provided. 5. Click **Save**. 6. Confirm the verification process, if prompted. ![Add a verified domain for automatic or request-based onboarding](/images/account/add-a-verified-domain.png) Once added: * Anyone who signs up using a verified domain can either join automatically or send a request to join, depending on your organization’s configuration. * These users are assigned the Viewer role by default. Only users with non-public domains (for example, not `gmail.com`) can be auto-joined. You must have at least one Owner from the domain before it can be verified. ## Revoke a pending invitation You can revoke an invitation if a user hasn't accepted it yet. Revoking an invite immediately makes the link invalid. 1. In the dashboard, go to **Settings** > **Team**. 2. Click the **Invitations** tab to view all pending invites. 3. In the **Actions** column, click the (⋯) menu next to the user whose invite you want to revoke. 4. Click **Revoke invitation** ![How to revoke a pending invitation](/images/account/revoke-invitation.png) You can still send a new invitation after revoking the previous one. ## Manage join requests Users with a verified email domain can request to join your Novu organization. Only owners can approve or decline these requests. 1. In the dashboard, go to **Settings** > **Team**. 2. Open the **Requests** tab. 3. Review the list of users who have requested to join your organization. 4. In the **Actions** column, click the (⋯) menu next to their name. 5. Accept or decline their request. Users who are approved will be added to your organization and assigned the Viewer role by default. ## Update a member's role You can update a member's role to control what they can access and manage within your organization. Only Owners can change roles. 1. In the dashboard, go to **Settings** > **Team**. 2. Click the **Members** tab. 3. Find the user whose role you want to change. 4. In the **Role** column, click the list next to their current role. 5. Select a new role from the list of [available account roles](/platform/additional-resources/roles-and-permissions). ![Updating a member's role](/images/account/managing-members-update-roles.png) The last owner of an organization cannot have their role changed. Every organization must have an owner. ## Remove a member from your organization You can remove members who no longer need access to your Novu organization. This action immediately revokes their access to your organization dashboard and signs them out. Only owners can perform this action. 1. In the dashboard, go to **Settings** > **Team**. 2. Click the **Members** tab. 3. Find the member you want to remove. 4. Click the action menu (⋯) next to their name. 5. Click **Remove member**. ![Removing members from your organization](/images/account/managing-members-remove.png) The owner who created the organization cannot be removed. Members can also leave your organization. file: ./content/docs/platform/account/roles-and-permissions.mdx # Roles and permissions Learn how roles and permissions are managed within Novu organizations Novu uses a role-based access control (RBAC) model at the [organization level](/platform/how-novu-works#organization-and-environments). Each member in the organization is assigned a role that determines the actions they can perform within the Novu dashboard. Every user can belong to more than one organization, each with separate configurations and permissions. When you invite a team member to your organization, you can assign them a role that determines the actions they can perform within that organization. You can later update their role from the [Team section](https://dashboard.novu.co/settings/team) in your organization settings. This feature is available to users on the Team and Enterprise pricing plans, and it is supported on both the new dashboard and the legacy dashboard. Below is an overview of the four roles available in Novu: * **Owner**. For your primary account administrator. This role has full access across the platform, including organization-level settings, billing, API keys, environment management, and user administration. Each Novu organization must have at least one owner. * **Admin**. For users who manage configuration and operational aspects of Novu. Admins can manage workflows, integrations, subscribers, environments, and message logs. They do not have access to billing. * **Author**. For users who design and update notification workflows. Authors can create and modify workflows, preview steps, manage topics, and trigger events. They do not have access to organization-level settings, billing, or member management. * **Viewer**. For read-only users. Viewers can view workflows, environments, logs, and messages, but cannot create, update, or delete resources. ## Roles and permissions table Below is a detailed table showing which permissions are associated with each role. Legend: * 📖 - Read access * ✏️ - Write (full access: read, write, delete) * ❌ - No access | Permission | **Viewer** | **Author** | **Admin** | **Owner** | | ------------------------------ | ---------- | ---------- | --------- | --------- | | Create and manage environments | ❌ | ❌ | ✏️ | ✏️ | | Create and manage messages | 📖 | ✏️ | ✏️ | ✏️ | | Create and manage topics | 📖 | ✏️ | ✏️ | ✏️ | | Create and manage webhooks | 📖 | 📖 | ✏️ | ✏️ | | Create and manage workflows | 📖 | ✏️ | ✏️ | ✏️ | | Manage API keys | ❌ | ❌ | ✏️ | ✏️ | | Manage billing | ❌ | ❌ | ❌ | ✏️ | | Manage bridges | ❌ | ❌ | ✏️ | ✏️ | | Manage custom domains | 📖 | 📖 | ✏️ | ✏️ | | Manage integrations | 📖 | 📖 | ✏️ | ✏️ | | Manage organization metadata | ❌ | ❌ | ❌ | ✏️ | | Manage organization profile | ❌ | ❌ | ❌ | ✏️ | | Manage partner integrations | 📖 | 📖 | ✏️ | ✏️ | | Manage subscribers | 📖 | ✏️ | ✏️ | ✏️ | | Trigger events | ❌ | ✏️ | ✏️ | ✏️ | | View and manage team members | 📖 | 📖 | 📖 | ✏️ | | View notifications | 📖 | 📖 | 📖 | 📖 | file: ./content/docs/platform/account/sso.mdx # SAML SSO Learn how to enable Single Sign-On (SSO) for your account. import { Card, Cards } from 'fumadocs-ui/components/card'; This feature is only available to Enterprise customers on our Cloud platform. For more information, please visit our [Enterprise Plan](https://novu.co/pricing) page. Novu provides a SAML SSO (Single Sign-On) integration, enabling you to authenticate your organization’s users with your existing SAML infrastructure. Novu is compatible with any SAML 2.0 compliant Identity Provider (IdP), including but not limited to: Okta Microsoft Entra ID (formerly Azure AD) Google Workspace Any other SAML 2.0 compliant IdP To enable SAML SSO for your account, please reach out to your account manager or contact us at [sales@novu.co](mailto:sales@novu.co). file: ./content/docs/platform/additional-resources/errors.mdx # Common errors Understand common errors in your notification workflows and how to resolve them The Activity Feed page in the Novu dashboard displays all the events that occur when a workflow is triggered. Each triggered workflow includes a log for every step, showing whether a notification failed and why. Below are some common errors that you might encounter. ## Webhook URL for the chat channel is missing This happens when the subscriber is missing the required chat channel provider credentials, specifically, the webhook URL. Chat providers require a valid webhook URL to receive notifications. **Solution:** Ensure that the correct webhook URL is configured for the subscriber’s chat provider. [Read more about updating webhook URLs](/platform/integrations/chat#update-credential-webhookurl). ## All chat channels failed to send the message When the workflow engine attempts to send a chat message, it tries all active chat providers. This error occurs when every configured provider fails to deliver the message. **Solution:** Check the individual chat provider configurations and logs to identify why each failed. Verify credentials, webhook URLs, and provider availability. ## Subscriber is missing a phone number To send an SMS, the subscriber must have a valid phone number. If this field is missing, then the system cannot deliver the message. **Solution:** Update the subscriber profile with a valid phone number. Similar errors may include missing recipient details. [Learn how to create and update subscriber attributes](/platform/concepts/subscribers). ## Subscriber does not have an active integration The system couldn't send the message because the required chat or push providers are not configured in the integration store. As a result, the subscriber lacks the credentials needed to use these channels. **Solution:** Configure the appropriate chat or push provider in the integration store. This allows the subscriber to inherit the necessary credentials. ## Subscriber does not have a configured channel This error indicates that the subscriber is not associated with any configured credentials for chat or push channels. **Solution:** Ensure the subscriber is mapped to an integration that supports the desired channel. [Read more on configuring chat and push channels](/platform/integrations/push). ## Message content could not be generated This usually happens due to invalid characters in the step editor content. For example, if it’s an email step, it could be caused by a syntax error in the email editor. **Solution:** Review the content in the step editor and ensure it contains valid syntax and characters. Correct any formatting or special character issues. ## Novu's provider limit has been reached Each environment in Novu is provisioned with demo providers for email and SMS channels. These demo providers are limited to 300 emails and 20 SMS per month. **Solution:** Upgrade to a production provider or reset the limit for testing. Avoid using demo providers for production use cases. ## Bridge execution failed This error occurs when the bridge URL is invalid or unreachable during the execution of a workflow step. **Solution:** Verify that the bridge URL is correctly set, publicly accessible (if needed), and responds within a reasonable timeout. file: ./content/docs/platform/additional-resources/glossary.mdx # Glossary Definitions ## Introduction In this section, you'll find a list of key terms, their definitions and various concepts associated with Novu. Familiarising yourself with these will help you understand and use Novu better. They will help you navigate our docs more effectively and utilise Novu to its maximum potential. If you have any questions or need further clarification on any of the terms listed above, please feel free to reach out to our support team or join our community! ## List of key terms and definitions ### Actor An `actor` refers to a user or subscriber who initiates actions that trigger events within the system. Each actor is uniquely identified by an "actorId," also known as "subscriberId". Actors hold user-related variables and can enhance notifications by allowing their avatars to be displayed. ### Channels Novu lets you send notifications across different communication mediums, including emails, in-app messages, push notifications, SMS, and chat. Each of these five communication mediums is referred to as a notification 'channel'. ### Delay Actions Delay actions introduce time intervals between workflow steps, optimizing message delivery timing. ### Digest Engine The digest engine aggregates multiple trigger events into a single message, ensuring efficient communication. ### Environments Novu runs all your requests in the context of an environment. By default, Novu creates two environments: 1. Development environment: For testing purposes and validating notification changes 2. Production environment: Your live environment (read-only, changes must be promoted from development) Data associated with an environment includes: * Subscribers (can't be promoted to production) * Workflows (can be promoted to production) * Messages * Execution logs * Connected integrations (can't be promoted to production) * Notification feeds (can be promoted to production) * Brand-related assets and settings ### Layouts Layouts are HTML designs or structures to wrap the content of email notifications. Layouts can be manipulated and assigned to new or existing workflows within the Novu platform. ### Notification A brief message or alert that informs users about events, updates, or some other information. ### Organizations Organizations allow separation of notifications across multiple products, managed through the Novu web dashboard. ### Providers Providers are responsible for handling message delivery across various channels. Novu currently supports multiple notification channels, each with its own set of providers. * Chat: Discord, MS Teams, Slack, Zulip * Email: Sendgrid, Amazon SES, Brevo, Resend, SparkPost, Postmark, Mailjet, Mailtrap, Plunk, Braze, Mailersend, Outlook 365, Mailgun, Mandrill, Netcore, Infobip, Custom SMTP * SMS: Twilio SMS, SMS77, Africa's Talking, Infobip, Nexmo, Plivo, Sendchamp, AWS SNS, Telnyx, Termii, Firetext, Gupshup, Clickatell, Azure SMS, BulkSMS, SimpleTexting, MessageBird * Push Notification: Firebase Cloud Messaging (FCM), Expo Push, Apple Push Notification Service (APNS), One Signal, Pushpad, Pusher Beams, Push Webhook * Inbox: React component, Angular component, Vue component, Web component, iFrame embed, Custom styling, Headless Inbox ### Step Filter Step filters customize workflow by specifying notification criteria, enhancing communication efficiency. ### Subscribers Subscribers are entities designated to receive the notifications you send. Each subscriber in Novu is uniquely identified by their `subscriberId`. ### Team members Members of a team have access to the Novu web dashboard. This allows you to have individuals work on and manage templates and notifications. ### Topics Topics facilitate bulk notifications to multiple subscribers simultaneously, streamlining communication. ### Workflow Workflow templates define the flow of messages sent to subscribers. file: ./content/docs/platform/additional-resources/legacy-documentation.mdx # v0.x Documentation undefined # v0.x documentation The v0.x documentation is available at [https://v0.x-docs.novu.co/getting-started/introduction](https://v0.x-docs.novu.co/getting-started/introduction). file: ./content/docs/platform/additional-resources/mcp.mdx # Model Context Protocol (MCP) Enable AI agents to securely access and interact with your Novu notification infrastructure using the Model Context Protocol (MCP). import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; ## What is Model Context Protocol? MCP is a protocol that enables AI tools and applications to connect with Novu's data and services in a secure, standardized way. It provides a structured method for AI models to: * Find and retrieve Novu data (subscribers, workflows, notifications, etc.) * Access specific tools and functionality provided by Novu * Maintain context about your Novu workspace when working with AI assistants * Trigger workflows and manage notification preferences ## How MCP Works Novu hosts a remote MCP server that handles requests from AI tools and provides access to Novu data through a secure interface. **Connection URLs:** * **US Region**: `https://mcp.novu.co/` (Streamable HTTP - Recommended) * **EU Region**: `https://mcp.novu.co/?region=eu` (Streamable HTTP - Recommended) * **Legacy SSE**: `https://mcp.novu.co/sse` (also supported in both regions but not recommended) When an AI tool connects to Novu's MCP server, authentication verifies the user's API key and permissions, then the tool can access relevant Novu data and functionality. ## Available Tools The Novu MCP Server provides **13 tools** for interacting with the Novu API: ### Core Operations * **get\_api\_key\_status** - Check API key status and region configuration * **get\_environments** - List development and production environments ### Workflow Management * **get\_workflows** - List all workflows with names and identifiers * **get\_workflow** - Get detailed workflow configuration and payload structure * **trigger\_workflow** - Execute workflows with custom data for specific subscribers * **create\_workflow** - Create new workflows with comprehensive configuration including steps, channels, and validation * **update\_workflow** - Update existing workflows with modified steps, preferences, and configuration * **delete\_workflow** - Delete workflows by their unique identifier (irreversible action) ### Subscriber Management * **find\_subscribers** - Search subscribers by email, phone, name, or ID ### Notification Analytics * **get\_notifications** - Get notifications with filtering by channels, workflows, dates * **get\_notification** - Get specific notification details with execution logs ### Preference Management * **get\_subscriber\_preferences** - Get subscriber notification preferences for all channels * **update\_subscriber\_preferences** - Update subscriber channel preferences globally or per workflow ## Setup ### Prerequisites * Novu API key from [dashboard.novu.co](https://dashboard.novu.co/settings/api-keys) * MCP-compatible AI assistant (Claude Desktop, Claude.ai, Cursor, Windsurf) * Node.js installed (for Claude Desktop users) ### Authentication 1. Get your API key from the [Novu Dashboard](https://dashboard.novu.co/settings/api-keys) 2. Identify your region from the dashboard URL: * `dashboard.novu.co` = **US region** * `eu.dashboard.novu.co` = **EU region** ### Configuration **File Location:** * **macOS**: `~/Library/Application\ Support/Claude/claude_desktop_config.json` * **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` ```json { "mcpServers": { "novu": { "command": "npx", "args": [ "mcp-remote", "https://mcp.novu.co/", "--header", "Authorization:Bearer ${NOVU_API_KEY}" ], "env": { "NOVU_API_KEY": "your-novu-api-key-here" } } } } ``` For **EU region**: ```json { "mcpServers": { "novu": { "command": "npx", "args": [ "mcp-remote", "https://mcp.novu.co/?region=eu", "--header", "Authorization:Bearer ${NOVU_API_KEY}" ], "env": { "NOVU_API_KEY": "your-novu-api-key-here" } } } } ``` Navigate to **Settings** → **Integrations** → **+ Add Integration**: * **Name**: `Novu` * **URL**: `https://mcp.novu.co/` (US) or `https://mcp.novu.co/?region=eu` (EU) * **Headers**: `Authorization: Bearer your-novu-api-key` In **Settings** → **Extensions** → **MCP Servers**: ```json { "mcpServers": { "novu": { "url": "https://mcp.novu.co/", "headers": { "Authorization": "Bearer your-novu-api-key" } } } } ``` **Standard MCP Configuration:** * **URL**: `https://mcp.novu.co/` (US) or `https://mcp.novu.co/?region=eu` (EU) * **Authentication**: Header `Authorization: Bearer your-novu-api-key` * **Protocol**: Streamable HTTP (MCP over HTTP) ## Testing Your Setup After configuration, test with these commands: ### Basic Operations ``` "Check my Novu API key status" "Show me all my notification workflows" "Find subscriber with email user@example.com" ``` ## Troubleshooting **Common Issues:** * **Authentication errors**: Verify API key is valid and has no extra spaces * **Empty results**: Check you're using the correct region (US/EU) * **Connection issues**: Restart your AI assistant after configuration changes **Test connectivity:** ```bash npx @modelcontextprotocol/inspector ``` Connect with Transport Type: Streamable HTTP, URL: `https://mcp.novu.co/`, Headers: `Authorization: Bearer your-api-key` *** For additional help, check the [Novu documentation](https://docs.novu.co) or join our [Discord community](https://discord.novu.co). file: ./content/docs/platform/additional-resources/security.mdx # Security and Compliance Learn about Novu security certifications, compliance standards, data residency, and privacy policies ## How to Request SOC and ISO Reports You can access our compliance reports and certifications directly from our Trust Center at [trust.novu.co](https://trust.novu.co). The Trust Center provides self-service access to: * SOC 2 Type II report * ISO 27001 certification * HIPAA compliance documentation * Security policies and procedures * Live compliance controls status Simply visit [trust.novu.co](https://trust.novu.co) to request and download any security documentation you need. ## Compliance Certifications ### SOC 2 Type II Novu Cloud is SOC 2 Type II compliant. We have completed penetration tests, security training, evidence collection, and follow secure development lifecycle (SDL) practices. You can see live control updates on our [Trust Center](https://trust.novu.co/). ### ISO 27001 Novu Cloud is ISO 27001 compliant. We have completed both Stage 1 and Stage 2 audits and fully defined ISMS requirements. This includes: * Creating comprehensive organization processes * Defining organization risk assessment policies * Building Incident Response & Disaster Recovery plans ### HIPAA Novu Cloud is HIPAA compliant and we offer Business Associate Agreements (BAA) for customers who require them. This enables healthcare organizations and their partners to use Novu while maintaining compliance with healthcare data protection requirements. ### GDPR Yes, Novu is fully GDPR compliant. You can see the complete compliance report on our [Trust Center](https://trust.novu.co/). Novu provides separate data residency options in both the EU and the US to support your compliance needs. ## Data Residency ### Available Regions Novu Cloud is available in the following regions: | Region | Location | | --------------- | ----------------------- | | **US** | Virginia, United States | | **EU** | Frankfurt, Germany | | **UK** | United Kingdom | | **Singapore** | Singapore | | **Australia** | Australia | | **Japan** | Japan | | **South Korea** | South Korea | As part of our GDPR compliance, you can choose which region your data resides in when creating your account. Enterprise regions (UK, Singapore, Australia, Japan, South Korea) are available on enterprise plans. ### Switching Regions To maintain data residency integrity, we cannot copy or move data between data warehouses in different regions. If you need to switch regions, please contact us at [sales@novu.co](mailto:sales@novu.co) to discuss your options. ### Self-Hosted and Hybrid Options * **Open Source**: You control where your data is stored * **Novu Hybrid-Cloud**: We help you deploy within your selected network infrastructure ## Data Storage and Retention By default, data is stored using the following retention periods: | Data Type | Free | Pro | Team | Enterprise | | ------------------ | ------- | ------- | ------- | ---------- | | Activity Feed Logs | 24 hrs | 7 days | 90 days | Custom | | Inbox Messages | 30 days | 90 days | 90 days | Custom | | Other Messages | 30 days | 90 days | 90 days | Custom | If you need to delete specific data or information, reach out to us at [support@novu.co](mailto:support@novu.co). ## Regulatory and PII Concerns We regularly work with large enterprises and are happy to provide guidance on various compliance requirements. Our compliance reports and certifications are available through our [Trust Center](https://trust.novu.co/) to help ease your security and legal team's review process. If you have specific concerns about PII, you have several options: * Use our **open source** version for full control * Choose the **Novu Hybrid-Cloud** enterprise plan * Contact us at [sales@novu.co](mailto:sales@novu.co), [support@novu.co](mailto:support@novu.co), or via [Discord](https://discord.novu.co) ## Reporting Security Vulnerabilities We are committed to our users' data security and highly appreciate responsible disclosure of security vulnerabilities. To report a security issue: * Submit a [GitHub security advisory](https://github.com/novuhq/novu/security/advisories/new) * Email us at [security@novu.co](mailto:security@novu.co) file: ./content/docs/platform/concepts/integrations.mdx # Integrations Learn how Novu integrations connect you to third-party providers In Novu, integrations are configured connections to third-party services that deliver notifications across supported channels, which are email, SMS, push, and chat. The In-App channel is handled internally by Novu and does not require a third-party integration. Each integration connects Novu to a specific provider, which workflows use to deliver notifications over the related channel. ## How integrations fit into the notification flow Workflows in Novu handle the logic of what message to send and when. Integrations handle how that message is delivered by routing the message through a specific provider. When a workflow step executes a channel-based action (such as sending an email), Novu: * Resolves the correct integration for the channel. * Applies any configuration, such as sender name or title overrides. * Hands off the message to the provider through the integration. If no active integration exists for the required channel, the workflow step fails, and no message will be delivered. ## Providers vs integrations It’s helpful to distinguish between providers and integrations: * A provider is the third-party service responsible for sending messages (for example, Twilio, SendGrid, Slack). * An integration is your instance of that provider, configured with the necessary credentials and settings. You can have multiple integrations for the same provider, for example, using two different SendGrid accounts for staging and production environments. Novu allows you to manage these as separate, named integrations. Refer to this resource to see the full list of [providerIds used in Novu](https://github.com/novuhq/novu/blob/next/packages/framework/src/shared.ts#L103). Novu also lets you use [Trigger Overrides](/platform/integrations/trigger-overrides) to modify the default behavior of a message during workflow trigger, such as overriding the notification title or content, or using a different integration than the primary integration. This works alongside integrations to fine-tune delivery behavior. ## Environment-scoped behavior Each integration is scoped to a specific environment—such as development, staging, or production. This means you must configure separate integrations for each environment, even if they point to the same provider. This separation ensures that test messages don’t accidentally go to production users, and different credentials or delivery settings can be safely isolated across environments. ## Primary and active integrations Each environment can support multiple active integrations per channel, but only one can be marked as the primary integration for email and SMS channels. However, for push and chat channels, all active integrations are used in parallel to deliver messages. The primary integration serves as the default route when a message is sent over that channel unless explicitly overridden. You can update which integration is marked as primary or deactivate an integration entirely. ## Integration credentials Integrations require credentials to authenticate with third-party providers. These credentials are encrypted at rest and managed securely within Novu. Alongside credentials, integrations may also define metadata, such as sender addresses or credentials, including API keys or tokens, as well as optional configuration settings depending on the provider. ## Available integrations Novu supports a wide range of providers across different channels: file: ./content/docs/platform/concepts/notification-event.mdx # Notification event Learn about the Novu notifications lifecycle and the key entities that make up a notification. In Novu, notifications are the core building blocks of your product’s communication experience. When your user receives a message via an in-app alert, an email, or a push notification, what they’re seeing is the final output of a notification. Notifications represent the complete journey of a message triggered by an event in your application and delivered to a specific user across one or more channels. They encapsulate all the execution logic and delivery metadata in a single traceable unit. ## Notification lifecycle A notification doesn't exist in isolation, it’s the result of a sequence of interconnected entities working together. Here’s how it works in Novu: Notification lifecycle ### Event Something meaningful happens in your application, such as a user signing up, a password reset being requested, or a comment being posted. You emit this event to Novu using the [Event Trigger API](/api-reference/events/trigger-event). Each event contains: * A name that maps to a specific workflow (user\_signed\_up) * A payload with dynamic data * One or more subscribers * Optional overrides or metadata This is how your application tells Novu, “It’s time to send something.” ### Workflow Novu matches the incoming event to a predefined workflow. This workflow is your logic for determining: * What channels to use (email, in-app, SMS, etc.) * What message templates to render * When and how messages should be sent The workflow is where message personalization, conditional logic, and multi-channel orchestration happens. Refer to the [Workflows documentation](/platform/concepts/workflows). ### Notification Once the workflow starts executing, Novu creates a notification for each subscriber involved. This notification acts as the central entity tracking everything that happens - every job, message, and delivery status. It includes: * The subscriber details * The workflow (template) used * The event payload * Jobs executed * Messages generated * Delivery status updates and errors Think of it as the container for all activity triggered by a single event for a single user. In simpler terms, when a workflow is triggered to a subscriber, it creates an event. One event is one notification. ### Message Each channel step in the workflow results in one or more messages. These are the actual pieces of content sent to users' emails, push notifications, SMS, and in-app alerts. Messages are live entities, tracked in real-time, with full visibility into: * Rendering logic * Channel used * Delivery status * Provider responses or errors Each message has a unique ID and status, learn more about this in the [Messages API](/api-reference/messages/message-schema). ## Notification structure A notification in Novu is more than just a message, it’s a full record of what happened from the moment an event was triggered to when one or more messages were delivered or failed to deliver. Each notification encapsulates a set of properties that describe its creation, context, execution, and result. These properties are essential for debugging, analytics, and tracking user communications. ### Notification properties | Property | Description | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `transactionId` | A unique identifier for this specific notification execution run. Useful for tracing across APIs and logs. | | `template` | The workflow (template) that was executed. Defines the steps and channels used. | | `subscriber` | The user who received the notification. Includes subscriber ID and metadata. | | `payload` | Dynamic data passed in from the event, used to render personalized message content. | | `jobs` | A list of steps (jobs) that were executed as part of the workflow. Each job corresponds to a workflow action, for example, send email or run delay. | | `messages` | The individual messages generated for each channel. Contains delivery status, content, and provider response. | | `status` | The current execution state of the notification (`pending`, `completed`, `failed`, and so on). | These fields are accessible via the [Notifications API](/api-reference/notifications/list-all-events) and are also displayed in the Novu dashboard’s [Activity Feed](https://dashboard.novu.co/activity-feed) for visual traceability. ## Frequently asked questions These are some of the most frequently asked questions about notifications in Novu. ### What is the difference between a notification and an event? A notification is the result of an event. An event is the trigger of a workflow to a subscriber that causes a notification to be created. ### What is the difference between a notification and a message? One notification event can have multiple messages as per the workflow configuration. Messages are the actual "notification message" to the end user. file: ./content/docs/platform/concepts/preferences.mdx # Preferences Learn how to manage subscriber preferences in Novu. Novu provides a way to store subscriber preferences. This allows subscribers, your users, to specify and manage their preferences and customize their notifications experience. **Levels of preferences:** * Workflow channel preferences * Subscriber channel preferences per workflow * Subscriber global preferences ## Workflow channel preferences Each workflow has its own channel preferences. By default, all channel preferences are enabled. If disabled, the subscriber will not receive notifications for that channel step. Steps to manage workflow channel preferences: 1. Go to the [Workflows page](https://dashboard.novu.co/workflows) in Novu dashboard 2. Click the workflow you want to manage channel preferences for 3. A node-based editor will appear. On the right side of the editor, click the `Configure channel preferences` option 4. Click on the All Channels checkbox to enable or disable all channels for the workflow 5. You will be able to change the preferences for only those steps which are present in the workflow. Non existing channel steps will be disabled. 6. The `Mark as critical` toggle will make this workflow critical. Read more about [critical workflows](#critical-workflows) If a workflow has only `in-app` and `email` steps, then it will have only `in-app` and `email` preferences. ![Workflow channel preferences](/images/concepts/preferences/workflow-channel-preferences.png) ### Critical workflows In some cases, you don't want the subscriber to be able to unsubscribe from mandatory notifications such as Account Verification, Password Reset, etc... In those cases, you can mark a workflow as `critical` in the workflow channel preferences. Critical workflows are not displayed in subscriber preferences, so subscribers cannot change preferences for that workflow. ## Subscriber global preferences Subscribers can set global channel preferences, which override individual settings. For instance, if there are 10 workflows, and a subscriber wants to disable SMS notifications for all of them, they can do so with via global preferences. ![Preferences](/images/inbox/preferences@2x.png) ## Subscriber channel preferences per workflow For each workflow, subscriber has its own channel preferences. Subscriber can manage these preferences from the {``}{" "} Preferences view. ![Subscriber channel preferences](/images/concepts/preferences/subscriber-workflow-preferences.png) Inbox displays only channels present in the current workflow. ## Priority of preferences Since there are three types of preferences, the priority order is as follows: Workflow channel preferences > Subscriber global preferences > Subscriber channel preferences per workflow Examples: 1. If the `email` channel is disabled in workflow channel preferences, global and subscriber preferences are ignored, and subscribers will not receive email notifications for this workflow. 2. If the `in-app` channel is enabled in workflow channel preferences but the workflow is marked as critical, subscribers cannot change their preferences and will always receive in-app notifications. 3. If both `chat` and `email` channels are enabled in the workflow but `email` is disabled in subscriber global preferences, the subscriber will receive only chat notifications for this workflow. ## Subscriber preferences APIs Subscriber preferences can be retrieved and updated using following APIs: file: ./content/docs/platform/concepts/subscribers.mdx # Subscribers Learn what a subscriber is in Novu, how they’re identified, and how they fit into the notification system. import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; In Novu, a subscriber represents a user or system entity that is intended to receive notifications. Subscribers are central to Novu’s delivery model: workflows are triggered with one or more targeted subscribers, and all delivery logic, such as channel routing, preferences, and personalization is applied at the subscriber level. ## How Novu identifies a subscriber Each subscriber is uniquely identified in Novu by a `subscriberId`. This ID is defined by your application and serves as the reference point for all subscriber-related operations whether sending messages, retrieving preferences, or managing user data. Unlike email addresses or phone numbers, which may change or be shared across users, the `subscriberId` must remain stable and unique within your system. It acts as the anchor that connects a subscriber’s profile, activity history, and delivery settings across all channels and workflows. Use your application's internal user ID as the `subscriberId` to ensure consistency across your systems. ## Subscriber metadata and profile structure A subscriber’s profile holds all relevant data Novu uses to personalize, deliver, and manage notifications. These fields can power dynamic content in your templates, define conditional logic in workflows, and control which channels a subscriber can receive notifications through. All metadata tied to a subscriber is centralized and accessible via API or dashboard. This structure ensures that when notifications are triggered, Novu references the most up-to-date context for delivery and personalization. These data includes: ### User data Data stored in the subscriber object that you can easily access in your notification templates. This contains basic info such as `email`, `phone`, `firstName`, `locale` and others. This data is fixed and structured. ### Custom data Apart from the above fixed structured user data, any unstructured custom data such as user's `address`, `membershipLevel`, `preferredTopics`, or `companySize` can also be stored in the `data` field using key-value pairs. ### Channel specific credentials To deliver messages through push or chat-based channels, Novu also supports storing delivery credentials on the subscriber profile: * `deviceTokens`: Used to target mobile devices via push notifications. * `webhookUrl`: Used by chat providers such as, Slack, Discord to reach the subscriber. These fields ensure Novu can deliver messages reliably to all supported destinations, even when the channel requires platform-specific configuration. Learn more about subscriber attributes and schema in the [Subscribers API](/api-reference/subscribers/subscriber-schema). ## Subscriber creation in Novu Before you can send notifications, a subscriber must exist in Novu. Asides from manually creating a subscriber via the Novu dashboard, Novu supports multiple approaches to subscriber creation depending on your application’s architecture and user lifecycle. ### Just in time Novu allows you to create a subscriber automatically at the moment a notification is triggered. If the subscriber doesn't already exist, Novu uses the information provided in the workflow trigger to create the subscriber on the fly. If the subscriber exists, Novu updates the stored data with the latest values. This approach is useful when: * Your system does not store subscriber profiles ahead of time. * Notifications are sent during real-time events like sign-ups or transactions. * You want to keep the creation and delivery logic tightly coupled. ```typescript import { Novu } from "@novu/api"; const novu = new Novu({ secretKey: "", }); await novu.trigger({ to: { subscriberId: "subscriber_unique_identifier", firstName: "Albert", lastName: "Einstein", email: "albert@einstein.com", phone: "+1234567890", }, workflowId: "workflow_identifier", }); ``` ### Ahead of trigger Alternatively, you can create and store subscriber profiles ahead of time — typically during onboarding, registration, or data sync events. This approach allows you to manage subscriber preferences, enrich profiles, and inspect delivery readiness before any notification is triggered. ```typescript import { Novu } from "@novu/api"; const novu = new Novu({ secretKey: "", }); await novu.subscribers.create({ subscriberId: "subscriber_unique_identifier", firstName: "Albert", lastName: "Einstein", email: "albert@einstein.com", phone: "+1234567890", data: { address: "123 Main St, Anytown, USA", membershipLevel: "Gold", preferredTopics: ["News", "Sports"], }, }); ``` This is recommended when: * You want to decouple user creation from notification logic. * You rely on persistent user data or prefer strict validation before delivery. * You plan to use advanced segmentation or preference-based delivery. ### Bulk Import For scenarios like data migration, syncing large lists, or preloading subscribers, Novu supports bulk creation. This is especially useful when integrating with existing systems or importing subscriber data from external sources. Bulk create method supports creating up to 500 subscribers at once. ```typescript import { Novu } from "@novu/api"; const novu = new Novu({ secretKey: "", }); await novu.subscribers.createBulk({ subscribers: [ { subscriberId: "subscriber_unique_identifier_1", firstName: "Albert", lastName: "Einstein", email: "albert@einstein.com", phone: "+1234567890", }, { subscriberId: "subscriber_unique_identifier_2", firstName: "Isaac", lastName: "Newton", email: "isaac@newton.com", phone: "+1234567891", }, ], }); ``` ## Where to manage subscriber data Subscriber data in Novu can be managed from the Novu dashboard or using the [Subscribers API](/api-reference/subscribers/subscriber-schema). Both offer full access to view, update, and organize subscriber profiles, but they serve different use cases depending on your requirements. ### Dashboard The Novu dashboard provides a visual interface for exploring and editing subscriber data. It’s useful for: * Searching and filtering subscribers by ID, email, phone and name. * Inspecting user profiles, including structured fields, custom data, topic subscriptions and channel preferences. * Performing manual updates or troubleshooting delivery issues This is ideal for non technical team members responsible for managing subscribers or teams that want to audit or manage subscriber data without relying on code. ### API For programmatic control, the Novu API offers endpoints to create, update, delete, and retrieve subscriber records at scale. It supports: * Automated onboarding or sync processes * Bulk operations such as imports or exports * Integration with external systems like CRMs or identity platforms Use the API when managing subscribers is part of your backend workflows or when changes need to happen dynamically based on user actions. ## Subscriber preferences and personalization Novu allows each subscriber to define how they want to receive notifications. These preferences influence both the delivery channels and the types of messages a subscriber will receive. ### Global preferences Subscribers can configure preferences that apply across all workflows. These include: * Opting in or out of specific channels, for example, email, SMS, push, or in-app * Disabling notifications altogether These global settings act as a default and are respected unless explicitly overridden in specific workflows. ### Subscriber workflow specific preferences In some cases, a subscriber may want to receive certain notifications but only through specific channels. Novu supports fine-grained overrides at the workflow level. This allows you to: * Adjust channel preferences per notification type * Honor granular user choices while maintaining global defaults ### Template personalization Subscriber data, both structured fields such as `firstName`, `email` and custom data can be used to personalize templates. This enables dynamic content such as: * Greeting users by name * Including location-based content * Adjusting language or tone based on locale or membership level Subscriber preferences and metadata personalization ensure that each subscriber receives relevant, well-targeted messages through the channels they care about. ### Subscriber API reference ## Frequently asked questions These are some of the most frequently asked questions about subscribers in Novu. ### Can two subscribers have the same email address? Yes, two subscribers can have the same email address, phone number, or any other attributes. However, each subscriber must have a unique subscriberId. ### Do I have to use subscriberId as same as the system userId? No, you don't need to use the same subscriberId as the system userId. You can use any unique ID as subscriberId. Novu recommends using userId as subscriberId to avoid any confusion. Some of our customers use a pattern like `auth0|userId` as a value for `subscriberId`. ### Can a notification be sent without adding a subscriber? No, a notification cannot be sent without adding a subscriber. A subscriber is an entity to which notification messages are sent. You must add a subscriber to Novu before triggering the workflow. ### How do I migrate millions of users to Novu? To migrate millions of users to Novu, use the [Bulk Create Subscribers](/api-reference/subscribers/bulk-create-subscribers) API endpoint. This endpoint allows you to create multiple subscribers in bulk. ### Can subscriber credentials for chat and push channels be added when creating a new subscriber? Subscriber credentials for Push and Chat channel providers can be added while creating a new subscriber using the `channels` field in the [create subscriber](/api-reference/subscribers/create-a-subscriber) request payload. file: ./content/docs/platform/concepts/tenants.mdx # Multi-tenancy Learn about how to implement multi-tenant notifications in Novu import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; Multi-tenancy is a common use case for a lot of companies that have multiple organizations that use their applications. In some cases, there is a need to separate the behavior of the notification system depending on the individual tenants. Tenants are also commonly called workspaces or organizations. Some of the common multi-tenancy use cases are: * Group subscribers notification feeds by the tenant * Adjust the content of the notification depending on the tenant ## How to implement multi-tenancy in Novu Novu supports multi-tenancy out of the box, the most simple way to implement tenant separation is by prefixing the subscriber identifier with the :tooltip\[tenant identifier]{label="The tenant identifier is a unique identifier for the tenant in your application. Usually the tenant id in your database."} ```typescript const subscriberId = `${tenantId}:${userId}`; await novu.trigger({ workflowId, to: { subscriberId, }, payload: { tenantId, }, }); ``` ### Tenant in Inbox When using the Inbox feature, you can use the tenant identifier to group notifications by tenant. ```tsx import { Inbox } from "@novu/react"; function InboxComponent({ tenantId, userId }) { return ; } ``` Each subscriber in a tenant will have it's own unique inbox feed, including a separate preference configuration set. ## Adjusting notification content based on tenant When triggering a notification, you can pass a custom tenant object or identifier and use it to manipulate workflows or content. ```typescript const subscriberId = `${tenantId}:${userId}`; await novu.trigger({ workflowId, to: { subscriberId, }, payload: { tenant: { id: tenantId, name: "Airbnb", logo: "https://airbnb.com/logo.png", primaryColor: "red", } }, }); ``` The tenant object will be available to use in the workflow editor as a variable for the following areas: * Content (use `{{payload.tenant.name}}` to display the tenant name in an email or any other channel) * Step Conditions (use `{{payload.tenant.id}}` to conditionally execute a step based on the tenant) * Use the Inbox [data object](/platform/inbox/react/components/inbox#data-object) to filter notifications by tenant. ## Frequently asked questions The following are the frequently asked questions about multi-tenancy in Novu. ### Can I use a different delivery provider for each tenant? Currently, we do not support using a different delivery provider for each tenant. You can reach out to [support@novu.co](mailto:support@novu.co) in case this is a feature required for your use case. ### Can I specify different workflow preferences for each tenant? We don't support specifying different workflow preferences for each tenant. You can reach out to [support@novu.co](mailto:support@novu.co) in case this is a feature required for your use case. file: ./content/docs/platform/concepts/topics.mdx # Topics Learn how topics work in Novu and how they help you organize and target groups of subscribers efficiently. In Novu, a *topic* is a way to group subscribers under a shared label so that you can broadcast a message to many users with a single workflow trigger. This fan-out mechanism removes the need to manage individual subscriber IDs for each notification, streamlining scenarios like product announcements, feature rollouts, or status updates. ## Topics identifiers Each topic is uniquely identified by a topic key, a permanent, internal reference in your system. You can also assign a name to help describe its purpose, for example, "Task Updates" or "Comment Thread Subscribers". The `topicKey` must be unique and cannot be changed after creation; Novu enforces this uniqueness behind the scenes. Example: `task:taskId` or `post:postId`. Common use cases for topics: * Notifying all members of a specific team or project * Messaging users who commented on a post * Updating subscribers to a specific product or feature ## How topics fit into Novu's model Novu’s notification system is built around workflows, which are triggered for one or more recipients. Topics act as a special type of recipient, representing a group of subscribers instead of an individual. When you trigger a workflow using a topic: * The `to` field accepts the topic’s key. * Novu looks up all subscribers assigned to the topic. * A separate workflow event is created for each subscriber. Topics let you target large groups with a single API call, removing the need to loop through subscribers or send multiple workflow triggers. Topics don’t replace individual targeting. You can still trigger workflows for specific subscribers or arrays of subscribers when needed. ## Dynamic and decoupled grouping Once a topic is created, you can assign or remove subscribers at any time. Subscribers don’t need to know they belong to a topic, and it doesn’t affect their personal notification preferences. Topics are dynamic and reflect real-time states. For example, a topic might include: * Users who are currently watching a post * Team members assigned to a project This makes topics especially useful for modeling dynamic relationships in your application. ## Scalability and limits Topics are designed for high-volume, high-efficiency use cases: * Each topic supports up to 100,000 subscribers. * A separate workflow event is created for each subscriber when a workflow is triggered to the topic ## Autogenerated topics Novu supports on-the-fly topic creation. If you attempt to add subscribers to a topic key that doesn't exist for the selected environment and organization, Novu will automatically create a new topic named `Autogenerated-` and assign the subscribers to it. This auto-generated topic can then be renamed and managed just like any other topic created manually. Topics can be managed from [Novu dashboard](https://dashboard.novu.co/topics) or using [Topics APIs](/api-reference/topics/topic-schema) ## Trigger workflow As mentioned above, a workflow can be triggered to a topic in the same way as it is triggered to subscribers. ### To a single topic To trigger a workflow to a topic, use `to` field with type `Topic` and topicKey. ```ts import { Novu } from "@novu/api" const novu = new Novu({ secretKey: "", }); await novu.trigger({ to: { type: "Topic", topicKey: "topic_key" }, workflowId: "workflow_identifier", }); ``` ```bash curl -L -g -X POST 'https://api.novu.co/v1/events/trigger' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: ApiKey ' \ -d '{ "name": "workflow_identifier", "to": { "type": "Topic", "topicKey": "topic_key" } }' ``` ### To multiple topics `to` field also accepts array of topics. This is useful when you want to trigger a workflow to multiple topics at once. ```ts import { Novu } from "@novu/api" const novu = new Novu({ secretKey: "", }); await novu.trigger({ to: [ { type: "Topic", topicKey: "topic_key_1" }, { type: "Topic", topicKey: "topic_key_2" } ], workflowId: "workflow_identifier", }); ``` ```bash curl -L -g -X POST 'https://api.novu.co/v1/events/trigger' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: ApiKey ' \ -d '{ "name": "workflow_identifier", "to": [ { "type": "Topic", "topicKey": "topic_key_1" }, { "type": "Topic", "topicKey": "topic_key_2" } ] }' ``` ### Exclude actor When a workflow is triggered to a topic, notification is sent to all subscribers present in the topic. However, you can exclude a specific subscriber from receiving the notification by using the `actor` field. Here actor is the subscriberId of the subscriber you want to exclude. ```ts import { Novu } from "@novu/api" const novu = new Novu({ secretKey: "", }); await novu.trigger({ to: [{ type: "Topic", topicKey: "topic_key_1" }], workflowId: "workflow_identifier", actor: "actor_subscriber_Id" }); ``` ```bash curl -L -g -X POST 'https://api.novu.co/v1/events/trigger' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: ApiKey ' \ -d '{ "name": "workflow_identifier", "to": [ { "type": "Topic", "topicKey": "topic_key_1" } ], "actor": "actor_subscriber_Id" }' ``` ## Explore the topics APIs These are commonly used topics APIs. Explore all topics APIs on topics [api reference page](/api-reference/topics/topic-schema). ## Frequently asked questions Below are some of the most frequently asked questions about topics: ### Do topics override subscriber preferences? No. Topics only define delivery groups. Each subscriber’s individual notification preferences remain intact. ### Is a topic like a mailing list? Conceptually, yes. Topics group users to receive shared messages. However, topics are more dynamic and integrated into your application logic. ### Can a subscriber belong to multiple topics? Yes. Subscribers can be members of any number of topics. This allows overlapping targeting strategies (for example, `task-updates`, `project-X`, and `admin-notifications`). ### Can I reuse the same topic key across environments? Topic keys must be unique within each [environment](/platform/concepts/environments). You can reuse the same key (for example, `feature-release`) in different environments like staging and production. ### What happens if I trigger a workflow to a topic with no subscribers? The workflow is processed, but no notifications are delivered and, hence, this trigger is not counted for billing. file: ./content/docs/platform/concepts/trigger.mdx # Trigger Managing Trigger Events from Request to Processing ## Trigger Request A trigger request is the initial step in handling a trigger event. It contains crucial details such as the template identifier, a list of subscribers who will receive the notification, the payload of the notification, and any overrides that need to be applied. ## Trigger Endpoint Upon sending the request to the [event/trigger](/api-reference/events/trigger-event) endpoint, a series of essential steps are initiated: 1. **Subscriber Mapping and Validation:** The first step involves mapping and validating the subscribers for the specified event. This ensures that notifications are sent to the correct recipients. 2. **Workflow Validation:** Following subscriber validation, the workflow associated with the event is validated. This validation process considers factors such as the active status to determine if it meets the necessary criteria for processing. 3. **Attachment Upload:** Once the validation process is successfully completed, any attachments associated with the event are uploaded to the designated storage service. 4. **Event Queuing:** The trigger event, now enriched with mapped subscribers and attachment links, is appended to the **trigger event queue**. This queuing mechanism optimizes response times, ensuring efficient event processing. ## Trigger Event Processing When an event is picked up by the **trigger queue worker**, the processing phase begins. Here's what happens: * **Subscriber Upsert:** The worker validates the subscribers associated with the event and either creates or updates the subscriber with the information passed in the `to` object. * **Notification Entity Creation:** For each subscriber listed in the trigger event, a corresponding notification entity is created. This entity contains essential data related to the organization, template, subscriber, and event payload. * **Job Creation:** Based on the notification template's defined steps, jobs are generated. These jobs are responsible for carrying out specific tasks related to the event notification. Additionally, the notification entity is updated with a "channels" field generated from these steps, indicating the communication channels through which notifications will be sent. ## Jobs Jobs play a crucial role in the trigger event lifecycle. They are created based on the steps outlined in the workflow. ### Job Statuses | Status | Description | | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **PENDING** | This status is assigned to a job before it is added to the worker queue. It indicates that the job is waiting to be processed. | | **QUEUED** | After the initial validation and just before adding a job to the worker queue, it is set to `QUEUED`. This status signifies that the job is ready for processing but is awaiting its turn in the queue. | | **RUNNING** | When a job is picked up by a worker from the queue, its status is changed to `RUNNING`. This indicates that the job is currently being processed by a worker. | | **COMPLETED** | Once a job has been successfully executed and processed, its status is changed to `COMPLETED`. This signifies that the job has been successfully completed. | | **FAILED** | If a job encounters an issue during processing or execution, its status is changed to `FAILED`. This indicates that the job has not been successfully completed, and there may have been errors or problems during processing. | | **DELAYED** | The `DELAYED` status is applied to specific types of jobs, such as `digest` or `delay` jobs, to indicate that they are delayed and not immediately processed. For `digest` jobs, it means that the digesting process is running or scheduled for a later time. For `delay` jobs, it signifies that the job is set to be executed at a specified delay time. | | **CANCELED** | When a job is canceled for any reason, its status is set to `CANCELED`. This indicates that the job will not be processed further and is effectively removed from the processing queue. | | **MERGED** | The `MERGED` status is assigned to events that are part of a `digest`. It indicates that an event will be merged into the digesting event. In a digesting process, there is typically a primary or initial event that serves as the digesting event, and subsequent events are merged into it. Instead of having a separate `COMPLETED` status for these merged events, they are marked as `MERGED` to indicate their specific role in the digesting process. | | **SKIPPED** | The `SKIPPED` status is used in the context of backoff versions of digesting. In this scenario, the first event's digesting is skipped, and the second event takes on the digesting role. The `SKIPPED` status is applied to the first event's digesting, indicating that it was intentionally skipped in the digesting process. Subsequent events may be merged into the second event's digesting process, as explained with the `MERGED` status. The `SKIPPED` status helps differentiate the skipped event from others in the digesting sequence. | ## Frequently asked questions ### What is an event? An event is a request (for instance, an API call to [/v1/events/trigger](/api-reference/events/trigger-event)) that starts off an action in the Novu Workflow Engine. Events can make many different types of actions, including digests, delays, and sending notifications to various channels, as well as filters and user preference checks. ### How events are billed? If a workflow is triggered to a [subscriber](/platform/concepts/subscribers), then it is billed as 1 event. So if workflow is triggered to group of 15 subscribers, then it is billed as 15 events. Similary, if a workflow is triggered to a [topic](/platform/concepts/topics), Novu creates one event per topic subscriber, this means that a workflow trigger to a topic with 100 subscribers creates 100 events. ### If notifications were filtered out, are they still billed? Yes, they are still billed. The number of billed events is the number of subscribers or topic subscribers the workflow was triggered to, regardless of whether notifications were filtered out as per subscriber's [workflow channel preferences](/platform/concepts/preferences#workflow-channel-preferences), [global channel preferences](/platform/concepts/preferences#subscriber-global-preferences) or [step conditions](/platform/workflow/step-conditions). ### How events are billed for digested events? As a workflow triggered to one subscriber is billed as 1 event. if events are digested 15 times and a single digested notification is sent, then 15 events will be billed. file: ./content/docs/platform/concepts/workflows.mdx # Workflows Learn what workflows are and how they work in Novu. In Novu, a workflow is the blueprint that defines the end-to-end process for delivering a notification to one or more subscribers. It acts as the central logic layer for routing messages across different channels such as email, SMS, in-app, push, or chat and controlling what notification to send, when to send it and through which channels they are sent. You can think of a workflow as a CI/CD pipeline for notifications or an assembly line: * The event enters at the start. * The logic defines how it moves through conditions, waits, and transformations. * Each step performs a specific action. * At the end, the user receives one or more messages based on the outcome. Workflows can be simple: * When a user signs up, send them a welcome email. Or complex: * When a payment fails, wait 30 minutes, check the user type, then send an email and an in-app message. If no response in 24 hours, follow up with an SMS message. It can also be extended further than these depending on your use case. ## Workflow steps A workflow in Novu is built as a sequence of steps, each representing a distinct action or delivery mechanism. These steps are the building blocks of your notification logic. They determine what happens, when it happens, and how the message is sent to the user. You define and arrange steps using the Workflow Editor in the Novu dashboard, where each node on the canvas represents a single step in the execution flow. Each step operates independently, meaning that if one step fails, others can still succeed. This makes workflows both modular and fault-tolerant. ### Types of steps Each workflow can contain two types of steps: channel steps and action steps. #### Channel steps Channel steps deliver the actual notification through one of Novu’s supported channels. Each step contains its own notification template, content editor, and channel-specific settings. Supported channel types include: * Email (SendGrid, Mailgun, or SMTP) * In-App (Inbox component) * SMS (Twilio) * Push (FCM, OneSignal) * Chat (Slack, Microsoft Teams) A channel step only runs if: * The subscriber has `email` field for `email` step, `phone` field for `sms` step and required credentials for `push` and `chat` channel steps. * A valid integration is configured for that channel in the environment. #### Action steps These steps allow you to introduce logic and flow control into the workflow. They don’t send notifications, but instead alter the timing or shape of the delivery pipeline. Common action steps include: * **Delay**: Pause the workflow for a fixed amount of time before moving to the next step. * **Digest**: Group multiple similar events into a single message (for example “You have 5 new mentions”). ### Designing multi-channel sequences Because steps are chainable, you can build complex notification sequences such as: 1. Send an in-App message. 2. Wait 24 hours (Delay). 3. If the in-app message is unread, send an email. 4. If the subscriber is a premium user, then follow up with an SMS message. This allows Novu workflows to adapt to user behavior and preferences while keeping the configuration visual and testable. ## Workflows editor All workflows in Novu are defined within the Workflow Editor, accessible via the Novu dashboard. This visual builder let's you define the sequence of steps such as sending emails, SMS, or in-app notifications that happen when a notification event is triggered. Workflows created in the dashboard run via Novu’s Shared Bridge Endpoint, a centralized infrastructure that securely handles workflow execution for all customers. This means that your app doesn’t need to manage orchestration logic; Novu takes care of triggering, step execution, retries, and delivery flow behind the scenes. While most users rely on the Shared Bridge Endpoint, Novu allows advanced users to host their own Bridge application and define workflows in code using the [Novu Framework](/framework/overview). To learn more about building workflows using the workflow editor, filters, and personalizing templates, refer to the [Building Workflows documentation](/platform/workflow/overview). ## Workflow identifier Each workflow has a unique `identifier` used to reference it during API calls. While the workflow name can be changed, the workflow identifier becomes immutable after creating the workflow. Workflow identifiers must be unique within one environment and are typically lowercase with hyphens (for example, `order-confirmation`). When naming a workflow, choose a clear, descriptive name that reflects its purpose. This name helps teams distinguish between multiple workflows and maintain clarity as your notification system grows. ## Workflow status Workflows can exist in one of three states: | **Status** | **What it means** | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `active` | The workflow is ready to be triggered. Steps are valid, templates are complete, and all required integrations are connected. | | `inactive` | The workflow is paused. It cannot be triggered, but you can still edit its structure or steps. | | `action required` | The workflow contains one or more errors, such as missing required fields or unconnected channels. It can still be triggered. | ## Workflow tags Tags are labels that help you organize and group workflows. They don’t affect delivery, but they play a major role in how notifications are filtered, displayed, or managed by both your app and your users via the Inbox component. Use tags to: * Group related notifications, for example, `security`, `account-activity`, and `marketing`. * Support filtered views on your frontend; for example, show only promotional messages. * Categorize workflows for better team visibility and long-term maintainability. Think of tags as your notification system's folders; they help your team stay organized. ### Example use cases: * Show only `security` tagged notifications in a high-priority tab. * Let users mute or opt out of `marketing` tagged workflows via preferences. * Organize large numbers of workflows by function, region, or product area. ## Critical workflows Critical workflows are notifications that must always be delivered to users, regardless of their personal preferences. These workflows are essential to the user experience and system integrity, and users cannot disable them through notification settings. Marking a workflow as critical ensures that high-priority messages are delivered without exception. This is particularly important for notifications that carry security, financial, or access-related information, where missing a message could have some consequences. Examples of critical workflows include: * **Security alerts**: Password resets or suspicious login attempts. * **Invitations**: Granting users access to workspaces, projects, or systems. * **Onboarding flows**: Guiding new users through setup steps. * **Payment and billing updates**: Confirmations or failed transactions. * **Order and shipping notifications**: Providing delivery status or tracking details. ## Syncing workflows between environments In Novu, syncing a workflow means copying it from one environment (typically development or staging) to another (such as production). This is how you deploy workflow changes without having to rebuild them manually across environments. Instead of editing workflows directly in production, it's best practice to design and test them in a non-production environment first. Once validated, you can sync the workflow to production with a single action. ### When a workflow is synced: * A copy is created or updated in the target environment. * The workflow identifier remains the same, ensuring existing triggers continue to function correctly. * Any updates to the template, steps, or configuration are reflected in the destination environment. ### Syncing is supported across environments based on your plan: * Team and enterprise plans support syncing to any available environment. * Lower-tier plans support syncing only between development and production. ## Triggering workflows Every workflow in Novu is initiated by a trigger, which can be an external event or action that starts the notification flow. Triggers connect real-world events, such as a comment being posted or an order being placed, to a specific workflow using a unique identifier, the `workflowId`. When you trigger a workflow, you send event data, including subscriber information, payload details, and optional overrides, to Novu’s API. Novu then validates the event, processes it, and orchestrates notification delivery across your defined channels. Behind the scenes, Novu manages the full lifecycle: subscriber validation, workflow matching, attachment uploads (if applicable), queuing, job execution, retries, and status tracking. To learn more about triggering workflows via API, handling payloads, and understanding the request lifecycle, refer to [Triggering Workflows](/platform/concepts/trigger). ## Workflow activity feed After a workflow is triggered, Novu tracks the full execution process in the [Activity Feed](https://dashboard.novu.co/activity-feed). The feed gives you real-time visibility into each step, helping you debug issues, trace delivery problems, and understand how notifications behave in production. Each environment has its separate feed. You can filter the Activity Feed by workflow name, channel type, time period, or transaction ID to easily find and inspect specific workflow executions. ## Workflow channel preferences When setting up a workflow, you can define channel preferences that determine how notifications are delivered to subscribers. In the Novu dashboard, you can set default preferences for each workflow, specifying which channels, such as email, in-app, SMS, or push, should be used by default during notification delivery. These defaults ensure that subscribers receive messages through the most appropriate channels, even if they haven’t customized their own preferences. To learn more about channel preferences, refer to [preferences](/platform/concepts/preferences). ## Workflow step conditions Step conditions in Novu are used to control whether a step in your workflow should be executed, based on dynamic logic. This lets you create more intelligent and personalized workflows that respond to dynamic user and application data. Conditions can be based on subscriber fields, payload data, or the results of previous steps. For example, you might skip sending an SMS if the user is already online, or send a follow-up email only if an in-app message hasn’t been read within 24 hours. To learn more about all supported condition types, operators, and usage example, refer to the [Step Conditions documentation](/platform/workflow/step-conditions). ## Frequently asked questions These are some of the most frequently asked questions about workflows in Novu. ### Do I have to create a workflow to send notifications? Yes, creating a workflow is mandatory to send notifications. ### Can workflows be created and managed via code? Yes, workflows can be managed via code, if you are using Node.js, you can use the [Novu Framework](/framework/introduction) ### Can all workflows be synced at once to another environment? Currently, only a single workflow can be synced at a time. file: ./content/docs/platform/developer/api-keys.mdx # API keys Manage authentication credentials and connection endpoints for your Novu integration. Novu provides a set of keys and hostnames that your application uses to authenticate requests, send events to the Novu API, and to interact with the {``}. Your API keys are environment specific and allow Novu to tie all request to a particular Novu environment. ## Finding your API keys Follow these steps to find your environment-specific API keys: 1. Login to the Novu dashboard. 2. On the developer section, click **API Keys** to find all your environment keys, whiich are: * Application Identifier * Secret Keys * API URLs ![API Keys](/images/developer-tools/api-keys.png) ## Application identifier The Application Identifier is a unique identifier for your application within Novu. It is considered a public key and is safe to expose in client-side code. The application identifier is used to initialize the {``} component. ## Secret key The Secret Key is a private token used to authenticate and authorize requests sent to the API service. It proves that your application is allowed to access resources. This key grants full administrative access to your account. It should never be used in publicly accessible code, such as frontend applications or public repositories (GitHub, GitLab). Keep your secret key private. You can regenerate the secret key from the API Keys dashboard. This will invalidate the old key and require you to update your environment variables. ## API URLs The following hostnames are used to connect to the Novu cloud platform. | **Endpoint** | **URL** | **Description** | | ---------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | **API Hostname** | `https://api.novu.co` | The domain that your application sends API requests to. It identifies the Novu API server. | | **WebSocket Hostname** | `https://ws.novu.co` | The domain used to establish a WebSocket connection. Similar to the API Hostname, but specifically for real-time, two-way communication. | file: ./content/docs/platform/developer/environments.mdx # Environments Understanding and managing environments in Novu Novu uses environments to separate your workflows and its data. This lets you safely test changes, like a new workflow, in one environment before moving it to a live production environment. ## Types of environments When you create a Novu account, you are provided with a development and production environment by default. On certain plans, you can also create custom environments to match your team’s workflow. * **Development environment**: Use the development environment to build and test new workflows, layouts, translations, and experiment with different configurations before publishing to other environments. * **Production environment**: The production environment is used to send notifications to your subscribers. To ensure stability and prevent unintended changes, this environment is view-only for workflows, layouts, and translations. Changes are not made here directly, they are made from other environment and then published to this production environment. * **Custom environment**: Custom environments are only available on Team and Enterprise plans. You can use them to reflect your release process, for example, staging or QA. ## Create a custom environment To create a custom environment: 1. Log in to the Novu dashboard. 2. Go to the [Environments page](https://dashboard.novu.co/environments) in the Novu dashboard. 3. Click **Create environment**. A menu appears. 4. Enter your preferred environment name in the **Name** field. 5. Assign unique colors to easily distinguish between environments. ![Publish changes](/images/developer-tools/create-environment.png) 6. Click **Create environment**. The new environment is created and is available in the environments list. ## What's unique to each environment? Each environment in Novu has it's own isolated resources, while the development environment has assets that can be published to other environments. Each environment maintains some isolated set of resources, while you can share or promote other assets through publishing. ### Environment-specific resources These resources are completely separate and unique to each environment. Data in one environment has no connection to data in another. They are: * Activity feed * API keys (application identifier and secret key) * Integrations (Provider credentials) * Subscribers * Topics * Webhooks ### Publishable assets These assets are managed centrally in the development environment and then published to other environments. * Layouts * Translations * Workflows ### Environment credentials Each environment has two unique identifiers: * **Application Identifier** * Public ID for client-side apps * Used with {``} * Different for each environment * Safe to expose in frontend code * **API Secret Key** * Used for backend API authentication * Keep this secure and never expose publicly * Different for each environment Configure these credentials in your application based on the active environment, similar to how you manage other service credentials. ## Publish changes to other environments Novu provides a publish mechanism that allows you to promote changes from your development environment to production or other custom environments. This process ensures that all changes are deliberate, reviewed, and deployed in a controlled manner. The publishing process bundles all modifications made to workflows, layouts, and translations since the last publish event. You can promote changes to other environments by following these steps: 1. Ensure you are in the **Development** environment. All changes must originate from here. 2. Click **Publish changes**. A list of available environments appears. ![Publish changes](/images/developer-tools/publish-changes.png) 3. Select the environment that you want to publish to. ![Publish changes](/images/developer-tools/list-of-environment.png) 4. Select the checkboxes next to the workflows that you want to publish. A menu appears, showing all the available workflows. ![Publish changes](/images/developer-tools/publish-changes-modal.png) 5. Click the publish button to publish the selected workflows to the selected environment. file: ./content/docs/platform/developer/limits.mdx # Limits System limits, quotas, and constraints for the Novu platform. Novu enforces default limits on specific resources and configurations. These limits apply to all new accounts by default as per subscription plan. | Resource | Free | Pro | Team | Enterprise | | ----------------------- | ------- | ------- | ------- | ---------- | | Workflows | 20 | 20 | 100 | Custom | | Layouts | 1 | 100 | 100 | Custom | | Steps per workflow | 20 | 20 | 20 | Custom | | Context per trigger | 5 | 5 | 5 | Custom | | Environments | 2 | 2 | 10 | Custom | | Delay duration | 1 day | 7 days | 30 days | Custom | | Digest duration | 1 day | 7 days | 30 days | Custom | | Throttle duration | 1 hr | 24 hrs | 7 days | Custom | | Inbox feed retention | 30 days | 90 days | 90 days | Custom | | Activity feed retention | 1 day | 7 days | 90 days | Custom | | Team members | 3 | 3 | Custom | Custom | Reach out to us at [support@novu.co](mailto:support@novu.co) if you are looking for a custom limit. ## Frequently Asked Questions ### What happens if I exceed the default workflow or step limit? Exceeding the default limit may result in restricted functionality or errors. We recommend upgrading to a higher plan to avoid any restrictions. [Contact support](mailto:support@novu.co) to help with the upgrade. ### Is there a cost associated with custom limits? Custom limits may incur additional costs depending on the resources required. [Support](mailto:support@novu.co) will provide details during the approval process. file: ./content/docs/platform/inbox/headless-mode.mdx # Headless Mode Learn how to build custom Inbox UI for your application using Novu custom hooks Build a fully custom notification inbox with Novu's headless React hooks without being constrained by the default {``} UI or dependencies. The [**`@novu/react`**](/platform/sdks/react) package provides a set of hooks, such as `useNotifications` and `useCounts`. You handle the layout, styling, and interactions, while Novu provides the notification state, real-time updates, and actions. Not using React? You can access the same data manually using the [JavaScript SDK](/platform/sdks/javascript). ## Install the React SDK package Run the following command in your terminal: ```bash npm i @novu/react ``` ```bash pnpm add @novu/react ``` ```bash yarn add @novu/react ``` ```bash bun add @novu/react ``` ## Add the `NovuProvider` Wrap your application or the components that need access to notifications with the `NovuProvider`. This component initializes the Novu client and provides it to all child hooks via [context](/platform/workflow/contexts). ```jsx import { NovuProvider } from '@novu/react'; function App() { return ( {/* Your app components */} ); } ``` For more `NovuProvider` options, such as HMAC encryption, see the [Novu provider documentation](/platform/sdks/react/hooks/novu-provider). ## Fetch and display notifications Use the `useNotifications` hook to fetch and display a list of notifications. The hook manages loading states, pagination (`hasMore`, `fetchMore`), and real-time updates for you. ```jsx import { useNotifications } from "@novu/react"; function NotificationsList() { const { notifications, isLoading, error } = useNotifications(); if (isLoading) return
Loading...
; if (error) return
Error: {error.message}
; return (
{notifications?.map((notification) => (

{notification.subject}

{notification.body}

))}
); } ``` ## Show notification counts A common use case is showing a badge with the unread count on a bell icon. The `useCounts` hook is designed for this. It fetches unread, unseen, or total counts and updates them in real-time. ```jsx import { useCounts } from "@novu/react"; function BellButton() { const { counts } = useCounts({ filters: [ { read: false }, // Unread notifications ] }); const unreadCount = counts?.[0]?.count ?? 0; return ( ); } ``` ## Adding notification actions To perform actions on notifications such as marking as read, unread, or archiving, you can use the `useNovu` hook. This hook gives you direct access to the Novu client instance to call its API methods. The hooks `useNotifications` and `useCounts` automatically refetch and update your UI when these actions are performed. ```jsx import type { Notification as INotification } from "@novu/react"; import { useNovu } from "@novu/react"; function NotificationItem({ notification }: { notification: INotification }) { const novu = useNovu(); const markAsRead = async () => { try { await novu.notifications.readAll({ notificationId: notification.id }); } catch (error) { console.error("Failed to mark as read:", error); } }; const archive = async () => { try { await novu.notifications.archiveAll({ notificationId: notification.id }); } catch (error) { console.error("Failed to archive:", error); } }; return (

{notification.subject}

{notification.body}

); } ``` ## Showing product updates banner `@novu/react` hooks can be used to show a product updates banner in your application along with `` component notifications. Differences between product update banner and `` component notifications are: * `` component notifications are displayed in `Popover` when bell is clicked while product update banner is displayed as a standalone top banner in the application. * `` component notifications are unique to the user while product update banner is displayed to all users. To show a product update banner use following steps: 1. Create a new workflow in the dashboard with in-app steps 2. Edit in-app step content and make this workflow as critical so that this workflow is not displayed in preferences. 3. Create a **system subscriber** in the dashboard with subscriberId `product-updates-subscriber` for product updates banner. This system subscriber will be used to show the product updates banner to all users. 4. Create `ProductUpdateBanner` component and use `useNotifications` hook to get the product updates notifications and show them in the banner. ```jsx title="components/ProductUpdateBanner.tsx" import { NovuProvider, useNotifications } from "@novu/react"; import React from "react"; const BannerNotification = () => { const { notifications } = useNotifications({ // with limit:1 and archived:false we are getting the latest product update notification limit: 1, archived: false, }); if (notifications && notifications.length > 0) { // customize the banner as per your needs return
; } return null; }; export const ProductUpdateBanner = async () => { return ( ); }; ``` 5. Use `ProductUpdateBanner` component in your application. In below example we are using `ProductUpdateBanner` component with `` component to show the product updates banner along with `` notifications. ```jsx title="components/NovuNotifications.tsx" import { Inbox } from "@novu/react"; import { ProductUpdateBanner } from "./ProductUpdateBanner"; export const NovuNotifications = () => { return (
); } ``` 6. Use `NovuNotifications` component in your application. 7. Trigger the step 1 workflow to system subscriber `product-updates-subscriber`. 8. As per product release requirements, [Permanently delete](/api-reference/messages/delete-a-message) or [archive](/platform/sdks/javascript#archive) the product update notification to hide the banner. Using above steps, two instances of Novu notifications will be shown in your application: * Product updates banner with common subscriberId `product-updates-subscriber` * `` notifications with user specific subscriberId `YOUR_SUBSCRIBER_ID` ## Real-time updates The `useNotifications` and `useCounts` hooks automatically listen for real-time events and update your component state. If you need to listen for events manually, then you can use the `novu.on()` method from the `useNovu` hook. For example, you might want to show a toast notification when a new message arrives. ```jsx import { useNovu } from "@novu/react"; import { useEffect } from "react"; import type { Notification as INotification } from "@novu/react"; function NotificationListener() { const novu = useNovu(); useEffect(() => { // Handler for new notifications const handleNewNotification = ({ result }: { result: INotification }) => { console.log("New notification:", result.subject); // You can use a toast library here // toast.show(result.subject); }; // Handler for unread count changes const handleUnreadCountChanged = ({ result }: { result: number }) => { document.title = result > 0 ? `(${result}) My App` : "My App"; }; // Subscribe to events novu.on("notifications.notification_received", handleNewNotification); novu.on("notifications.unread_count_changed", handleUnreadCountChanged); // Cleanup function return () => { novu.off("notifications.notification_received", handleNewNotification); novu.off("notifications.unread_count_changed", handleUnreadCountChanged); }; }, [novu]); return null; // This component doesn't render anything } ``` ### Using websocket events The websocket event `notifications.notification_received` can be used to play a sound and show a toast message on the screen when a notification is received. ```jsx import { useNovu } from "@novu/react"; import { useEffect, useRef } from "react"; import type { Notification as INotification } from "@novu/react"; function NotificationSoundPlayer() { const novu = useNovu(); // Keep audio instance stable across renders const notificationSoundRef = useRef(null); useEffect(() => { // Initialize sound once notificationSoundRef.current = new Audio("/notification.mp3"); notificationSoundRef.current.preload = "auto"; // Handler for new notifications const handleNewNotification = ({ notification }: { notification: INotification }) => { console.log("New notification:", notification.subject); // Play notification sound notificationSoundRef.current ?.play() .catch((err) => { // This can fail if user hasn’t interacted with the page yet console.warn("Notification sound could not be played:", err); }); // Example: toast notification // toast({ // title: notification.subject, // description: notification.body, // }); }; // Subscribe to events novu.on("notifications.notification_received", handleNewNotification); // Cleanup return () => { novu.off("notifications.notification_received", handleNewNotification); }; }, [novu, notificationSoundRef]); return null; } export default NotificationListener; ``` file: ./content/docs/platform/inbox/migration-guide.mdx # Migrate to the New Inbox This guide outlines the key differences between the `@novu/notification-center` package and the new `@novu/react` package and how to migrate to the latest Inbox version. * Use [@novu/react](/platform/sdks/react) Inbox React component and [@novu/js](/platform/sdks/javascript) headless package with new dashboard workflows and [@novu/framework](/platform/workflow/overview)-based workflows. * With legacy dashboard based workflows, use [@novu/notification-center](https://v0.x-docs.novu.co/notification-center/client/react/get-started) package. The `@novu/react` package introduces a more flexible and customizable way to display notifications in your application. This guide outlines the key differences between the `@novu/notification-center` package and the new `@novu/react` package. Follow the steps below to migrate your application to the latest Inbox version. ## Why you should upgrade to `@novu/react` * **Customization**: The `@novu/react` package provides more customization options for the appearance and behavior of the notification components. * **Flexibility**: The new package offers more flexibility in handling notifications and integrating with third-party libraries. * **Performance**: It is optimized for performance and provides a smoother user experience. * **Bundle Size**: The new package has a smaller bundle size and improved tree-shaking capabilities. * **Compatibility with the `@novu/framework`**: The `@novu/react` package is designed to work seamlessly with the `@novu/framework` package for creating and managing notifications. ## Breaking Changes The `@novu/react` package introduces several breaking changes compared to the `@novu/notification-center` package. Here are the key differences: ### Components * The `PopoverNotificationCenter` component has been replaced with the `Inbox` component. * The `NotificationCenter` component has been replaced with the `Notifications` component. * The `NotificationBell` component has been replaced with the `Bell` component. ### Styling * The `styles` props is replaced by an enchanced and easy to use `appearance` prop to customize the appearance of the notification components. For more information on `appearance` customization visit [here](/platform/inbox/react/styling). ### Notification * Removal of `seen` , `lastSeenDate`, `content`, `templateIdentifier`, `payload`, `cta` properties from the Notification object. * We have introduced `archive` functionality to the Notification object. ```diff title="Notification object" type Notification = { - _id: string; + id: string; - content: string; + body: string; - cta: IMessageCTA; + redirect?: Redirect; + primaryAction?: Action; + secondaryAction?: Action; - channel: ChannelTypeEnum; + channelType: ChannelType; - payload: Record; + data?: Record; - subscriber: Subscriber; + to: Subscriber; - seen: boolean; - lastSeenDate: string; - templateIdentifier: string; + subject?: string; + isRead: boolean; + isSeen: boolean; + isArchived: boolean; + isSnoozed: boolean; + readAt?: string | null; + archivedAt?: string | null; + avatar?: string; + tags?: string[]; + avatar?: string; + workflow: Workflow; + deliveredAt: string | null; + firstSeenAt: string | null; updatedAt: string; createdAt: string; }; ``` ```ts title="Types" type Workflow = { critical: boolean; id: string; identifier: string; name: string; tags: string[]; }; type Subscriber = { id: string; firstName?: string; lastName?: string; avatar?: string; subscriberId: string; }; type Redirect = { url: string; target?: '_self' | '_blank' | '_parent' | '_top' | '_unfencedTop'; }; type Action = { label: string; isCompleted: boolean; redirect?: Redirect; }; ``` ## Getting started To begin, install the `@novu/react` package. ```bash npm install @novu/react ``` ```bash pnpm add @novu/react ``` ```bash yarn add @novu/react ``` ```bash bun add @novu/react ``` ## Basic usage ### Legacy implementation with `@novu/notification-center` ```tsx import { NovuProvider, PopoverNotificationCenter, NotificationBell, } from '@novu/notification-center'; function Novu() { return ( {({ unseenCount }) => } ); } export default Novu; ``` ### Current implementation with `@novu/react` ```tsx import { Inbox } from '@novu/react'; function Novu() { return ( ); } export default Novu; ``` ## Notification center without bell icon The @novu/react package introduces a flexible way to display notifications as a list without the default bell icon. Use the `Inbox` and `Notifications` components to achieve this functionality. ### Legacy implementation with `@novu/notification-center` ```tsx import { NovuProvider, NotificationCenter } from '@novu/notification-center'; function Novu() { return ( ); } export default Novu; ``` ### Current implementation with `@novu/react` ```tsx import { Inbox, Notifications } from '@novu/react'; function Novu() { return ( ); } export default Novu; ``` ## Custom bell icon Customize the bell icon that triggers the notifications popover using the `renderBell` prop. ### Legacy implementation with `@novu/notification-center` ```tsx import { NovuProvider, PopoverNotificationCenter } from '@novu/notification-center'; import CustomBell from './CustomBell'; // Your custom bell icon component function Novu() { return ( {({ unseenCount }) => ( )} ); } export default Novu; ``` ### Current implementation with `@novu/react` ```tsx import { Inbox } from '@novu/react'; import CustomBell from './CustomBell'; // Your custom bell icon component function Novu() { return ( } /> ); } export default Novu; ``` ## Notification actions Handle user interactions with notifications effectively using the action handlers provided by `@novu/react`. ### onNotificationClick Trigger a callback function when a user clicks on a notification item. #### Legacy implementation with `@novu/notification-center` ```tsx import { NovuProvider, PopoverNotificationCenter, NotificationBell, IMessage, } from '@novu/notification-center'; function Novu() { function handleOnNotificationClick(message: IMessage) { // your logic to handle the notification click if (message?.cta?.data?.url) { window.location.href = message.cta.data.url; } } return ( {({ unseenCount }) => } ); } export default Novu; ``` #### Current implementation with `@novu/react` ```tsx import { Inbox } from '@novu/react'; function Novu() { const handleNotificationClick = (notification) => { // Your custom logic here console.log('Notification clicked:', notification); }; return ( ); } export default Novu; ``` ### onPrimaryActionClick and onSecondaryActionClick Handle primary and secondary actions within a notification explicitly. #### Legacy implementation with `@novu/notification-center` ```tsx import { NovuProvider, PopoverNotificationCenter, IMessage, MessageActionStatusEnum, useUpdateAction, ButtonTypeEnum, NotificationBell, } from '@novu/notification-center'; function Novu() { const CustomNotificationCenter = () => { const { updateAction } = useUpdateAction(); const handleOnActionClick = async ( templateIdentifier: string, btnType: ButtonTypeEnum, notification: IMessage ) => { if (templateIdentifier === 'friend-request') { if (btnType === 'primary') { /** Call your API to accept the friend request here **/ /** And then update Novu that this action has been taken, so the user won't see the button again **/ updateAction({ messageId: notification._id, actionButtonType: btnType, status: MessageActionStatusEnum.DONE, }); } } }; return ( {({ unseenCount }) => } ); }; return ( ); } export default Novu; ``` #### Current implementation with `@novu/react` ```tsx import { Inbox } from '@novu/react'; function Novu() { const handlePrimaryActionClick = (notification) => { // Handle primary action console.log('Primary action clicked:', notification); }; const handleSecondaryActionClick = (notification) => { // Handle secondary action console.log('Secondary action clicked:', notification); }; return ( ); } export default Novu; ``` ## Avatar icons In the legacy implementation, you could set a notification’s avatar icon by enabling the Add an avatar option in the workflow UI. Novu would then use the avatar field of the actor subscriber as the icon. In the new implementation, you can set the avatar icon for a notification by adding the avatar icon in the workflow UI. There are three options to choose from: * Use a default avatar icon. * Use a hard-coded avatar icon URL. * Use a payload variable to dynamically set the avatar icon. For more information, refer to [Icons](/platform/inbox/configuration/icons#change-or-remove-a-in-app-notification-avatar). ## Popover positioning For advanced positioning and styling of the notifications popover, integrate third-party popover libraries such as Radix UI. ### Legacy implementation with `@novu/notification-center` ```tsx import { PopoverNotificationCenter, NotificationBell, NovuProvider, } from '@novu/notification-center'; function Novu() { return ( {({ unseenCount }) => } ); } export default Novu; ``` ### Current implementation with `@novu/react` and Radix UI as an example ```tsx import React from 'react'; import * as RadixPopover from '@radix-ui/react-popover'; import { Inbox, Bell, Notifications } from '@novu/react'; function Novu() { return ( ); } export default Novu; ``` ## Custom notification item Customize the appearance and structure of individual notification items using the `renderNotification` prop. ### Legacy implementation with `@novu/notification-center` ```tsx import { NovuProvider, PopoverNotificationCenter, NotificationBell, } from '@novu/notification-center'; function Novu() { return ( { return ( { e.preventDefault(); handleNotificationClick(); }}> {notification.content} ); }}> {({ unseenCount }) => { return ; }} ); } export default Novu; ``` ### Current implementation with `@novu/react` ```tsx import { Inbox } from '@novu/react'; function Novu() { const renderCustomNotificationItem = (notification) => (
notification.read()}> Avatar

{notification.subject}

{notification.body}

); return ( ); } export default Novu; ``` ## Styling with appearance prop Customize the overall look and feel of the notification components using the appearance prop, which supports both CSS objects and class names (including Tailwind CSS classes). ### Legacy implementation with `@novu/notification-center` ```tsx import { NovuProvider, PopoverNotificationCenter, NotificationBell, } from '@novu/notification-center'; function Novu() { return ( {({ unseenCount }) => } ); } export default Novu; ``` ### Current implementation with `@novu/react` ```tsx import { Inbox } from '@novu/react'; function Novu() { return ( ); } export default Novu; ``` For more information on `appearance` customization visit [here](/platform/inbox/react/styling). ## Multiple tabs support Organize notifications into different categories using tabs by leveraging the tags property in workflow definitions and the tabs prop in the Inbox component. ### Create multiple tabs #### Legacy implementation with `@novu/notification-center` After defining the feeds on the workflow UI, you were able to filter notifications based on the feedIdentifier. ```tsx import { NovuProvider, PopoverNotificationCenter, NotificationBell, } from '@novu/notification-center'; function Novu() { return ( {({ unseenCount }) => { return ; }} ); } export default Novu; ``` #### Current implementation with `@novu/react` 1. Define multiple workflows with relevant tags. Add tags on the workflow Tags field. One tag can be used for multiple workflows. 2. Use those tags in the `tabs` prop of the `Inbox` component. ```tsx import { Inbox } from '@novu/react'; const tabs = [ { label: 'All Notifications', value: [] }, { label: 'Security', value: ['security'] }, { label: 'Promotions', value: ['promotions'] }, ]; function InboxWithTabs() { return ( ); } export default InboxWithTabs; ``` ## Localization Customize the language and text content of the notification components using the localization prop. Refer to the [localization documentation](/platform/inbox/advanced-concepts/localization). ## HMAC encryption The process remains the same as before. For more information, refer to [Secure your inbox with HMAC encryption](/platform/inbox/prepare-for-production#secure-your-inbox-with-hmac-encryption). ## Handling notifications Handle notifications using the methods provided by the notification object. ### Marking notifications as read Mark notifications as read using the `read` method provided by the notification object. ```tsx import { Inbox } from '@novu/react'; function Novu() { const handleNotificationClick = (notification) => { notification.read(); }; return ( ); } export default Novu; ``` ### Marking notifications as unread Mark notifications as unread using the `unread` method provided by the notification object. ```tsx import { Inbox } from '@novu/react'; function Novu() { const handleNotificationClick = (notification) => { notification.unread(); }; return ( ); } export default Novu; ``` ### Marking notifications as archive Mark notifications as archive using the `archive` method provided by the notification object. ```tsx import { Inbox } from '@novu/react'; function Novu() { const handleNotificationClick = (notification) => { notification.archive(); }; return ( ); } export default Novu; ``` ### Marking notifications as unarchive Mark notifications as unarchive using the `unarchive` method provided by the notification object. ```tsx import { Inbox } from '@novu/react'; function Novu() { const handleNotificationClick = (notification) => { notification.unarchive(); }; return ( ); } export default Novu; ``` If you still have questions or need further assistance, please reach out to us at [support@novu.co](mailto:support@novu.co). file: ./content/docs/platform/inbox/overview.mdx # Introduction to Inbox Learn how to integrate Novu Inbox component, a pre-built notification center component for real-time in-app notifications in your application. import { Card, Cards } from 'fumadocs-ui/components/card'; import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; import { Bell, Code, Layout, Sliders } from 'lucide-react'; The Novu Inbox is a prebuilt, ready-to-use, and fully customizable UI component for delivering real-time in-app notifications. It gives your subscribers a centralized place to view and manage notifications. With just a few lines of code, you can embed a polished, real-time notification experience directly into your application. ![Introduction to inbox](/images/inbox/intro-to-inbox.png) ## Composable architecture The Novu Inbox is built with a composable architecture. It is composed of other sub-components: } href="/platform/inbox/advanced-customization/customize-bell#bell-component"> Used to display the bell icon and trigger the notification component when clicked } href="/platform/inbox/advanced-customization/customize-popover#notifications-component"> Displays the notifications list } href="/platform/inbox/advanced-customization/customize-popover#inboxcontent-component"> Displays the content of the `` menu } href="/platform/inbox/configuration/preferences#using-the-preferences--component"> Used to display the preferences modal ![Fully functional and customizable React Inbox component](/images/inbox/overview@2x.png) By composing these individual components, you can create multiple popular inbox layouts that fit perfectly within your application's design. To aid your design process, we provide a [free Figma file](https://www.figma.com/community/file/1425407348374863860) that contains all of the design assets. Make a copy of the file into your own account to get started with customizing your graphical Inbox elements. ## Key feature * Full stack integration: The Inbox handles UI, unread states, routing, and preferences all in one place. * Highly customizable: Override styles, replace every UI elements and icons. * Flexible layouts: Use the default Inbox UI layout or build your own. * Built-in support for Tabs and filters, localization, snoozing, preferences management, and more. ## How it works At a high level, the Inbox abstracts away the complexity of building a notification center. 1. When you drop the Inbox component into your application, it securely connects to Novu's services. 2. It automatically fetches user notifications and manages the real-time unread count displayed on the bell icon. 3. When a user clicks the bell, it presents the list of notifications and user preferences. 4. All user interactions, such as marking a notification as read or changing a preference, are automatically synchronized with the Novu backend in real-time. ## Ways to implement the Novu Inbox There are two integration approaches, depending on your needs for speed versus customization. ### The "Plug-and-Play" approach This is the fastest way to integrate the Novu Inbox. The Inbox component, encapsulate all the UI and logic. You simply drop the component into your application, configure it with the necessary properties, and you're done. ### "Build-Your-Own" Approach For maximum flexibility and complete control over the look and feel, use the [@novu/react SDK](/platform/sdks/react) for react hooks or [@novu/js SDK](/platform/sdks/javascript) for framework agnostic javascript methods. You get the power of Novu's notification engine while building a user interface that perfectly matches your application's design system. file: ./content/docs/platform/inbox/prepare-for-production.mdx # Prepare for Production Learn how to prepare your Inbox for production by enabling HMAC encryption for security and managing Novu's branding. Before deploying the Inbox UI to production, you should secure your integration and configure the correct environment. You can also remove Novu's branding from your notifications. This ensures that your end users receive notifications safely, without exposure to unnecessary risks, and in a way that aligns with your product branding. ## Set the correct environment Novu supports multiple environments, including development, production, and any custom environments you create. When preparing for deployment, choose the environment that will serve as your production environment and update your configuration accordingly: * Use the API keys for your selected production environment from the [API Keys](https://dashboard.novu.co/api-keys) page in your application. * Store keys in `.env` file or your server’s environment variables. * Confirm your `applicationIdentifier` and `subscriber` match the configuration for your chosen production environment. * Add these two props, if using the EU region: * `apiUrl` with value **[https://eu.api.novu.co](https://eu.api.novu.co)** * `socketUrl` with value **wss\://eu.socket.novu.co** ## Secure your Inbox with HMAC encryption When you add the Inbox to your application, you're required to pass: * `subscriberId`: Identifies the current subscriber. * `applicationIdentifier`: A public key to communicate with the notification feed API. Without additional security, a malicious actor could potentially guess another subscriber's `subscriberId` and use your public `applicationIdentifier` to view that user's notifications. You can prevent this by enabling HMAC (Hash-based Message Authentication Code) encryption. This process uses a *secret key* to create a secure signature (`subscriberHash`) for each `subscriberId`. Novu then verifies this hash to ensure that requests to view a notification feed are authentic and not from an impersonator. Follow these steps to enable HMAC encryption. ### 1. Enable HMAC in the dashboard Activate the HMAC security feature within your Novu in-app provider settings. 1. Go to [Novu Dashboard](https://dashboard.novu.co). 2. Navigate to the [Integrations Store](https://dashboard.novu.co/integrations) page. 3. Click on the **Novu In-App** for your chosen production environment 4. A side panel opens from the right side of the screen with the provider settings, enable `Security HMAC encryption` toggle in **Integration Credentials** section. ![Enabling HMAC in the Novu dashboard](/images/inbox/hmac.png) ### 2. Generate HMAC hash on the server side Next, use your secret key from the API Keys page on the Novu dashboard to generate an HMAC hash of the `subscriberId` on the server side. ```tsx import { createHmac } from 'crypto'; // The subscriberId of the logged-in user const subscriberId = 'REPLACE_WITH_SUBSCRIBER_ID'; // The secret key from your Novu API Keys page const novuSecretKey = process.env.NOVU_SECRET_KEY; // Generate the HMAC hash const hmacHash = createHmac('sha256', novuSecretKey) .update(subscriberId) .digest('hex'); ``` Keep `NOVU_SECRET_KEY` secure and never expose it to the client. ### 3. Use the HMAC hash in the Inbox component Send the `hmacHash` generated in the previous step to the client side application. You can include it in the initial data payload when a subscriber or user logs in or fetch it from a dedicated API endpoint. Pass the hash to the `subscriberHash` prop in your Inbox component. ```tsx import { Inbox } from '@novu/react'; // Example: The hmacHash is passed to the frontend // as part of the user object after they authenticate. const { user } = currentUser(); const hmacHash = user?.novuSubscriberHash; ; ``` If HMAC encryption is active in In-App provider settings and `subscriberHash` along with `subscriberId` is not provided, then Inbox will not load ## Remove Novu branding Users on a paid plan can remove the "Inbox by Novu" branding from the Inbox UI. To remove the branding: 1. Go to [Novu Dashboard](https://dashboard.novu.co). 2. Navigate to the **Settings** page. 3. Under the **Organization** tab, find the **Branding & Integrations** section. 4. Enable the **Remove Novu branding** toggle. ![Removing Novu branding](/images/inbox/novu-branding.png) file: ./content/docs/platform/inbox/setup-inbox.mdx # Set up the Inbox Learn how to integrate the Novu Inbox component into your application to display real-time notifications for your subscribers. import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; import Link from 'next/link'; The Inbox component displays a notification bell by default, which opens a menu containing the subscriber's notifications and preferences. ## Installation To get started, install the Inbox UI: ```bash npm install @novu/nextjs ``` ```bash pnpm add @novu/nextjs ``` ```bash yarn add @novu/nextjs ``` ```bash bun add @novu/nextjs ``` ```bash npm install @novu/react ``` ```bash pnpm add @novu/react ``` ```bash yarn add @novu/react ``` ```bash bun add @novu/react ``` ## Try Inbox in keyless mode Keyless mode lets you test the look and features of the Inbox component instantly in your application, no setup required. ![Inbox keyless](/images/inbox/keyless-inbox.png) ```tsx import { Inbox } from '@novu/nextjs'; export function Novu() { return ( ); } ``` ```tsx import { Inbox } from '@novu/react'; export function Novu() { return ( ); } ``` This is only for testing, the data is temporary (expires in 24h) and not tied to real subscribers. ## Use Inbox with real subscribers To display real-time notifications for your subscribers, connect the Inbox component to your Novu environment using your `applicationIdentifier` and a `subscriberId`. You can create or manage subscribers from the Novu Dashboard. ### US region (default) ```tsx import { Inbox } from '@novu/nextjs'; export function Novu() { return ( ); } ``` ```tsx import { Inbox } from '@novu/react'; export function Novu() { return ( ); } ```
Sign in to get your own API keys
### EU region If your Novu account is in the EU region, then include the `backendUrl` and `socketUrl` props to connect to EU-specific API endpoints: ```tsx import { Inbox } from '@novu/nextjs'; export function Novu() { return ( ); } ``` ```tsx import { Inbox } from '@novu/react'; export function Novu() { return ( ); } ```
Sign in to get your own API keys
file: ./content/docs/platform/integrations/demo-integration.mdx # Demo Integrations Learn about the default Novu Email and SMS provider. Demo integrations are built-in, sandboxed providers for the email and SMS channels. Their purpose is to allow you to test your notification workflows immediately, without needing to sign up for or connect a real third-party provider. When you create a Novu account, your environments come pre-configured with two demo email integrations and two demo SMS integrations. All demo integrations are active by default, and you can set any of them as the primary integration for their channel at any time from the **Integration Store**. Demo integrations are only available in Novu cloud. This feature doesn't work in community self-hosted version and local environment. ## Limits of the demo integrations Demo integrations are intended strictly for testing and development. To ensure this, they operate with the following rules: * Both the demo email and demo SMS integrations are limited to 300 notifications per month each. * The demo Email integration can only send emails to the address associated with your logged-in Novu account. It does not deliver messages to any other email address. ## How test using a demo integration Demo integrations are active by default, so you can start testing in just a few steps. The demo integrations are set as primary by default in new Novu environments. 1. Log in to the Novu dashboard. 2. Click **Workflows**. 3. Click **Create workflow** and [create a new workflow](/platform/workflow/build-a-workflow). 4. Add two steps in the workflow: one for Email and one for SMS. ![Add step](/images/channels-and-providers/add-step.png) 5. Click **Test Workflow**. ![Test workflow](/images/channels-and-providers/test-workflow.png) 6. Verify the notification. There are two ways to confirm that your test was successful: * Check the activity feed or event logs to confirm if the notification was successfully sent to the various channels. ![Event logs](/images/channels-and-providers/event-logs.png) * Check the inbox of the email address and phone number you use for your Novu account. You should receive a real email and SMS from the demo integration. ## Next step Once you have finished testing with the demo integrations, select a channel below to find the list of all supported providers integrations and their guides: Integrate Email providers. Integrate SMS providers. Integrate Push providers. Integrate Chat providers. Integrate In-app notifications. file: ./content/docs/platform/integrations/overview.mdx # Overview Learn about the providers that Novu supports for Email, Push, SMS and Chant channels, and how to integrate them to send notifications and receive events. Providers are the third-party services that deliver your notifications through the various channels. These are services such as SendGrid for email, Twilio for SMS, or Slack for chat. Novu provides a unified integration layer that connects your application to all these different providers. You connect your provider accounts to Novu from the Integration Store on the Novu dashboard, and Novu's API handles the rest. This approach means you can add, remove, or switch providers at any time without having to change your application's code. It also allows Novu to manage complex logic like provider fallbacks, for example, "If SendGrid fails, try sending with Amazon SES". ## Provider vs. Integration Understanding the difference between a provider and an integration is key to managing your channels. * **Provider**: A provider is the third-party service responsible for sending the actual notification such as, Twilio, SendGrid, or Slack. Every provider supported by Novu is identified by a providerId. Refer to this resource to see the full list of provider IDs used in Novu. * **Integration**: An integration is your specific, configured instance of that provider. It's the provider plus your unique credentials and settings. When creating an integration, you are required to assign it a name and identifier. While the name can be changged, the identifier cannot be changed after creation. This identifier is called the `integrationIdentifier`. ![integrationIdentifier](/images/channels-and-providers/integrationidentifier.png) This means that you can have provider but two separate integrations for that particular provider. Each of these is a unique integration that you can manage and trigger independently. Novu providers demo integrations. To learn more about how to use them for testing, see the [Demo Integration](/platform/integrations/demo-providers) guide. ### Primary vs. Active integrations Each environment can have multiple active integrations for a single channel. ![Primary and active integration](/images/channels-and-providers/primary-active-integrations.png) * **Active integration**: Any integration that is "on" and ready to send notifications. You can disable an active integration at any time to stop sending messages through it. * **Primary integration**: This is the default integration used when you trigger a notification. How the primary integration behaves depends on the channel: * **Email and SMS channels**: You can have many active integrations, but only one can be marked as the primary at a time. This primary integration is used by default for all email or SMS notifications unless you specifically override it. * **Push and chat channels**: These channels do not use a single primary integration. Instead, Novu uses all active integrations in parallel to deliver the message. If you disable an integration that is currently marked as "primary" for Email or SMS channels, then Novu automatically promotes another active integration to be the new primary. You can also manually set which active integration you want to be the primary one from the Integration Store. ## Managing integrations across environments Each integration is scoped to a specific [environment](/platform/concepts/environments). This means you must configure separate integrations for each environment, even if they point to the same provider. This separation ensures that test messages don’t accidentally go to production users, and different credentials or delivery settings can be safely isolated across environments. ## Override default settings Novu provides an `overrides` object that can be used to access features that Novu doesn't currently support but are supported by certain providers. This feature includes: * Setting custom SendGrid headers. * Using Slack blocks. * Defining platform-specific sounds for FCM push notifications. For a complete guide on Overrides, refer to the [Trigger Overrides](/platform/integrations/trigger-overrides) documentation. ## Integration guides Select a channel below to find the list of all supported providers integrations and their detailed setup guides: Configure email providers and settings Set up SMS messaging capabilities Enable push notification delivery Integrate with chat platforms Manage in-app notification center file: ./content/docs/platform/integrations/trigger-overrides.mdx # Trigger Overrides Learn how to customize the behavior of your workflows at trigger time import { Mail, MessageSquare } from 'lucide-react'; import { Card, Cards } from 'fumadocs-ui/components/card'; Trigger overrides let you to modify the default behavior of specific aspects of a workflow trigger (event), giving you fine-tuned control over how messages are delivered across different channels and providers. ## Channel overrides Channel overrides let you customize specific channel settings and content during workflow trigger. Each channel has its own supported override options, documented in their respective channel guides. Channel overrides is only available for Email and SMS channels. } href="/platform/integrations/email" /> } href="/platform/integrations/sms" /> ## Provider overrides Provider overrides give you fine-tuned control over how messages are delivered by allowing direct configuration of the underlying provider SDKs during the workflow's trigger phase. This feature is designed for advanced use cases where Novu's default message editor or UI does not expose specific provider capabilities. Use provider overrides to: * Access native provider features not surfaced by Novu’s abstraction layer. For example, custom headers in SendGrid, topic messaging in FCM, or Slack blocks. * Adapt to provider-specific options by injecting parameters that Novu doesn’t yet officially support. * Configure shared or unique settings across steps without altering templates or workflow logic. This mechanism offers a flexible customization layer that lets you pass deeply nested payloads that align directly with your provider’s native API. It helps decouple workflow behavior from provider-specific implementation details, which is essential when working across multiple channels like email, push, or chat. Because overrides interact directly with provider SDKs, they won’t work if they're misconfigured. Make sure you understand the supported options for each provider before using this feature. ### Provider override scopes Overrides are defined in the `overrides` property of a trigger payload. You can specify configuration values at two levels: * Workflow-level: Applies to all steps using a specific provider and takes precedence over the default workflow provider settings. * Step-level: Targets a specific step in the workflow and it takes precedence over both workflow-level overrides and the default workflow provider settings. ### Workflow-level provider overrides Workflow-level provider overrides apply configuration to all steps that use a given provider in the workflow. They’re useful for applying shared logic across multiple steps, without repeating the same settings in each one. Use workflow-level overrides when: * You need to define common metadata like headers, personalization, or layout settings. * You want consistent behavior across all steps for a given provider. ```typescript import { Novu } from "@novu/api"; const novu = new Novu(""); async function run() { const result = await novu.trigger({ to: { subscriberId: "subscriber_unique_identifier", firstName: "Albert", lastName: "Einstein", email: "albert@einstein.com", phone: "+1234567890", }, workflowId: "workflow_identifier", payload: { comment_id: "string", post: { text: "string", }, }, overrides: { providers: { sendgrid: { template_id: "xxxxxxxx", // Make sure this is a string trackingSettings: { clickTracking: { enable: true, enableText: false, }, }, }, }, }, }); } run(); ``` This configuration affects every step in the workflow that uses SendGrid, unless a step-level override provides a more specific value. #### Sending extra fields supported by provider SDK You can also send extra fields supported by the provider SDK. For example, if you want to send a headers to the provider SDK, then you could use the `_passthrough` field. ```json "overrides": { "providers" : { "sendgrid": { "_passthrough": { "headers": { "Authorization": "Bearer my-api-key" } } }, }, }, ``` ### Step-level overrides Step-level overrides let you apply provider-specific settings directly to an individual step in your workflow. Use step-level overrides when: * You want to send push notifications through the same provider, but with different settings for each step. For example, two steps use FCM, but each sends a different sound or title. * You need to customize the payload for a specific push step, such as platform-specific settings for Android and iOS, without affecting other steps. ```typescript import { Novu } from "@novu/api"; const novu = new Novu({ secretKey: "" }); async function run() { const result = await novu.trigger({ to: { subscriberId: "subscriber_unique_identifier", firstName: "Albert", lastName: "Einstein", email: "albert@einstein.com", phone: "+1234567890", }, workflowId: "workflow_identifier", payload: { comment_id: "string", post: { text: "string", }, }, overrides: { steps: { 'push-step': { providers: { fcm: { notification: { title: 'New Comment', body: 'Someone replied to your post!', sound: 'default', // Play default system sound }, android: { notification: { sound: 'sound_bell' // matches res/raw/sound_bell.mp3 } }, apns: { payload: { aps: { sound: 'notification_bell.caf' // For iOS } } } } } } } } }); } run(); ``` The `push-step` refers to the step identifier, which you can copy directly from your workflow in the Novu dashboard. Use this identifier to target the specific step you want to override. In this example, only the `push-step` is affected, and multiple FCM-specific settings are overridden for that step, which are the notification title, body, and sound configurations for both Android and iOS platforms. file: ./content/docs/platform/novu-for/developers.mdx # Developers Novu is a powerful notifications platform designed for developers and engineers to build, manage, and deliver scalable, extensible, and captivating multi-channel notification experiences while empowering product teams with intuitive tools and prebuilt components. ## Who is Novu for? How you use and get started with Novu depends on your role. While it’s initially implemented by engineering and development teams, Novu unifies everyone in an organization that authors, creates, sends, manages, and measures results from notifications being sent to end users. Novu empowers engineers to deliver notification platforms for product teams. ### Novu for developers and engineers Novu empowers developers and engineering teams to quickly deliver a fully extensible notifications platform for product teams to create captivating notification experiences. We provide the following proven stack for developers to simply integrate notifications into their products: * **[Code-first Notification Framework](/framework/typescript/overview)** Opinionated, yet flexible, Framework for building and managing notification workflows. * **[JSON Schema Based](/api-reference/overview)** Controls to craft a no-code visual editor to enable non-technical team members to modify content and behaviour. * **[Prebuilt, customisable UI components](/platform/inbox/overview)** for in-app user notification feeds and preference experiences. * **[Integration with multiple delivery providers](/platform/integrations/overview)**, allowing you to continue using your preferred vendors with Novu. * **[Scalable, reliable Novu Cloud SaaS infrastructure](https://dashboard.novu.co)** developed from scratch to meet the demands of high-volume notification delivery and storage (think hundreds of millions of notifications). * **Observability** for delving into the lifecycle of a notification's success or failure. Eliminate guesswork of how, when, and why a user receives a notification. * **Comprehensive documentation**, implementation guides, recipes and illustrative examples. * **Compliance and security** for safely managing your data. * **Open source** provides transparency you can trust, cultivates community contributions for fast improvement, and enables you to deploy and self-host a Novu instance into any environment of your choosing. Notification content can be written in a variety of common content tooling, including [React](/framework/content/react-email), [Vue-email](/framework/content/vue-email), MJML, and more. Content can also be customized and hydrated using any datasource. file: ./content/docs/platform/novu-for/product.mdx # Product Change notification messaging, verbiage, and cadence without requiring engineering's help by using the Novu Dashboard UI. Novu Framework was designed to bridge the gap between developers and non-developers in the team. * **Developers** - Define and abstract away complex logic, data manipulation, hydration, html, and etc... * **Non-Developers** - Use the Novu Dashboard UI to control the content and behavior of the notifications using [controls](/framework/controls). ## How to modify controls? Controls are modified directly in the [Novu Dashboard UI](https://dashboard.novu.co), under the 'Step controls' tab of the **Workflow Editor** page. Upon saving the changes, the new Control values will be used in the notification sent to your subscribers. ## Dynamic payload data Dynamic data passed as part of the trigger payload can be easily used inside of the Controls. For example, you can pass a `user_name` as part of the payload and use it in the controls as `{{payload.user_name}}` to personalize the notification. ## Common usecases ### Change notification content A control can be as broad as `content` or as specific as `2fa_code_title_color`. This allows you to change the content of the notification without needing to touch the code. ### Create new email designs Novu's built-in email editor enables rich content creation without ever touching the code. ### Change digest or delay frequency Controls can be used for controlling any aspect of the step configuration, for example: * Delay amount * Digest length * Digest type (daily, weekly, monthly) * etc... Read more about [digest strategies and how to prevent notification fatigue.](https://novu.co/blog/digest-notifications-best-practices-example/) ### Control structure and layout Controls can be used to show/hide or rearrange the layout of the notification. For example, you can have an input to show/hide a button, or change the position of a specific email section. file: ./content/docs/platform/quickstart/angular.mdx # Angular Create an account and learn how to start using Novu Inbox Notification in your angular application. import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { Code2, Library, Workflow } from 'lucide-react'; import { CodeTemplateBlock } from '@/components/quickstart/inbox-code'; This guide walks you through integrating Novu’s Inbox into your Angular application for real time in-app notifications, from setup to triggering your first notification. By the end, you'll have a working notification inbox. This guide uses @novu/js javascript sdk to build the Inbox component in Angular. Novu currently does not support native Angular Inbox component. ### Create a Novu account Create a Novu account or sign in to access the Novu dashboard. ### Create an Angular application Run the following command to create a new Angular app using angular cli: ```bash ng new novu-inbox-angular cd novu-inbox-angular ``` ### Install `@novu/js` The [Novu JavaScript SDK](/platform/sdks/javascript) gives you access to the Inbox component. Run the following command to install the SDK: ```bash npm install @novu/js ``` ```bash pnpm add @novu/js ``` ```bash yarn add @novu/js ``` ```bash bun add @novu/js ``` ### Add the Inbox component Update the `src/app/app.ts` file to add the Inbox component. You'll need to provide your :tooltip\[applicationIdentifier]{label="The application identifier is a unique identifier for your application. You can find it in the Novu Dashboard under the API keys page."} and :tooltip\[subscriberId]{label="The subscriber ID is the unique identifier for the user in your application, typically the user's id in your database."}: If you’re signed in to your Novu account, then the :tooltip\[applicationIdentifier]{label="The application identifier is a unique identifier for your application. You can find it in the Novu Dashboard under the API keys page."} and :tooltip\[subscriberId]{label="The subscriber ID is the unique identifier for the user in your application, typically the user's id in your database."} are automatically entered in the code sample above. Otherwise, you can manually retrieve them: * `applicationIdentifier` - In the Novu dashboard, click API Keys, and then locate your unique Application Identifier. * `subscriberId` - This represents a user in your system (typically the user's ID in your database). For quick start purposes, an auto-generated subscriberId is provided for your Dashboard user. **Note:** If you pass a `subscriberId` that does not exist yet, Novu will automatically create a new subscriber with that ID. ### Add the Inbox component to your application Add a `#novuInbox` reference to your application in the starting of the `src/app/app.html` file: ```html title="src/app/app.html"
```
### Run Your Application Start your development server: ```bash npm run start ``` ```bash pnpm run start ``` ```bash yarn run start ``` ```bash bun run start ``` Once the application is running, a bell icon will appear on the top left side of the screen. Clicking it opens the notification inbox UI. Currently, there are no notifications. Let’s trigger one! ### Trigger your first notification In this step, you'll create a simple workflow to send your first notification via the Inbox component. Follow these steps to set up and trigger a workflow from your Novu dashboard. 1. Go to your [Novu dashboard](https://dashboard-v2.novu.co/auth/sign-in). 2. In the sidebar, click **Workflows**. 3. Click **Create Workflow**. Enter a name for your workflow (e.g., "Welcome Notification"). 4. Click **Create Workflow** to save it. 5. Click the **Add Step** icon in the workflow editor and then select **In-App** as the step type. 6. In the In-App template editor, enter the following: * **Subject**: "Welcome to Novu" * **Body**: "Hello, world! " 7. Once you’ve added the subject and body, close the editor. 8. Click **Trigger**. 9. Click **Test Workflow**. ### View the notification in your app Go back to your Angular app, then click the bell icon. You should see the notification you just sent from Novu! 🎉
## Next steps } href="/platform/sdks/javascript"> Explore JavaScript SDK API reference for more advanced use cases. } href="/platform/workflow/overview"> Design and manage advanced notification workflows. } href="/platform/concepts/tenants"> Manage multiple tenants within an organization. file: ./content/docs/platform/quickstart/nextjs.mdx # Next.js Learn how to integrate the Novu Inbox component into your Next.js application using the App Router. import { Accordion, Accordions } from '@/components/accordion'; import { CodeBlock } from '@/components/codeblock'; import { BuildWorkflowStep, CreateAccountStep, CreateSubscriberStep, TriggerNotificationStep, } from '@/components/quickstart/common-steps'; import { InboxCodeBlock } from '@/components/quickstart/inbox-code'; import { Button } from '@/components/ui/button'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { SignedIn, SignedOut, SignInButton } from '@clerk/nextjs'; import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; import { Code2, Library, Palette, Workflow } from 'lucide-react'; Learn how to integrate Novu’s Inbox for real-time in-app notifications in your Next.js application. By the end of this guide, you’ll have a working notification inbox that displays messages triggered from the Novu dashboard. ### Create a Novu account Create a Novu account or sign in to access the Novu dashboard. ### Create a new Next.js application Run the following command to create a new Next.js application: ```bash npm create next-app@latest ``` ```bash pnpm create next-app ``` ```bash yarn create next-app ``` ```bash bunx create-next-app ``` ### Install `@novu/nextjs` Run the following command to install the Next.js Novu SDK: ```bash npm install @novu/nextjs ``` ```bash pnpm add @novu/nextjs ``` ```bash yarn add @novu/nextjs ``` ```bash bun add @novu/nextjs ``` ### Add the notification Inbox to your app Import Novu’s built-in {``} component into your layout file and place it in the navbar: If you are signed in to your Novu account, then the :tooltip\[applicationIdentifier]{label="The application identifier is a unique identifier for your application."} and :tooltip\[subscriberId]{label="The subscriber ID is the unique identifier for the user in your application, typically the user's ID in your database."} will be automatically populated. Otherwise, retrieve them from: * `applicationIdentifier`: On the Novu dashboard, click **API Keys**, and copy your unique Application Identifier. * `subscriberId`: This represents a user in your system, usually the user ID from your database. For testing, you can use the auto-generated subscriberId from your Novu dashboard. You can locate it under the Subscribers tab on the Novu dashboard. **Note:** If you pass a `subscriberId` that does not exist yet, Novu will automatically create a new subscriber with that ID. ### Run your application Start your development server: ```bash npm run dev ``` ```bash pnpm run dev ``` ```bash yarn dev ``` ```bash bun run dev ``` Once your application is running, you would see a **bell icon** in the navbar. Click on it, to open the notification Inbox UI. There are no notifications yet, so let’s trigger one! ### Trigger your first notification In this step, you'll create a simple workflow to send your first notification via the Inbox component. Follow these steps to set up and trigger a workflow from your Novu dashboard. 1. Go to your [Novu dashboard](https://dashboard.novu.co/auth/sign-in). 2. Click **Workflows**. 3. Click **Create Workflow** and then enter a name (e.g., "Welcome Notification"). 4. Click **Create Workflow** to save it. 5. Click the **Add Step** icon in the workflow editor and then select **In-App** as the step type. 6. In the **In-App template editor**, enter: * **Subject**: "Welcome to Novu" * **Body**: "Hello, world!" 7. Once you’ve added the subject and body, close the editor. 8. Click **Trigger**. 9. Click **Test Workflow**. ### View the notification in your app Go back to your Next.js app, then click the bell icon. You should see the notification you just sent from Novu! 🎉 ## Next steps } href="/inbox/react/styling"> Customize the look and feel of your Inbox to match your application's design. } href="/platform/inbox/overview"> Explore our full-stack UI components libraries for building in-app notifications. } href="/platform/workflow/overview"> Design and manage advanced notification workflows. } href="/platform/concepts/tenants"> Manage multiple tenants within an organization. file: ./content/docs/platform/quickstart/react.mdx # React Learn how to integrate the Novu Inbox component into a React application and add routing with React Router. import { Accordion, Accordions } from '@/components/accordion'; import { BuildWorkflowStep, CreateAccountStep, CreateSubscriberStep, TriggerNotificationStep, } from '@/components/quickstart/common-steps'; import { Step, Steps } from '@/components/steps'; import { Button } from '@/components/ui/button'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; import { Code2, Library, Palette, Workflow } from 'lucide-react'; import { CodeTemplateBlock } from '@/components/quickstart/inbox-code'; This guide walks you through integrating Novu’s Inbox into your React application for in-app notifications in real-time, from setup to triggering your first notification. By the end, you'll have a working notification inbox. ### Create a Novu account Create a Novu account or sign in to access the Novu dashboard. ### Create a React app using Vite Run the following command to create a new React app using [Vite](https://vite.dev/guide/#scaffolding-your-first-vite-project): ```bash npm create vite@latest novu-inbox-react -- --template react-ts cd novu-inbox-react npm install npm run dev ``` ```bash pnpm create vite novu-inbox-react --template react-ts cd novu-inbox-react pnpm install pnpm run dev ``` ```bash yarn create vite novu-inbox-react --template react-ts cd novu-inbox-react yarn install yarn dev ``` ```bash bunx create-vite novu-inbox-react --template react-ts cd novu-inbox-react bun install bun run dev ``` ### Install `@novu/react` The [Novu React SDK](/platform/sdks/react) gives you access to the Inbox component. Run the following command to install the SDK: ```bash npm install @novu/react ``` ```bash pnpm add @novu/react ``` ```bash yarn add @novu/react ``` ```bash bun add @novu/react ``` ### Create the Inbox component In the `src` directory, create a `components/novu-inbox.tsx` file and use the {``} component, passing :tooltip\[applicationIdentifier]{label="The application identifier is a unique identifier for your application. You can find it in the Novu Dashboard under the API keys page."} and :tooltip\[subscriberId]{label="The subscriber ID is the unique identifier for the user in your application, typically the user's id in your database."}: If you’re signed in to your Novu account, then the :tooltip\[applicationIdentifier]{label="The application identifier is a unique identifier for your application. You can find it in the Novu Dashboard under the API keys page."} and :tooltip\[subscriberId]{label="The subscriber ID is the unique identifier for the user in your application, typically the user's id in your database."} are automatically entered in the code sample above. Otherwise, you can manually retrieve them: * `applicationIdentifier` – In the Novu dashboard, click API Keys, and then locate your unique Application Identifier. * `subscriberId` – This represents a user in your system (typically the user's ID in your database). For quick start purposes, an auto-generated subscriberId is provided for your Dashboard user. **Note:** If you pass a `subscriberId` that does not exist yet, Novu will automatically create a new subscriber with that ID. ### Set up React Router and import the Inbox component Now you can set up React Router and add the `NovuInbox` component to your app layout: ```tsx title="src/App.tsx" import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { NovuInbox } from './components/novu-inbox'; function Layout({ children }: { children: React.ReactNode }) { return (
{children}
); } function Home() { return
Welcome to the Home page!
; } function App() { return ( } /> {/* Add your routes here */} ); } export default App; ```
### Run Your Application Start your development server: ```bash npm run dev ``` ```bash pnpm run dev ``` ```bash yarn dev ``` ```bash bun run dev ``` Once the application is running, a bell icon will appear in the navbar. Clicking it opens the notification inbox UI. Currently, there are no notifications. Let’s trigger one! ### Trigger your first notification In this step, you'll create a simple workflow to send your first notification via the Inbox component. Follow these steps to set up and trigger a workflow from your Novu dashboard. 1. Go to your [Novu dashboard](https://dashboard.novu.co/auth/sign-in). 2. In the sidebar, click **Workflows**. 3. Click **Create Workflow**. Enter a name for your workflow (e.g., "Welcome Notification"). 4. Click **Create Workflow** to save it. 5. Click the **Add Step** icon in the workflow editor and then select **In-App** as the step type. 6. In the In-App template editor, enter the following: * **Subject**: "Welcome to Novu" * **Body**: "Hello, world! " 7. Once you’ve added the subject and body, close the editor. 8. Click **Trigger**. 9. Click **Test Workflow**. ### View the notification in your app Go back to your React app, then click the bell icon. You should see the notification you just sent from Novu! 🎉
## Next steps } href="/inbox/react/styling"> Customize the look and feel of your Inbox to match your application's design. } href="/platform/inbox/overview"> Explore our full-stack UI components libraries for building in-app notifications. } href="/platform/workflow/overview"> Design and manage advanced notification workflows. } href="/platform/concepts/tenants"> Manage multiple tenants within an organization. file: ./content/docs/platform/quickstart/remix.mdx # Remix Create an account and learn how to start using Novu notification Inbox in your Remix application. import { Accordion, Accordions } from '@/components/accordion'; import { BuildWorkflowStep, CreateAccountStep, CreateSubscriberStep, TriggerNotificationStep, } from '@/components/quickstart/common-steps'; import { Step, Steps } from '@/components/steps'; import { Button } from '@/components/ui/button'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; import { Code2, Library, Palette, Workflow } from 'lucide-react'; import { CodeTemplateBlock } from '@/components/quickstart/inbox-code'; This guide walks you through integrating Novu’s Inbox into your Remix application for in-app notifications in real-time, from setup to triggering your first notification. By the end, you'll have a working notification inbox. ### Create a Novu account Create a Novu account or sign in to access the Novu dashboard. ### Create a Remix application Run the following command to create a new Remix app: ```bash npx create-remix@latest ``` ```bash pnpm dlx create-remix@latest ``` ```bash yarn dlx create-remix@latest ``` ```bash bun x create-remix@latest ``` ### Install `@novu/react` The [Novu React SDK](/platform/sdks/react) gives you access to the Inbox component. Run the following command to install the SDK: ```bash npm install @novu/react ``` ```bash pnpm add @novu/react ``` ```bash yarn add @novu/react ``` ```bash bun add @novu/react ``` ### Create an Inbox component In the `app` directory, create a `components/notification-center.tsx` file and use the {``} component, passing :tooltip\[applicationIdentifier]{label="The application identifier is a unique identifier for your application. You can find it in the Novu Dashboard under the API keys page."} and :tooltip\[subscriberId]{label="The subscriber ID is the unique identifier for the user in your application, typically the user's id in your database."}: If you’re signed in to your Novu account, then the :tooltip\[applicationIdentifier]{label="The application identifier is a unique identifier for your application. You can find it in the Novu Dashboard under the API keys page."} and :tooltip\[subscriberId]{label="The subscriber ID is the unique identifier for the user in your application, typically the user's id in your database."} are automatically entered in the code sample above. Otherwise, you can manually retrieve them: * `applicationIdentifier` – In the Novu dashboard, click API Keys, and then locate your unique Application Identifier. * `subscriberId` – This represents a user in your system (typically the user's ID in your database). For quick start purposes, an auto-generated subscriberId is provided for your Dashboard user. **Note:** If you pass a `subscriberId` that does not exist yet, Novu will automatically create a new subscriber with that ID. ### Add the Notification Center component to your layout Now you can import the `NotificationCenter` component and add it to your app layout: ```tsx title="app/root.tsx" import { NotificationCenter } from "~/components/notification-center"; import type { MetaFunction } from "@remix-run/node"; import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, } from "@remix-run/react"; export const meta: MetaFunction = () => { return [ { title: "New Remix App" }, { name: "description", content: "Welcome to Remix!" }, ]; }; export default function App() { return ( ); } ``` ### Run Your Application Start your development server: ```bash npm run dev ``` ```bash pnpm run dev ``` ```bash yarn dev ``` ```bash bun run dev ``` Once the application is running, a bell icon will appear in the navbar. Clicking it opens the notification inbox UI. Currently, there are no notifications. Let’s trigger one! ### Trigger your first notification In this step, you'll create a simple workflow to send your first notification via the Inbox component. Follow these steps to set up and trigger a workflow from your Novu dashboard. 1. Go to your [Novu dashboard](https://dashboard.novu.co/auth/sign-in). 2. In the sidebar, click **Workflows**. 3. Click **Create Workflow**. Enter a name for your workflow (e.g., "Welcome Notification"). 4. Click **Create Workflow** to save it. 5. Click the **Add Step** icon in the workflow editor and then select **In-App** as the step type. 6. In the In-App template editor, enter the following: * **Subject**: "Welcome to Novu" * **Body**: "Hello, world! " 7. Once you’ve added the subject and body, close the editor. 8. Click **Trigger**. 9. Click **Test Workflow**. ### View the notification in your app Go back to your React app, then click the bell icon. You should see the notification you just sent from Novu! 🎉 ## Next steps } href="/inbox/react/styling"> Customize the look and feel of your Inbox to match your application's design. } href="/platform/inbox/overview"> Explore our full-stack UI components libraries for building in-app notifications. } href="/platform/workflow/overview"> Design and manage advanced notification workflows. } href="/platform/concepts/tenants"> Manage multiple tenants within an organization. file: ./content/docs/platform/quickstart/vanilla-js.mdx # Vanilla JS Learn how to integrate the Novu Inbox component into a Vanilla JS and HTML project. import { Accordion, Accordions } from '@/components/accordion'; import { BuildWorkflowStep, CreateAccountStep, CreateSubscriberStep, TriggerNotificationStep, } from '@/components/quickstart/common-steps'; import { Step, Steps } from '@/components/steps'; import { Button } from '@/components/ui/button'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; import { Code2, Library, Palette, Workflow } from 'lucide-react'; import { CodeTemplateBlock } from '@/components/quickstart/inbox-code'; This guide walks you through integrating Novu’s Inbox into your Vanilla JS and HTML project for in-app notifications in real-time, from setup to triggering your first notification. By the end, you'll have a working notification inbox. ### Add Novu ESM script Add the following code to your HTML file: ```html title="index.html" Novu Inbox - Vanilla JS

Novu Inbox - Vanilla JS Demo

```
### Change subscriberId and applicationIdentifier In above code, replace `NOVU_APPLICATION_IDENTIFIER` with actual :tooltip\[applicationIdentifier]{label="The application identifier is a unique identifier for your application. You can find it in the Novu Dashboard under the API keys page."} value and `NOVU_SUBSCRIBER_ID` with actual :tooltip\[subscriberId]{label="The subscriber ID is the unique identifier for the user in your application, typically the user's id in your database."} value. * `applicationIdentifier` – In the Novu dashboard, click API Keys, and then locate your unique Application Identifier. * `subscriberId` – This represents a user in your system (typically the user's ID in your database). For quick start purposes, an auto-generated subscriberId is provided for your Dashboard user. **Note:** If you pass a `subscriberId` that does not exist yet, Novu will automatically create a new subscriber with that ID. ### Run Your Application Open `index.html` in your browser. You should see the Novu Inbox component with a bell icon. ### Trigger your first notification In this step, you'll create a simple workflow to send your first notification via the Inbox component. Follow these steps to set up and trigger a workflow from your Novu dashboard. 1. Go to your [Novu dashboard](https://dashboard.novu.co/auth/sign-in). 2. In the sidebar, click **Workflows**. 3. Click **Create Workflow**. Enter a name for your workflow (e.g., "Welcome Notification"). 4. Click **Create Workflow** to save it. 5. Click the **Add Step** icon in the workflow editor and then select **In-App** as the step type. 6. In the In-App template editor, enter the following: * **Subject**: "Welcome to Novu" * **Body**: "Hello, world! " 7. Once you’ve added the subject and body, close the editor. 8. Click **Trigger**. 9. Click **Test Workflow**. ### View the notification in your app Open `index.html` in your browser, then click the bell icon. You should see the notification you just sent from Novu! 🎉
## Next steps } href="/inbox/react/styling"> Customize the look and feel of your Inbox to match your application's design. } href="/platform/inbox/overview"> Explore our full-stack UI components libraries for building in-app notifications. } href="/platform/workflow/overview"> Design and manage advanced notification workflows. } href="/platform/concepts/tenants"> Manage multiple tenants within an organization. file: ./content/docs/platform/quickstart/vue.mdx # Vue Create an account and learn how to start using Novu Inbox in your vue application. import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { Code2, Library, Workflow } from 'lucide-react'; import { CodeTemplateBlock } from '@/components/quickstart/inbox-code'; This guide walks you through integrating Novu's Inbox into your Vue application for real time in-app notifications, from setup to triggering your first notification. By the end, you'll have a working notification inbox. This guide uses @novu/js javascript sdk to build the Inbox component in Vue. Novu currently does not support native Vue Inbox component. ### Create a Novu account Create a Novu account or sign in to access the Novu dashboard. ### Create a Vue application Run the following command to create a new Vue app: ```bash npm create vue@latest novu-inbox-vue ``` ```bash pnpm create vue novu-inbox-vue ``` ```bash yarn create vue novu-inbox-vue ``` ```bash bunx create-vue novu-inbox-vue ``` ### Install `@novu/js` The [Novu JavaScript SDK](/platform/sdks/javascript) gives you access to the Inbox component. Run the following command to install the SDK: ```bash npm install @novu/js ``` ```bash pnpm add @novu/js ``` ```bash yarn add @novu/js ``` ```bash bun add @novu/js ``` ### Add the Inbox component Create the `src/components/NovuInbox.vue` file to add the Inbox component passing :tooltip\[applicationIdentifier]{label="The application identifier is a unique identifier for your application. You can find it in the Novu Dashboard under the API keys page."} and :tooltip\[subscriberId]{label="The subscriber ID is the unique identifier for the user in your application, typically the user's id in your database."}: If you're signed in to your Novu account, then the :tooltip\[applicationIdentifier]{label="The application identifier is a unique identifier for your application. You can find it in the Novu Dashboard under the API keys page."} and :tooltip\[subscriberId]{label="The subscriber ID is the unique identifier for the user in your application, typically the user's id in your database."} are automatically entered in the code sample above. Otherwise, you can manually retrieve them: * `applicationIdentifier` - In the Novu dashboard, click API Keys, and then locate your unique Application Identifier. * `subscriberId` - This represents a user in your system (typically the user's ID in your database). For quick start purposes, an auto-generated subscriberId is provided for your Dashboard user. **Note:** If you pass a `subscriberId` that does not exist yet, Novu will automatically create a new subscriber with that ID. ### Add the Inbox component to your application Import and use the `NovuInbox` component in `src/App.vue` file: ```vue title="src/App.vue" ``` ### Run Your Application Start your development server: ```bash npm run start ``` ```bash pnpm run start ``` ```bash yarn run start ``` ```bash bun run start ``` Once the application is running, a bell icon will appear on the top left side of the screen. Clicking it opens the notification inbox UI. Currently, there are no notifications. Let's trigger one! ### Trigger your first notification In this step, you'll create a simple workflow to send your first notification via the Inbox component. Follow these steps to set up and trigger a workflow from your Novu dashboard. 1. Go to your [Novu dashboard](https://dashboard-v2.novu.co/auth/sign-in). 2. In the sidebar, click **Workflows**. 3. Click **Create Workflow**. Enter a name for your workflow (e.g., "Welcome Notification"). 4. Click **Create Workflow** to save it. 5. Click the **Add Step** icon in the workflow editor and then select **In-App** as the step type. 6. In the In-App template editor, enter the following: * **Subject**: "Welcome to Novu" * **Body**: "Hello, world! " 7. Once you've added the subject and body, close the editor. 8. Click **Trigger**. 9. Click **Test Workflow**. ### View the notification in your app Go back to your Vue app, then click the bell icon. You should see the notification you just sent from Novu! 🎉 ## Next steps } href="/platform/sdks/javascript"> Explore JavaScript SDK API reference for more advanced use cases. } href="/platform/workflow/overview"> Design and manage advanced notification workflows. } href="/platform/concepts/tenants"> Manage multiple tenants within an organization. file: ./content/docs/platform/sdks/overview.mdx # Overview Explore Novu's comprehensive collection of server-side and client-side SDKs for seamless notification integration across multiple programming languages and frameworks. import { Card, Cards } from 'fumadocs-ui/components/card'; /* server side icons */ import { DotnetIcon } from '@/components/icons/dotnet'; import { GolangIcon } from '@/components/icons/golang'; import { JavaIcon } from '@/components/icons/java'; import { LaravelIcon } from '@/components/icons/laravel'; import { PhpIcon } from '@/components/icons/php'; import { PythonIcon } from '@/components/icons/python'; import { RubyIcon } from '@/components/icons/ruby'; import { KotlinIcon } from '@/components/icons/kotlin'; /* client side icons */ import { ReactIcon } from '@/components/icons/react'; import { TypescriptIcon } from '@/components/icons/typescript'; import { JavascriptIcon } from '@/components/icons/javascript'; ## Server-side SDKs ### API SDKs Novu's server-side SDKs simplify the integration with Novu's REST API. #### Offical SDKs maintained by Novu: } color="#ea5a0c" href="/platform/sdks/server/typescript"> Connect your Node app to Novu via the Node.js SDK. } color="#dc2626" href="/platform/sdks/server/python"> Connect your Python app to Novu via the Python SDK. } color="#0285c7" href="/platform/sdks/server/go"> Connect your Golang app to Novu via the Go SDK. } color="#16a34a" href="/platform/sdks/server/php"> Connect your PHP app to Novu via the PHP SDK. } color="#dc2626" href="/platform/sdks/server/dotnet"> Connect your C#/.NET app to Novu via the .NET SDK. #### SDKs maintained by the community: } color="#dc2626" href="/platform/sdks/server/laravel"> Connect your Laravel app to Novu via the Laravel SDK. } color="#dc2626" href="/platform/sdks/server/kotlin"> Connect your Kotlin app to Novu via the Kotlin SDK. } color="#dc2626" href="/platform/sdks/server/java"> Connect your Java app to Novu via the Java SDK. } color="#dc2626" href="/platform/sdks/server/ruby"> Connect your Ruby app to Novu via the Ruby SDK. ### Framework SDK The Framework SDK is a TypeScript library that allows you to build notification workflows and execute them in your own runtime environment. While triggering notifications is supported in all SDKs, creating and managing notification workflows is only supported in the Framework Typescript SDK. } href="/framework/typescript/overview"> Build and execute notification workflows in TypeScript ## Web and Mobile SDKs Novu provides the following web client SDKs to enable integrations with Novu's prebuilt UI components, allowing you to easily add notification functionality to your applications without handling complex notification logic manually. } href="/platform/inbox/react/get-started"> Official React SDK for Novu's notification center } href="/platform/inbox/headless/get-started"> Framework-agnostic SDK for custom implementations } href="/platform/sdks/react-native"> Official React Native SDK for mobile applications file: ./content/docs/platform/subscription/customize-and-configure.mdx # Customize and configure Learn how to filter visible preferences, customize styling, and use custom render functions. The Subscription UI works within the existing Novu environment and uses the same provider and styling architecture as the {``} component. You can configure which options are visible to your users, apply your brand's theme, or take full control of the rendering. ## Filter using preferences The `preferences` prop on the {``} component controls which workflows the component displays to the user. ### Default preferences If you want to simply filter which workflows are visible without changing their labels, then you can pass an array of workflow identifier strings. The component uses the workflow names defined in the Dashboard as the labels. ```tsx import { NovuProvider, Subscription } from '@novu/react'; export function Novu() { return ( ) } ``` ### Custom preferences You can filter using tags, workflow IDs, or both. You can also customize the labels, descriptions, or default states by passing an array of objects. ```tsx import { NovuProvider, Subscription, } from '@novu/react'; export function Novu() { return ( ); } ``` You can also update the labels for the workflows with the `dynamic` prop of the localization. For full localization options, see the [Inbox localization documentation](/platform/inbox/advanced-concepts/localization). ## Styling The {``} component shares the same styling architecture as the Inbox component. You can use the `appearance` prop to customize elements, variables, icons and themes. For full styling and theming options, see the [Inbox styling documentation](/platform/inbox/configuration/styling). ### Dark mode Novu provides `subscriptionDarkTheme` in the `@novu/react/themes` package to apply the built-in dark theme to the {``} component. You can apply this conditionally based on your app's state. ```tsx import { NovuProvider, Subscription } from '@novu/react'; import { subscriptionDarkTheme } from '@novu/react/themes'; export function Novu() { const isDarkMode = true; return ( ); } ``` ### Localization You can customize the text labels used in the component to support different languages or to match your app's voice. This works identically to Inbox localization. ```tsx import { NovuProvider, Subscription } from '@novu/react'; export function Novu() { return ( ); } ``` For full localization options, see the [Inbox localization documentation](/platform/inbox/advanced-concepts/localization). ## Custom rendering The `renderPreferences` prop on the {``} component lets you override how the component displays the list of workflows while still relying on the component to handle data fetching and state management. The function receives the current `subscription` object and a `loading` boolean. You can map over the `preferences` array and render your own HTML elements. ```tsx import { NovuProvider, Subscription, TopicSubscription } from '@novu/react'; export function Novu() { return ( (
{subscription?.preferences.map((preference, idx) => (
{preference.description} preference.update({ value: e.target.checked }) } />
))}
)} />
) } ``` file: ./content/docs/platform/subscription/headless-hooks.mdx # Headless hooks Build a fully custom Subscription interface using hooks. Novu `@novu/react` package provides Subscription hooks that let you build your own custom subscription experiences from scratch. These hooks allow you to list, create, update, and delete subscriptions while leveraging Novu's state management. Hooks are available in [`@novu/nextjs`](/platform/sdks/react), [`@novu/react-native`](/platform/sdks/react-native), [`@novu/react`](/platform/sdks/react). For a complete reference on all available Subscription endpoints, refer to the [API reference documentation](/api-reference/overview). ## Get all subscriptions The `useSubscriptions` hook retrieves all subscriptions associated with a specific topic for the current subscriber. This is useful for rendering a list of "My Subscriptions," where subscribers can view all the content they've opted into. The `subscriptions` array contains `TopicSubscription` objects. You can refer to the [TopicSubscription interface](/sdks/javascript/reference/topic-subscription) in the `@novu/js` package for the full type definition. ```tsx import { useSubscriptions } from '@novu/react'; function SubscriptionList() { const { subscriptions, isLoading } = useSubscriptions({ topicKey: 'product-updates' }); if (isLoading) return
Loading...
; return (
    {subscriptions.map((subscription) => (
  • {subscription.name}

    + {/* Render your custom subscription UI here */}
  • ))}
); } ``` ## Get a single subscription If you need to manage a specific subscription instance, then use the `useSubscription` hook. This hook fetches the details and preference states for a single subscription. ```tsx import { useSubscription } from '@novu/react'; function ProjectSettings() { const { subscription, isLoading } = useSubscription({ topicKey: 'project-123', identifier: 'user-project-alert' }); if (isLoading) return
Loading...
; if (!subscription) return
You are not subscribed to this project.
; return (

{subscription.name}

{/* Render preferences or other subscription details */}
); } ``` ## Create a subscription To allow users to opt-in to a topic, use the `useCreateSubscription` hook. This is often used on "Follow" or "Subscribe" buttons. ```tsx import { useCreateSubscription } from '@novu/react'; function SubscribeButton() { const { create, isCreating } = useCreateSubscription(); const handleSubscribe = async () => { const { data, error } = await create({ topicKey: 'project-123', identifier: 'user-project-alert', // Optional: Set initial preferences preferences: [] }); if (error) { console.error('Failed to subscribe', error); return; } console.log('Subscribed successfully!', data); }; return ( ); } ``` ## Update a subscription Use `useUpdateSubscription` to modify an existing subscription. This is primarily used to toggle workflow preferences, enabling, or disabling specific notifications within the subscription. ```tsx import { useUpdateSubscription } from '@novu/react'; function PreferenceToggle({ subscriptionId, workflowId, }) { const { update, isUpdating } = useUpdateSubscription(); const savePreferences = async () => { await update({ topicKey: 'project-123', subscriptionId: subscriptionId, preferences, }); }; return ( ); } ``` ### Update a single preference If you need to update a single preference, use the dedicated helpers available on the subscription object, such as: * `subscription.updatePreference(...)`, or * mapping over `subscription.preferences` and calling `preference.update(...)` ```tsx import { useSubscription } from '@novu/react'; function PreferenceList() { const { subscription } = useSubscription({ topicKey: 'project-123', identifier: 'user-project-alert' }); if (!subscription) return null; return (
{subscription.preferences.map((preference) => (
preference.update({ value: e.target.checked })} />
))}
); } ``` ## Delete a subscription To allow users to opt out or unsubscribe, use the `useRemoveSubscription` hook. ```tsx import { useRemoveSubscription } from '@novu/react'; function UnsubscribeButton({ subscriptionId }) { const { remove, isRemoving } = useRemoveSubscription(); const handleUnsubscribe = async () => { await remove({ subscriptionId: subscriptionId, topicKey: 'project-123' }); }; return ( ); } ``` file: ./content/docs/platform/subscription/overview.mdx # Introduction Learn what a Subscription is in Novu, how they fit into the notification system. Subscriptions introduce topic-level settings that allow subscribers to decide exactly which notifications they want to receive or mute. While standard preferences allow subscribers to toggle specific channels on or off, Subscriptions provide more precise control through conditional rules. With Subscriptions, subscribers can mute topics, subscribe or unsubscribe to them, and also define conditions that determine when Novu should deliver a notification. ## How subscription fits into the notification flow The Subscription introduces a filtering layer between the trigger and the subscriber. When you trigger a notification to a topic key, the system doesn't automatically send it to everyone. Instead, it follows this evaluation flow: 1. **Trigger**: You trigger a workflow to a `topicKey` and include a data payload. 2. **Match**: Novu retrieves all subscribers associated with that topic. 3. **Evaluate**: The system checks the conditions and preferences of each subscription against the payload. 4. **Deliver**: Only subscribers whose conditions match the payload receive the notification. This filtering happens for every event on a topic, which allows you to support many use cases, including advanced preferences, segmentation, and multi-tenant experiences. Workflow and global settings control delivery and take precedence. If a subscriber has disabled email notifications globally, then they won't receive email notifications for a subscription, even if the subscription condition is met. ## Core concepts To implement Subscriptions effectively, you should understand these components: * **Topic**: A topic groups notifications of a similar type. Refer to the [Topics documentation](/platform/concepts/topics) for details on how to create and manage them. * **Subscribers**: A subscriber represents the user or entity that receives notifications. Subscribers can join or leave topics and define preferences that control delivery. Refer to the [Subscribers documentation](/platform/concepts/subscribers) for details on how to create and manage subscribers. ### Subscription rules Each subscription can include one or more conditions. Conditions determine whether an event should trigger a notification for that subscriber. You can define available conditions in your UI, and subscribers choose which ones apply to them. ### Multiple subscriptions per topic Novu supports a 1-to-N relationship for subscriptions. This means, one subscriber can have multiple subscriptions to the same topic with different conditions. Novu evaluates each subscription independently. For example, in a trading app, a subscriber can have three separate alerts set up for the same `price-alerts` topic: * Notify me when BTC is above $80k. * Notify me when BTC is below $40k. * Notify me when ETH is above $4k. Novu stores these configurations per subscriber, per topic, and per workflow. ## Subscription components Novu provides a set of pre-built React components that you can use together or independently to build subscription experiences. ![Subscription components](/images/subscription-preferences/subscription.png) ### `` The {``} component serves as the root provider and the default UI. It requires the `topicKey` prop and optionally an `identifier` prop, and you must wrap it with the Novu provider as the source of session context. ```tsx import { NovuProvider, Subscription } from '@novu/react'; export function Novu() { return ( ) } ``` ### `` The `` component renders only the subscribe or unsubscribe action. This is useful when you want to place a subscription control inline. Use this component within the {``} component. ```tsx import { NovuProvider, Subscription, SubscriptionButton } from '@novu/react'; export function Novu() { return ( ) } ``` ### `` The `` component renders the list of preferences associated with a subscription. Preferences typically map to workflows or workflow groups within a topic. Use this component when you want to control where and how preferences appear in your UI. Use it within a {``} context. ```tsx import {NovuProvider, Subscription, SubscriptionPreferences } from '@novu/react'; export function Novu() { return ( ) } ``` ## How Subscriptions relate to the Inbox If you use the {``}, then subscriptions determine which notifications appear in a subscriber's feed. When a subscriber joins a topic, the {``} shows notifications from that topic according to the subscriber's selected conditions. If the subscriber mutes the topic or sets rules that don't match an event, then the Inbox doesn't show those messages to that subscriber. Subscriptions work with or without the {``}, and have a standalone {``} component. file: ./content/docs/platform/subscription/quickstart.mdx # Setup the Learn how to integrate and render the Subscription component in your React app to manage user preferences. import { Step, Steps } from '@/components/steps'; import { Code, Blocks } from 'lucide-react'; Subscription is avialable from v3.12.0 for the `@novu/nextjs`, `@novu/react` and `@novu/js` packages. This quickstart shows how to render the subscription UI in your app. The subscription UI lets users subscribe to topics, define conditions, and manage how they receive notifications. To learn how to render custom UI for the Subscription preferences, refer to the [Headless hooks documentation](/platform/subscription/Headless-hooks). The {``} component is designed to work within an existing Novu environment and uses the same provider and session context as the Inbox component. ### Create a topic Subscriptions are managed at the topic level. You can create a topic manually in the Novu Dashboard or via the API. However, you don't have to create it beforehand. If the topic key you provide to the component does not exist, Novu will automatically create it for you. Topics can represent any category of notifications. To learn more, see the [Topics documentation](/platform/concepts/topics). ### Install `@novu/nextjs` Ensure you have the latest version of the `@novu/nextjs` package installed in your project. ```bash npm install @novu/nextjs ``` ### Initialize the Novu client Wrap your app or the specific section where the subscription component would live with the ``. Configure the provider with your `applicationIdentifier` and the current user's `subscriberId`. ```tsx import { NovuProvider } from '@novu/nextjs'; const NOVU_CONFIG = { subscriber="SUBSCRIBER_ID" applicationIdentifier="APPLICATION_IDENTIFIER" }; function App() { return ( {/* Application content */} ); } ``` ### Add the component Place the {``} component inside the provider. You must pass the `topicKey`. The `identifier` prop is optional, you only need to provide it if you intend to manage multiple distinct subscriptions for the same topic. ```tsx import { NovuProvider, Subscription } from '@novu/nextjs'; const NOVU_CONFIG = { subscriber="SUBSCRIBER_ID" applicationIdentifier="APPLICATION_IDENTIFIER" }; function App() { return ( ); } ``` ## Next steps } href="/platform/subscription/customize-and-configure"> Customize and configure the Subscription component appearance and behavior. } href="/platform/subscription/overview"> Learn how Subscriptions work and explore the available components. file: ./content/docs/platform/workflow/build-a-workflow.mdx # Build a Workflow Learn how to build a Novu Workflow This is where you can find and manage your existing workflows. This will open the initial workflow creation screen. Here you define: * Name of the workflow * This is also referred to as the workflow `Identifier` * Tags (optional) * Description of the workflow (optional) * Enable [translations](/platform/workflow/translations) (optional) Once you've filled in the required fields, click **Create Workflow**. This is where you can start adding steps to your workflow. The first step will always be the **"Workflow Trigger"**. You can add the following steps by clicking on **+** which is the add step icon : **Channels** * Email * In-app * Push * Web push * Mobile push * Chat * Enterprise messaging platforms (e.g. Slack, Microsoft Teams, etc.) * Consumer messaging tools (e.g. WhatsApp, Telegram, Discord, etc.) * SMS If a channel step you've added is not configured in your Novu account, you'll see an Error. **Actions** * Digest * Delay * Custom (Coming soon, currently only available in the [Novu Framework](/framework/custom)) Each step in the workflow requires a template that defines the notification or message content and payload. The editor enables you to preview the rendered output of your content. **Content creation and templates** * Each channel step has its own template configuration options, tailored to the limitations and requirements of the specific channel. * The editor provides a live preview to show how the final message will appear. * You can use system variables for personalization, including: * Subscriber variables: firstName, lastName, email, phone, avatar. * Actor variables: for details about the event initiator. * Step variables: for data specific to the workflow execution. * Brand variables: for aligning with your organization's visual identity. * Tenant variables: for organization-specific data. **Dynamic content** * Inject dynamic data into your templates using payload variables. * These variables can be utilized in message content, subjects, and sender names for enhanced personalization. **Important:** When using dynamic content with payload variables, ensure that the required payload is passed when triggering the workflow. Once you've finished configuring your template, don't forget to click the `Save step` button to apply your changes. There are three main ways to trigger a workflow: * **Via the Novu Dashboard**\ This method is ideal for conducting quick tests directly from the Dashboard. It's a simple and convenient way to verify basic functionality. * **Using the trigger code snippet**\ Copy the code snippet and execute it in your local environment or an online sandbox. This approach allows for more thorough testing, enabling you to integrate the trigger with your application logic and live data for a realistic evaluation. * **Integrating the trigger in your application**\ Once all tests are complete, you can implement the trigger method directly in your application. This allows you to test the workflow in a real-world scenario, ensuring it functions seamlessly with your app's actual environment and users. Novu operates in a multi environment setup, with the currently available environments: * **Development**: Acts as a staging and test environment, where your non-technical peers can view and modify controls. * **Production**: For triggering workflows to your customers. After you've tested your workflow in the **Development** environment, you can promote it to **Production**. [Learn more about promoting workflows to production](/framework/deployment/production) ## Manage payload schema The Payload Schema in Novu allows you to define, manage, and validate the structure of incoming data for each workflow. By creating a schema, you ensure that dynamic payloads sent to workflows are predictable, type-safe, and consistent across environments. Novu’s payload schema is based on the [JSON Schema](https://json-schema.org/) standard. With a defined schema in place, you can: * Prevent unexpected runtime errors caused by invalid or missing data. * Build reliable conditional logic using type-aware operators. * Generate accurate previews powered by intelligent mock data. * Enable autocomplete suggestions when referencing payload variables. The schema acts as a contract between your systems and Novu, ensuring that every payload sent into your workflow conforms to expected rules. It provides your team with clear visibility into which variables exist, how they’re structured, and what validations are applied. Payload schemas are especially useful when building complex workflows that rely on dynamic content, reusable blocks, or strict validation requirements. ### Define workflow schema You can define the expected payload schema in three ways: manually, by importing a JSON sample, or by creating inline variables. When adding or editing a schema property, you can mark it as required. If a property is marked as required, then it must be included in the payload when triggering the workflow using `novu.trigger()`. #### Add Schema properties manually You can manually define each property in your payload by specifying: * Property name (It must be a string) * Property type (string, integer, number, boolean, enum, array, object, null) Each property you define becomes part of the payload schema, and helps Novu suggest accurate variables when configuring channels steps or digest actions. #### Import from JSON If you already have a sample payload, then you can quickly define the schema by importing it as a JSON object. Novu infers property names, types, and structures for you. #### Create an inline variable When referencing a variable that doesn’t exist in the schema (for example, `payload.title`) while editing a workflow step, follow these steps: * Novu will prompt you to create the variable inline. * Click Insert to add the variable to your schema with a default type of string. * (Optional) After insertion, edit the variable in the manage schema tab. You can: * Changing its type. * Marking it as required. * Adding validation rules. ### Enforce schema validation Schema validation is enabled on a per-workflow basis. When active, Novu validates incoming payloads against the schema when the workflow is triggered. This means: * Missing required properties will cause the request to fail. * Data types must match exactly. For example, a string cannot be passed where a number is expected. * Invalid values are rejected before the workflow executes. This validation is applied at the HTTP trigger level, and prevents invalid data from entering the workflow entirely. ### Schema configuration options When defining a schema property, the available configuration fields vary depending on the selected type. #### General fields (for all types) * Property name * Property type * Required property checkbox * Default value – Optional fallback if no value is provided * Min length and max length #### Type-specific configuration Depending on the selected type, additional configuration options appear: * **String**: * **Format**: None, date-time, date, time, duration, email, hostname, ipv4, ipv6, uuid, uri-reference, uri-template, json-pointer, relative-json-pointer, regex * **Pattern**: Regex-based validation * **Enum**: Add choices, which are a list of predefined, allowed values. This restricts the field to only those values. * **Array**: Select the Array item type, which defines the data type of each array element. * **Object**: Add nested properties, each with their own type, required status, and validation options. ## Notification severity Notification severity lets you classify worfklows based on their importance level. Each workflow can be assigned one of four severity levels: * **High**: Indicates a critical notification. Applies a red hue to the notification and the bell icon. * **Medium**: For important, but not critical, notifications. Applies an orange hue. * **Low**: For general or informational messages. By default, it has no color, but it can be styled. * **None**: The default level for all new and existing workflows. ### Setting a severity level You can set the severity for any workflow directly from the Novu dashboard. 1. From the **Workflows** page, open the workflow you want to configure, or create a new one. 2. In the workflow editor, go to the **Configure workflow** section and then select **Configure channel preferences**. ![](/images/workflows/notification-workflows/configure-notification-severity.png) 3. Choose the desired severity level option from the **Notification severity** list. ![Notification Severity](/images/workflows/notification-workflows/notification-severity.png) This classification directly impacts how notifications are displayed in the Novu Inbox, helping your users quickly identify and prioritize messages. Severity affects visual cues like color-coding, icons, and the appearance of the notification bell. For more information, refer to the [Inbox component](/platform/inbox/overview). file: ./content/docs/platform/workflow/channel-steps.mdx # Channel Steps undefined import { Card, Cards } from 'fumadocs-ui/components/card'; import { Bell, Mail, MessageSquare, MessagesSquare, Smartphone } from 'lucide-react'; A **channel step** within a workflow is the core building block for creating and delivering notifications to subscribers (End users). Each channel step is linked to a specific notification template and represents a notification to be sent through a single channel type (e.g., email, push, SMS, in-app, etc.). ### Channel Step Execution When a channel step is executed, Novu performs the following actions: 1. **Evaluates Step Conditions:** Novu checks any conditions defined for the step to determine if it should be executed. This allows for dynamic notification workflows. 2. **Verifies Subscriber's Information:**\ Novu ensures the subscriber has the required information to receive notifications via this channel. For example: * For email, the recipient must have a valid email address. * For push notifications, the required channel data (device token) must be configured. 3. **Checks Subscriber's Preferences:**\ Novu respects the recipient's notification preferences, verifying whether they've opted out of receiving notifications from this channel or workflow. ### Rendering and Delivering Notifications If the step is valid and all conditions are met, Novu renders the associated template for the step and queues the message for delivery. The message is then sent to the selected provider using the credentials configured for the channel. ### Available Channels } href="/platform/integrations/email" /> } href="/platform/inbox/overview" /> } href="/platform/integrations/push" /> } href="/platform/integrations/sms" /> } href="/platform/integrations/chat" /> file: ./content/docs/platform/workflow/delay.mdx # Delay Learn how to use delay step to add a delay in the workflow execution. The delay step allows you to pause the execution of your workflow for a specified duration. This is useful for implementing features like follow-up notifications, reminder sequences, or cooldown periods. ## Common use cases * Waiting for X amount of time before sending the message * Wait for a short period of time before sending a push message in case the user seen the notification in the Inbox Component * Send a reminder email 24 hours after a user abandons their shopping cart to encourage checkout completion * Send a follow-up message 3 days after user registration to check if they need onboarding assistance ## Adding a delay step Delay steps can be inserted at any stage of your workflow execution, they can happen after or before any action. The workflow execution will be halted for the given amount of time and then resumed to the next step in the flow. Notes: * Step conditions can be applied to delay step to skip the delay if certain conditions are met. * Delay step can be extended to subscriber's schedule. This option can be enabled in the delay step configuration. Channel notification after delay step will be sent on the next available slot in the subscriber's schedule. * Channel notification after delay step will be sent as per subscriber time zone if timezone attribute ofsubscriber is set. If timezone attribute is not set, then channel notification will be sent as per UTC timezone. Changing the step content after triggering the workflow with delay step will affect the existing pending delayed notification content. ## Delay types * Fixed delay * Scheduled delay * Dynamic delay ### Fixed delay Fixed delay is a delay that is fixed and does not change. For example, if you want to delay the workflow for 1 day, you can use the fixed delay. It can be specified in seconds, minutes, hours, days, weeks, or months. ![Fixed delay example](/images/platform/workflow/delay/fixed-delay.gif) ### Scheduled delay Scheduled delay is a delay that halts the workflow execution until the specified time is reached as per subscriber's time zone. Scheduled delay can be configured with these configurations: * util minute * until hour at specific minutes * until day at specific hour and minutes * until week and days of the week at specific hours and minutes * until month on specific days of the month and weekdays at hours and minutes Example: