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/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 | Business | Enterprise | Endpoints | | ------------- | ------ | ------- | -------- | ---------- | -------------------------------------------------------------- | | Events | 60 RPS | 240 RPS | 600 RPS | 6K RPS | Trigger | | Configuration | 20 RPS | 80 RPS | 200 RPS | 2k RPS | Subscribers, Topics, Tenants | | 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/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://roadmap.novu.co/changelog"> 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:node** - Runs the `@novu/node` package in watch mode * **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_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/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. * **[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 # Overview 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 # Introduction 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 { 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: ```tsx 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 in-app 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. ### 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/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. Additional information can be passed according the body interface below. 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/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/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**. 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/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 push tokens. **providerId** is required field. This API appends the **deviceTokens** to 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. */} Update credentials for a provider such as **slack** and **FCM**. **providerId** is required field. This API replaces the existing deviceTokens with the provided 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 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/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.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/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/node ``` ### Initialize the SDK Configure the SDK with your self-hosted backend URL: ```typescript import { Novu } from '@novu/node'; 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({ name : '', to: { 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/node'; const config = { backendUrl: '', }; const novu = new Novu('', config); await novu.trigger('', { to: { 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, 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. ### 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 (Not available) Layout management is not available yet in v2, and we are planning on introducing more reusable components in the future iterations. ### 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/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/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. ## Add Zod to your project ```bash npm install zod ``` ```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/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](/platform/concepts/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/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 Business or enterprise tier subscriptions 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/limits.mdx # Limits System limits for Novu import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; This document outlines the default limits for workflows and steps within workflows for new accounts. These limits are designed to ensure optimal performance and resource allocation. Users requiring custom limits can contact support for further assistance. *** ## Workflow Limits **Maximum Number of Workflows**: 100 All accounts are set to a maximum of 100 workflows. This limit ensures efficient resource management and system performance. ## Step Limits **Maximum Number of Steps per Workflow**: Each new workflow is limited to 20 steps. This safeguard ensures workflows remain manageable and performant. ## Custom Limits If you need to increase the limits for workflows or steps, you can contact [support](mailto:support@novu.co) to discuss your needs. Custom limits may be granted based on usage requirements and system capacity. *** ## FAQs Exceeding the default limit may result in restricted functionality or errors. Contact support to request a custom limit. Yes, custom step limits can be applied to individual workflows upon request. Custom limits may incur additional costs depending on the resources required. Support will provide details during the approval process. file: ./content/docs/platform/additional-resources/security.mdx # Security and Compliance Common questions about security, compliance, privacy policy, and terms and conditions import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; We regularly work with big companies and are happy to help and support you with guidance, and various compliances including reports to ease your security and legal team. If you have concerns about PII, you can use our OS version, Novu Hybrid-Cloud enterprise plan, or reach out to us at [sales@novu.co](mailto:sales@novu.co), [support@novu.co](mailto:support@novu.co), or Discord. Yes, as part of our GDPR compliance we have our cloud version available on both EU (Frankfurt), as well as US (Virginia). Yes, you can see the complete compliance report on our [security page](https://trust.novu.co/). Novu also decided to take the extra step and provide separate data residency in both the EU and the US. Not exactly, to keep data residency intact we cannot simply copy or move data between data warehouses across US and EU. However if you have the need please contact us at [sales@novu.co](mailto:sales@novu.co) Yes, Novu Cloud is SOC 2 Type II compliant, we have made sure to do penetration tests, security training, evidence collection, and SDL. You can see live control updated on our [security page](https://trust.novu.co/), and ask for our security report as well at [sales@novu.co](mailto:sales@novu.co). Yes, Novu Cloud is ISO27001 compliant, we have made sure to go through both Stage 1 and Stage 2 audits, and fully define ISMS requirements. From entirely creating our organization processes, defining organization risk assessment policies, and building organization Incident Response & Disaster Recovery plans. Based on the selected solution there are a couple of options. On the OS option based on where you choose to store it :) As to the Novu Cloud solution, you can choose the EU (Frankfurt) or the US (Virginia). In case you are working on the Novu Hybrid-Cloud solution we will help you deploy your data inside your select network. By default, data is stored using the following TTL values: * Notifications (for 1 month) * Jobs (for 1 month) * Message (for in-app messages - 12 months, for all other messages - 1 month) * Execution details (for 1 month) * Subscribers, workflows, feeds, layouts (not deleted automatically, can be deleted by the user at any time) If you want to delete any specific data or information, reach out to us at [support@novu.co](mailto:support@novu.co) We are equally committed to our users and their data's security. We highly appreciate it if someone shares security vulnerabilities with us. Feel free to use the [github issue](https://github.com/novuhq/novu/security/advisories/new) or email us at [security@novu.co](mailto:security@novu.co). file: ./content/docs/platform/concepts/endpoint.mdx # Framework Endpoint Learn how to customize and extend any aspect of the Novu platform using the Framework SDK powered bridge endpoint. Novu was built for endless extensibility. Although not required for majority of your notification usecases, you can customize and extend any aspect of the Novu platform using the Framework SDK powered bridge endpoint. ## Overview The framework endpoint is a Restful API path that runs on your server and environment and can be used for multiple usecases including: * Automating workflow creation using GitOps * Adding custom components to your emails and other channels * Hydrating resources including subscribers, topics and tenants directly from your database * Executing custom logic before, during and after a workflow is executed * Connect to 3rd-party services such as OpenAI, Stripe, Salesforce and more. * Implement custom delivery providers with internal systems ## How it works How Novu works ### Build phase When [building new workflows](/platform/workflow/overview), you will be using the [Novu Framework SDK](/framework/typescript/overview) to define the workflows and their steps directly in your IDE. The local workflows can be previewed and tested using our [Local Studio](/framework/studio) companion app. ### Sync phase A sync is needed once a notification workflow code is created or updated. We offer multiple sync methods including CI/CD integrations and a custom CLI tool. During a sync, the workflows defined using the Novu Framework SDK will be pushed to our cloud service and will be persisted in our database. Once synced, you will be able to view the workflows in the workflows Page on the Dashboard. All the controls created using JSON Schema or Zod will be transformed into UI elements that can be modified without changing the source code. Read more about controls [here](/framework/controls). ### Execution phase Once a workflow is synced, the workflow engine will make API requests to the bridge endpoint to execute individual workflow steps. After the fan-out is complete, the bridge endpoint will be triggered with the relevant execution context and will respond with the compiled content and metadata needed to deliver the notification. ## Get Started To get started with the framework endpoint, visit the Getting Started page relevant to your tech-stack: The Framework SDK is currently only supported in Typescript for JS based run-times. file: ./content/docs/platform/concepts/environments.mdx # Environments Understanding and managing environments in Novu Novu uses environments to separate your development and production workflows. When you create an account, you'll get two default environments: `Development` and `Production`. ## Development Environment Use the development environment to test new notification workflows, validate changes before deploying to production, and experiment with different configurations. ## Production Environment The production environment is your live environment where notifications are sent to real users. ## Custom Environments Custom environments are available in Business and Enterprise plans Create custom environments to match your development workflow: 1. Go to the Environments page in your dashboard 2. Create environments like `Staging` or `QA` 3. Assign unique colors to easily distinguish between environments 4. Sync changes between environments just like with development and production The sync process works the same way as between development and production environments. ## What's Unique to Each Environment? Each environment maintains its own separate: * Subscriber list * Notification workflows * Message history * Activity feed * Integration settings * Notification feeds * Brand assets and settings ## Environment Credentials Each environment has two unique identifiers: 1. **Application Identifier** * Public ID for client-side apps * Unique per environment * Safe to expose in frontend code 2. **API Secret Key** * Used for backend API authentication * Keep this secure and never expose publicly * Different for each environment **Best Practice**: Configure these credentials in your application based on the active environment, similar to how you manage other service credentials. ## Promoting Changes to Production You can move changes from development to production in two ways: 1. Using the Dashboard's sync interface 2. Using the Sync API in your CI/CD pipeline 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. Each integration is scoped to a specific [environment](/platform/concepts/environments). You must create and configure separate integrations for each environment (for example, development, staging, production). ## Providers vs integrations A provider is the third-party service (for example, SendGrid for email or Twilio for SMS), and an integration is a configured instance of that provider within Novu. * One provider can have multiple integrations. * Each integration has: * A unique, editable identifier. * An optional name, which is useful when you have multiple integrations from the same provider. Novu uses the `providerId` as the default name. It is recommended to change the default name if you have multiple integrations from the same provider. 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/workflows/trigger-overrides) to modify the default behavior of a message during workflow trigger, such as overriding the notification title or content, using different integration that primary integration. This works alongside integrations to fine-tune delivery behavior. ## 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 is used by default whenever a workflow sends a message for that channel . You can: * Set or change the primary integration via the dashboard or API. * Mark an integration as inactive, which means Novu will no longer use it to send messages until it is marked active again. If no active integration is available for a sms or email channel, Novu cannot send notifications for email and sms channels. The corresponding workflow channel step will fail. ## Integration credentials Each integration requires a set of credentials to authenticate with the third-party provider. Secret credentials, such as API keys or tokens, are encrypted at rest, while non-secret fields, such as the sender name or from address, are stored as plain text. When setting up an integration, you may need to provide: * Sender information (for example, from email address, sender name). * Credentials such as API keys or tokens. * Optional configuration settings depending on the provider. Read [documentation](/platform/integrations/overview) for each provider to see the required credentials and configuration settings. ## Integration store You can manage integrations directly from the [Integration Store](https://dashboard.novu.co/integrations) in the Novu dashboard. From there, you can: * Add new integrations by providing credentials and configuration values. * Set a primary integration per channel. * Mark integrations as active or inactive as needed. * Delete an integration, but if it’s currently set as the primary integration, you’ll be prompted to select another active integration to take its place. ## Available integrations Novu supports a wide range of providers across different channels: ## Explore the integration API For implementation details and usage examples, refer to the [Integration API reference](/api-reference/integrations/integration-schema) documentation. file: ./content/docs/platform/concepts/notifications.mdx # Notifications 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 When creating a new workflow, you can specify default preferences for your subscribers via code or in the Dashboard. These preferences will be used during notification delivery unless the subscriber overrides via the preferences page in ``. Preferences ## Subscriber channel preferences per workflow `` displays the available preferences per workflow, allowing subscribers to modify them for each channel. Critical workflows will be excluded from the list. Inbox Workflow Preferences Inbox displays only channels used by the current 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. Inbox Global Preferences ## 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 Dashboard. Critical workflow are not displayed in the subscriber preferences page. 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. 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. ## 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-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: "Acme Corp", logo: "https://acme-corp.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) ## FAQ 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. 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. import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; 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 topic key must be unique and cannot be changed after creation; Novu enforces this uniqueness behind the scenes. Common use cases: * 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 for delivery and billing. Topics let you target large groups with a single API call, no 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 This ensures accurate delivery and billing while supporting massive audiences. ## 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. ## Explore the topics API For implementation details and usage examples, visit the [Topics API reference](/api-reference/topics/topic-schema). ## Frequently asked questions No. Topics only define delivery groups. Each subscriber’s individual notification preferences remain intact. Conceptually, yes. Topics group users to receive shared messages. However, topics are more dynamic and integrated into your application logic. Yes. Subscribers can be members of any number of topics. This allows overlapping targeting strategies (for example, `task-updates`, `project-X`, and `admin-notifications`). Topic keys must be unique within each environment. You can reuse the same key (for example, `feature-release`) in different environments like staging and production. The workflow are processed, but no notifications are delivered. 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` 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/webhooks.mdx # Webhooks Receive notifications when events occur in your Novu account Webhooks are currently in beta. If you encounter any issues, please reach out to our support team. ## The Intro Webhooks are how services notify each other of events. At their core they are just a POST request to a pre-determined endpoint. The endpoint can be whatever you want, and you can just add them from the UI. You normally use one endpoint per service, and that endpoint listens to all of the event types. For example, if you receive webhooks from Novu, you can structure your URL like: `https://www.example.com/novu/webhook/`. The way to indicate that a webhook has been processed is by returning a 2xx (status code 200-299) response to the webhook message within a reasonable time-frame (15s). It's also important to disable CSRF protection for this endpoint if the framework you use enables them by default. Another important aspect of handling webhooks is to verify the signature and timestamp when processing them. You can learn more about it in the signature verification section. ## Events and event types The core value of webhooks is to notify users when events happen, so it's extremely important to understand what events are available and their payload schemas. Novu webhooks allow you to receive notifications when specific events occur in your Novu account. These events include workflow updates, subscriber changes, and message delivery status updates. ### Supported event types Novu supports the following webhook event types: * **Workflow Events** (available): Notifications about workflow creation, updates, and deletions * **Subscriber Events** (upcoming): Notifications about subscriber creation, updates, and preference changes * **Message Events** (upcoming): Notifications about message delivery status changes Each event includes detailed information about the affected resource and the changes that occurred. ## How to add an endpoint To start listening to messages, you will need to configure your endpoints. 1. Go to the [Webhooks](https://dashboard.novu.co/webhooks) page in the Novu dashboard. 2. Click **Add Endpoint**. 3. Enter the URL of your endpoint. 4. Add description for this webhook endpoint. 5. Select the event types you want to listen to. 6. Optional: add advanced configuration for your endpoint. 7. Click **Create**. If your endpoint isn't quite ready to start receiving events, you can use a service like [Webhook.site](https://webhook.site/) or [RequestBin](https://requestbin.com/) to have a unique URL generated for you. ## How to test endpoints Once you've added an endpoint, you'll want to make sure it's working. The "Testing" tab lets you send test events to your endpoint. After sending an example event, you can view the message payload, all of the message attempts, and whether it succeeded or failed. ## Verifying signatures Webhook signatures let you verify that webhook messages are actually sent by Novu and not a malicious actor. For a more detailed explanation, check out this article on [why you should verify webhooks](https://docs.svix.com/receiving/verifying-payloads/why). Our webhook partner Svix offers a set of useful libraries that make verifying webhooks very simple. Here is an example using Javascript: ```javascript import { Webhook } from "svix"; const secret = "YOUR_WEBHOOK_SECRET_KEY"; // These were all sent from the server const headers = { "svix-id": "msg_p5jXN8AQM9LWM0D4loKWxJek", "svix-timestamp": "1614265330", "svix-signature": "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=", }; const payload = '{"test": 2432232314}'; const wh = new Webhook(secret); // Throws on error, returns the verified content on success const verifiedPayload = wh.verify(payload, headers); ``` For more instructions and examples of how to verify signatures, check out the [webhook verification documentation](https://docs.svix.com/receiving/verifying-payloads/how). ## Retry schedule ### Retries We attempt to deliver each webhook message based on a retry schedule with exponential backoff. #### The schedule Each message is attempted based on the following schedule, where each period is started following the failure of the preceding attempt: * Immediately * 5 seconds * 5 minutes * 30 minutes * 2 hours * 5 hours * 10 hours * 10 hours (in addition to the previous) If an endpoint is removed or disabled, delivery attempts to the endpoint will be disabled as well. For example, an attempt that fails three times before eventually succeeding will be delivered roughly 35 minutes and 5 seconds following the first attempt. ### Manual retries You can also use the application portal to manually retry each message at any time, or automatically retry ("Recover") all failed messages starting from a given date. ## Troubleshooting & Failure Recovery ### Common reasons why your webhook endpoint is failing There are some common reasons why your webhook endpoint is failing: * **Not using the raw payload body** This is the most common issue. When generating the signed content, we use the raw string body of the message payload. If you convert JSON payloads into strings using methods like stringify, different implementations may produce different string representations of the JSON object, which can lead to discrepancies when verifying the signature. It's crucial to verify the payload exactly as it was sent, byte-for-byte or string-for-string, to ensure accurate verification. * **Missing the secret key** From time to time we see people simply using the wrong secret key. Remember that keys are unique to endpoints. * **Sending the wrong response codes** When we receive a response with a 2xx status code, we interpret that as a successful delivery even if you indicate a failure in the response payload. Make sure to use the right response status codes so we know when messages are supposed to succeed vs fail. * **Responses timing out** We will consider any message that fails to send a response within 15 seconds a failed message. If your endpoint is also processing complicated workflows, it may timeout and result in failed messages. We suggest having your endpoint simply receive the message and add it to a queue to be processed asynchronously so you can respond promptly and avoid getting timed out. ### Re-enable a disabled endpoint If all attempts to a specific endpoint fail for a period of 5 days, the endpoint will be disabled. To re-enable a disabled endpoint, go to the webhook dashboard, find the endpoint from the list and select "Enable Endpoint". ### Recovering/Resending failed messages If your service has downtime or if your endpoint was misconfigured, you probably want to recover any messages that failed during the downtime. If you want to replay a single event, you can find the message from the UI and click the options menu next to any of the attempts. From there, click "resend" to have the same message send to your endpoint again. If you need to recover from a service outage and want to replay all the events since a given time, you can do so from the Endpoint page. On an endpoint's details page, click "Options > Recover Failed Messages". From there, you can choose a time window to recover from. For a more granular recovery - for example, if you know the exact timestamp that you want to recover from - you can click the options menu on any message from the endpoint page. From there, you can click "Replay..." and choose to "Replay all failed messages since this time." ## FAQs ### How do I secure my webhook endpoint? To secure your webhook endpoint, you should: 1. Verify the webhook signature using the Svix library 2. Use HTTPS for your endpoint URL 3. Implement rate limiting to prevent abuse 4. Keep your webhook secret secure and rotate it periodically ### What happens if my endpoint is unavailable? If your endpoint is unavailable, Novu will retry sending the webhook according to the retry schedule. If all attempts fail for 5 days, the endpoint will be disabled and you'll need to manually re-enable it. ### Can I filter webhooks by event type? Yes, when configuring your webhook endpoint, you can select specific event types to receive. This allows you to filter out events that aren't relevant to your use case. ### How can I test webhooks locally? To test webhooks locally, you can use a service like ngrok or localtunnel to expose your local server to the internet. Alternatively, you can use webhook testing services like Webhook.site or RequestBin to inspect webhook payloads. file: ./content/docs/platform/concepts/workflows.mdx # Workflows Learn what workflows are and how they work in Novu. import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; 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/inbox/overview.mdx # Overview Learn how to integrate Novu , a pre-built notification center component for real-time in-app notifications in your application. import { Card, Cards } from 'fumadocs-ui/components/card'; import { MessageSquare, Palette, Zap } from 'lucide-react'; The Inbox component enables a rich context-aware in-app notifications center directly in your application, and with minimal effort. Novu provides a pre-built, ready-to-use, and customizable Inbox UI component. It's in React today, and soon will be available in other popular frameworks including Vue, React Native, and full headless. Fully functional and customizable React Inbox component ## Getting Started ## Design Files To aide 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. file: ./content/docs/platform/integrations/demo-providers.mdx # Demo Providers Learn about the default Novu email and sms providers ## Novu Demo Providers To help you evaluate our services better, Novu provides an email demo provider by default for every account created. After signing up, you can go to the [Integrations store](https://dashboard.novu.co/integrations?utm_campaign=docs-default-providers) on the Novu Dashboard to see this. This feature is available only in Novu cloud. This feature will not work in community self hosted version and local environment. ## Novu Email Provider Novu offers a demo email provider that you can use to send emails to yourself during the evaluation period. This provider should not be used in production or staging environments. ### Limits of the Novu Demo Providers 300 emails per organization per month, and those can be only sent to the current logged in user. To send more than these limits, or to other email addresses, you can configure your own email provider from the Integration store. file: ./content/docs/platform/integrations/overview.mdx # Overview Discover how to integrate Novu with your tech stack including delivery providers, content frameworks, and validation libraries. import { Card, Cards } from 'fumadocs-ui/components/card'; Novu was designed to be integrated with any part of your tech stack. This includes: * Delivery providers * Content frameworks * Validation and schema libraries * and more! ## Delivery provider integrations You can find the list of available integrations for each channel: 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 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. ## 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. ### Override structure 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. #### Step-level provider 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'; 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](https://dashboard-v2.novu.co/auth/sign-up) or [sign in](https://dashboard-v2.novu.co/auth/sign-in) to an existing account. ### Create an Angular application Create a new Angular app with [angular cli](https://angular.dev/tools/cli) using the command below. Skip this step if you already have an existing project: ```bash ng new novu-inbox-angular ``` Navigate to the newly created project: ```bash cd novu-inbox-angular ``` ### Install the required package Run the command below to install [Novu Javascript SDK](/platform/sdks/javascript), which provides required component for Inbox UI: ```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.component.ts` 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."}: ```ts title="src/app/app.component.ts" import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; import { RouterOutlet } from '@angular/router'; import { NovuUI } from '@novu/js/ui'; @Component({ selector: 'app-root', standalone: true, imports: [RouterOutlet], templateUrl: './app.component.html', styleUrl: './app.component.css', }) export class AppComponent implements AfterViewInit { @ViewChild('novuInbox') novuInbox!: ElementRef; title = 'inbox-angular'; ngAfterViewInit() { const novu = new NovuUI({ options: { applicationIdentifier: 'YOUR_APPLICATION_IDENTIFIER', subscriberId: 'YOUR_SUBSCRIBER_ID', }, }); novu.mountComponent({ name: 'Inbox', props: {}, element: this.novuInbox.nativeElement, }); } } ```
[Sign in](https://dashboard-v2.novu.co/auth/sign-up) to get your own API keys
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.component.html` file: ```html title="src/app/app.component.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 Create an account and learn how to start using Novu notification Inbox in your Next.js application. 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](https://dashboard.novu.co/auth/sign-up) or [sign in](https://dashboard.novu.co/auth/sign-in) to access the Novu dashboard. ### Install `@novu/nextjs` In your Next.js project, run the following command to install the Novu Inbox 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 Create an account and learn how to start using Novu Inbox Notification in your 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'; 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](https://dashboard.novu.co/auth/sign-up) or [sign in](https://dashboard.novu.co/auth/sign-in) to an existing account. ### Create a React application Create a new React app with [Vite](https://vite.dev/guide/#scaffolding-your-first-vite-project) using the command below. Skip this step if you already have an existing project: ```bash npm create vite@latest my-app -- --template react-ts ``` ```bash pnpm create vite my-app --template react-ts ``` ```bash yarn create vite my-app --template react-ts ``` ```bash bunx create-vite my-app --template react-ts ``` ### Install the required packages Run the command below to install [Novu React SDK](/platform/sdks/react), which provides React components for building notification UIs and React Router Dom: ```bash npm install @novu/react react-router-dom ``` ```bash pnpm add @novu/react react-router-dom ``` ```bash yarn add @novu/react react-router-dom ``` ```bash bun add @novu/react react-router-dom ``` ### Create the Inbox component In the `src` 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."}: ```tsx title="src/components/notification-center.tsx" import React from 'react'; import { Inbox } from '@novu/react'; import { useNavigate } from 'react-router'; export function NotificationCenter() { const navigate = useNavigate(); return ( navigate(path)} /> ); } ```
[Sign in](https://dashboard.novu.co/auth/sign-up) to get your own API keys
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 add the Notification Center Now you can set up React Router and add the `NotificationCenter` component to your app layout: ```tsx title="src/App.tsx" import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { NotificationCenter } from './components/notification-center'; function Layout({ children }: { children: React.ReactNode }) { return (
{children}
); } 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'; 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](https://dashboard.novu.co/auth/sign-up) or [sign in](https://dashboard.novu.co/auth/sign-in) to an existing account ### Create a Remix application Create a new Remix app using the command below. Skip this step if you already have an existing project: ```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’s inbox package ```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."}: ```tsx title="app/components/notification-center.tsx" import React from 'react'; import { Inbox } from '@novu/react'; import { useNavigate } from '@remix-run/react'; export function NotificationCenter() { const navigate = useNavigate(); return ( navigate(path)} /> ); } ```
[Sign in](https://dashboard.novu.co/auth/sign-up) to get your own API keys
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/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'; 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](https://dashboard-v2.novu.co/auth/sign-up) or [sign in](https://dashboard-v2.novu.co/auth/sign-in) to an existing account. ### Create a Vue application Create a new vue app with create-vue package. Skip this step if you already have an existing project: ```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 ``` Navigate to the newly created project: ```bash cd novu-inbox-vue ``` ### Install the required package Run the command below to install [Novu Javascript SDK](/platform/sdks/javascript), which provides required component for Inbox UI: ```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."}: ```vue title="src/components/NovuInbox.vue" ```
[Sign in](https://dashboard-v2.novu.co/auth/sign-up) to get your own API keys
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. #### 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. } color="#dc2626" href="/platform/sdks/server/dotnet"> Connect your C#/.NET app to Novu via the .NET 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/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) Once you've filled in the required fields, click on **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. 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 steps to control timing and pacing in your notification workflows. The delay action awaits a specified amount of time before moving on to trigger the following steps of the workflow. ## 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 * 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. 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/platform/workflow/digest.mdx # Digest Engine Collect Multiple Events to a Single Message with the Digest Engine import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; The digest engine collects multiple trigger events, aggregates them into a single message, and delivers that new payload to the next workflow step. This becomes useful when a user would otherwise receive a large number of notifications, and you want to avoid over-notifying. When you use a Digest action step, Novu automatically batches the incoming trigger events based on the `subscriberId` and an **optional** `digestKey` that can be added to control the digest logic of the events. ## Digest action step After adding a digest step to a workflow, each node that will be below the digest node will be only triggered once in the specified digest interval. You can decide to send messages before adding a digest node and they will be triggered in real-time. Digest Engine ## Digest configuration ### Digest key If specified, the digest engine will group the events based on the `digestKey` and `subscriberId`, otherwise the digest engine will group the events based only on the subscriberId. The digest key might come useful when you want a particular subscriber to get events grouped on a custom field. For example when an actor likes the user's post, you might want to digest based on the `post_id` key. ### Time interval The time interval determines how long the digest engine will wait before sending the message once created. You can specify the amount and the unit that best suits your needs. In the image below, `5` is the interval amount, and `mins` is the interval unit. Interval units can be `sec(s)`, `min(s)`, `hour(s)`, or `day(s)`. ## Digest strategy types The strategy which Novu should use to handle the digest step. More details on available strategies below. Novu allows you to define different digest strategies depending on the actual use-case you are trying to achieve. At this point we allow you to select from 2 strategies: * Regular * Look-back * Scheduled Let's explore them in detail: ### Regular strategy In regular strategy, a digest will always be created for the specified window time. Which means that from the first event trigger, if no active digest exists for this subscriber, one will be created and the user will receive the message only when the digest window time is reached. ### Look-back strategy In the Look-Back strategy, before creating a digest, Novu will check if a message was sent to the user in the Look-back period. If a message was sent, a digest will be created. Otherwise, a message will be sent directly to the user and the digest creation will be skipped. Look-back digest has two intervals, `digest interval` and `look-back window`. First, it checks if any event is triggered within the past look-back window, only then a digest is created for the digest interval. If not, the event is considered non-digest and workflow execution continues to the next step. #### Example Let's set the digest interval as 20 minutes and the look-back interval as 15 minutes. If we trigger the first event. Since it is the first event and there was no event triggered in the past 15 minutes (look-back interval), this event will send a message immediately (without digest). Now, if we trigger a second event within 15 minutes range, then a new digest will be created with this second event. From now on, for the next 20 minutes (digest interval), all triggers will be digested, and after 20 minutes, the workflow will carry forward to the next step with digested events as a payload. ### Scheduled digest All digest times are in UTC time Digest incoming events for the specified time. Once that time threshold since the first event has passed, proceed to the next workflow step. Available timeframes: * Minutes * Hours * Daily * Weekly ## Consuming digested events The digest step collects and groups multiple events over time. Once digested, the results can be used directly in your notification templates to create clear and concise summaries, such as daily digests or batch notifications. Novu provides built-in variables that expose the results of the digest step. These variables can be referenced in your templates to display totals, format summaries, or present grouped event data using LiquidJS filters. These variables are automatically exposed in all variable-supporting inputs following a digest step, as the digest payload determines their values. ### Digest variables Digest variables can be used to reference the list of events and their count, making it possible to create dynamic content inside your templates based on the actual events processed during the digest window. #### steps.digest-step.events The `steps.digest-step.events` variable is an array that contains all the events collected by the digest step. Each item in the array represents a digested event, including the payload with which the workflow was triggered. You can loop through it to build dynamic message lists, such as comment summaries or blog post digests, and use it with LiquidJS filters. ``` New posts published: {{steps.digest-step.events | toSentence:'payload.title', 2, 'more'}} ``` If two events were collected, the rendered message would be: ``` New posts published: Scaling Node.js Apps, and Designing for Accessibility ``` If three or more events were collected, the rendered message would be: ``` New posts published: Scaling Node.js Apps, and Designing for Accessibility, and more. ``` #### steps.digest-step.eventCount The `steps.digest-step.eventCount` variable holds the total number of events collected during the digest step window. It is an integer value and can be used directly or passed into LiquidJS filters like pluralize to generate grammatically correct summaries. ``` You have {{steps.digest-step.eventCount | pluralize: "new comment"}}. ``` If one event was collected, then the rendered message would be: ``` You have 1 new comment. ``` If two events were collected, then the rendered message would be: ``` You have 2 new comments. ``` ### Digest template variables Digest template variables are designed to simplify how you represent digested event data in user-facing messages. They wrap digest variables and LiquidJS filters to output preformatted summaries with minimal effort. #### steps.digest-step.countSummary The `countSummary` variable returns a sentence fragment summarizing the total number of digested events with correct pluralization. This variable is built on top of: * `steps.digest-step.eventCount` * The LiquidJS pluralize filter Internally, `countSummary` uses the event count to apply plural logic. To use `countSummary` in the editor, select it from the variables dropdown. This will automatically insert the variable like this: ``` You have {{steps.digest-step.eventCount | pluralize: 'notification'}} ``` For example, if the digest collected five events, then the output would be: ``` You have 5 notifications ``` If only one event was collected: ``` you have 1 notification ``` #### steps.digest-step.sentenceSummary The `sentenceSummary` variable returns a sentence-style string listing key items from the digested events array, such as a list of names, and gracefully handles formatting depending on the number of items present. This variable is built on top of: * `steps.digest-step.events` * The LiquidJS `toSentence` filter You can configure the `toSentence` filter by passing: * A key path to extract from each item (for example, `payload.name`). This is a required argument. * The number of items to display before collapsing (for example, 3) * The suffix to use for remaining items (for example, "others") Internally, `sentenceSummary` uses the events array to generate the sentence. To use `sentenceSummary` in the editor, select it from the variables dropdown. This automatically inserts the variable as shown: ``` {{steps.digest-step.events | toSentence:'payload.name', 3, 'others'}} replied to your post. ``` If the events contain names "Radek", "Dima", and five more users, then the result would be: ``` Radek, Dima, and 5 others replied to your post. ``` If there are only two events: ``` Radek and Dima replied to your post. ``` These variable allows for dynamic personalization without writing custom iteration logic in your template. ## Email editor digest block The digest block is available in the email editor when working with workflows that include a digest step. It loops over the events collected by your digest step and displays them in your email content, handling both layout and repetition automatically. When you add a digest block to your email template: * It automatically inserts a repeat block that loops over `steps.digest-step.events`, with the maximum number of iterations defaulting to five. * Inside this loop, you define the structure once (for example, how each comment or notification looks), and it repeats for every event. * Inside the loop, you can also use the special alias variable `current` to reference the item currently being processed (for example, `current.payload.name`). * The block also supports rendering a summary using the `steps.digest-step.eventCount` variable, typically with the pluralize LiquidJS filter. You don’t need to write any code manually. The Email Editor handles the looping logic for you. Simply drag the digest block into your message and customize the layout visually using the available variables. The digest Block only appears if your workflow has a preceding digest step. This block will not be shown in the editor without a digest step. ## Frequently Asked Questions ### If scheduled digest is set for 9:00 AM daily then will the digest be sent at 9:00 AM every day without any event triggered?" If scheduled digest is sent for 9:00 daily, then novu will collect all events triggered between 9:00 AM today till 9:00 AM tomorrow and send the digest at 9:00 AM tomorrow. This process is repeated daily. If there is no any event triggered between 9:00 AM today and 9:00 AM tomorrow, then no digest will be sent. ### What is the difference between Regular and Scheduled Digest set to 1 Hour? Both digests are same in this case. ### 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/platform/workflow/overview.mdx # Overview The workflow Editor combines no-code simplicity and code-based flexibility, enabling users to design and manage advanced notification workflows tailored to their needs. import { Card, Cards } from 'fumadocs-ui/components/card'; import { ArrowRightIcon } from 'lucide-react'; The workflow Editor is a robust visual tool that empowers both no-code users and developers to design advanced notification workflows. It seamlessly combines the intuitive simplicity of no-code building blocks with the adaptability and precision of code-based customization. ## What is a workflow? A workflow in Novu is a container for all notification/message logic and templates within your system. Each workflow: * Has a unique identifier (key) * Executes for one subscriber at a time (e.g. end user, recipient, customer, etc.) * Contains complete notification/message logic and templates * Supports subscriber preference management * Can be triggered via API calls, events, or scheduled operations ## Different types of Novu Workflows ### Visual workflow editor (No-Code) **Best suited for:** * Straightforward use cases without complex logic * Building emails using Novu's Email WYSIWYG Editor * Modifying existing workflows * Quick prototyping, testing, and iteration * Collaboration with non-technical stakeholders ### Framework SDK (Code-Based) **Best suited for:** * Complex workflow logic implementation * External API integration * Custom data transformation * Advanced routing rules * Type safe workflow payloads * Specialized business logic * Complex conditional branches * Custom email templates (React Email, Vue Email, MJML etc.) * Workflow versioning [Learn more about Novu Framework](/framework/overview) file: ./content/docs/platform/workflow/step-conditions.mdx # Step Conditions Create dynamic notification workflows using rule-based conditions. Control message delivery based on subscriber data, payload information, and workflow outcomes. The Step Condition feature in Novu enables you to define conditional logic for each workflow step (node), ensuring a precise and tailored notification experience. This feature adds flexibility and control by allowing you to determine whether a step should execute based on subscriber data, payload data, or conditions stemming from previous workflow steps. ## What Are Workflow Step Conditions? When adding a workflow step (node) in Novu, it can either be: 1. **Channel Step**: In-App, Email, SMS, Push, or Chat. 2. **Action Step**: Digest or Delay. Each step includes the ability to configure **step conditions** that define whether the step is executed. The conditions can combine multiple logical rules using **AND** and **OR** operators. ## Areas for Configuring Step Conditions ### 1. **Subscriber Variables** Conditions can leverage subscriber-related fields to tailor notifications based on user-specific data. Examples of subscriber variables include: * `subscriber.firstName` * `subscriber.lastName` * `subscriber.email` * `subscriber.phone` * `subscriber.avatar` * `subscriber.locale` * `subscriber.data` * `subscriber.subscriberId` * `subscriber.isOnline` * `subscriber.lastOnlineAt` For instance, you might want to send an SMS only to users whose `subscriber.isOnline` is `false`. ### 2. **Payload Data** Conditions can also depend on custom payload data passed during the workflow trigger call to the Novu API. This allows you to define dynamic rules based on the data unique to each workflow execution. Example payload data: ```json { "orderId": "12345", "totalAmount": 150, "orderStatus": "completed" } ``` You can configure conditions such as: * `payload.orderStatus = "completed"` * `payload.totalAmount > 100` This makes it possible to create dynamic notifications based on context-specific information. ### 3. **Previous Step Conditions** For workflows involving sequential steps, conditions can also depend on the **outcome of a previous step**. For example, if the prior step was an **In-App Notification**, the condition could check: * `steps.in-app-step.seen` * `steps.in-app-step.read` * `steps.in-app-step.lastSeenDate` * `steps.in-app-step.lastReadDate` This is especially useful for tailoring follow-up notifications. For instance, send an email only if the In-App notification was not read within 24 hours. ### 4. **Advanced Application State Calculations** For more complex scenarios where you need to perform advanced calculations based on your application state, you can use the [Novu Framework ](/framework/typescript/steps). This approach allows you to: * Access your application's database or external services * Perform complex business logic calculations * Make API calls to external services * Execute custom validation rules The skip step gives you full programmatic control over whether a notification step should be executed, going beyond the built-in condition builder capabilities. ## Query Builder Options The query builder enables you to construct powerful logical expressions for your step conditions. Supported operators include: | Operator | Description | Example | | --------------------- | --------------------------------------- | ----------------------------------------------- | | `=` | Equal to | `subscriber.locale = "en-US"` | | `!=` | Not equal to | `subscriber.isOnline != true` | | `<` | Less than | `payload.totalAmount < 100` | | `>` | Greater than | `payload.totalAmount > 100` | | `<=` | Less than or equal to | `payload.totalAmount <= 200` | | `>=` | Greater than or equal to | `payload.totalAmount >= 200` | | `contains` | Contains a substring | `payload.orderId contains "123"` | | `begins with` | Starts with | `subscriber.firstName begins with "J"` | | `ends with` | Ends with | `subscriber.email ends with "@xyz.com"` | | `does not contain` | Does not contain a substring | `payload.orderId does not contain "456"` | | `does not begin with` | Does not start with | `subscriber.firstName does not begin with "M"` | | `does not end with` | Does not end with | `subscriber.lastName does not end with "Smith"` | | `is null` | Is null | `subscriber.phone is null` | | `is not null` | Is not null | `subscriber.email is not null` | | `in` | Matches one of several values | `subscriber.locale in ["en-US", "es-ES"]` | | `not in` | Does not match any of the listed values | `subscriber.locale not in ["fr-FR", "de-DE"]` | | `between` | Within a range | `payload.totalAmount between [50, 200]` | | `not between` | Outside of a range | `payload.totalAmount not between [0, 50]` | ## Using Dynamic Data Fields for Comparison The **value** field in a condition can also be a dynamic data field. This allows you to compare two data points dynamically rather than using static values. For example: ```json { "operator": "AND", "conditions": [ { "field": "payload.foo", "operator": "=", "value": "{{payload.bar}}" } ] } ``` Step Conditions In this case, the step will execute only if `payload.foo` is equal to `payload.bar` at runtime. You can also use subscriber variables in the same way: ```json { "operator": "AND", "conditions": [ { "field": "subscriber.firstName", "operator": "=", "value": "{{payload.firstName}}" } ] } ``` Step Conditions This enables flexible condition logic based on real-time data comparisons. ## Building Condition Groups Novu allows you to group multiple conditions using **AND** and **OR** operators to create complex logic. For instance: * **AND Group**: All conditions in the group must be true for the step to execute. * **OR Group**: At least one condition in the group must be true. Condition groups can also be nested for advanced use cases. Novu's Step Condition feature empowers you to build intelligent and dynamic workflows tailored to your specific use cases. By leveraging subscriber data, payload information, and step outcomes, you can ensure that each notification reaches the right audience at the right time with the appropriate content. file: ./content/docs/platform/workflow/tags.mdx # Tags Learn how to organize and manage notification tags using Novu's visual workflow editor for better user experience and notification management. **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. For example, you might want to group all security-related notifications together, separate from updates about account activity or promotional offers. ## Why Use Tags? * **Filtering Notifications**: Tags make it easy to filter and display notifications based on categories. For instance, you can create a UI that allows users to view only security or promotional notifications. Learn more about using tags to filter notifications in the [Inbox](/platform/inbox/react/multiple-tabs) section. Think of it as organizing emails into folders—tags help keep things tidy and manageable for both you and your users. ## How to Add Tags to Notifications Navigate to the **"Workflows"** tab Create a new workflow or edit an existing workflow One of the fields in the workflow is the **Tags** field. You can add one or more (max. 16) tags to a notification ## Best Practices for Using Tags * **Define Categories Early**: Identify key notification categories for your app, such as security, promotions, or updates. * **Consistent Naming**: Use clear and consistent tag names to avoid confusion (e.g., prefer security over sec\_alert). * **Keep It Manageable**: Avoid overloading with too many tags. Focus on meaningful groupings that provide real value. Tags are a powerful way to streamline your notification system, helping users stay organized and informed while giving you greater control over your app's notification behavior. file: ./content/docs/platform/workflow/template-editor.mdx # Template Editor Learn how to use the Novu notification template editor to design notifications import { Card, Cards } from 'fumadocs-ui/components/card'; import { FilterIcon } from 'lucide-react'; Each channel step in a Novu workflow comes with its own notification template. This template defines how notifications appear for a specific channel. Quality templates are used to create personalized, visually appealing, and effective notifications. * **Injecting variables from your trigger data into your notification template.** * **Using Liquid syntax for logic and control flow within templates.** * **Previewing and testing your notification templates.** ## Personalizing Notifications with Template Variables To insert a variable into your Novu notification template, use double curly braces: `{{ variable_name }}`. Novu templates allow you to reference several types of variables: ### Data payload variables These variables originate from the data payload in your workflow trigger. For example, if you include `{ "order_id": "12345" }` in your payload, you can reference it in your template as `{{ payload.order_id }}`. ### User properties You can access user properties (like `firstName` or custom subscriber properties) using `{{ subscriber.* }}`. For instance: ```liquid Hi {{ subscriber.firstName }}, You've been upgraded to the {{ subscriber.data.plan }} plan. Thanks, The Novu Team ``` ## Variable Popover When clicking on a variable in the template editor, a popover will appear. This popover can be used to easily manipulate the variable formatting by applying default values or Liquid Filters. Variable Popover ### Applying Liquid Filters The variable popover will display a list of suggested Liquid Filters based on the variable type, you can apply one or more filters to the variable and re-order using drag and drop. Re-ordering filters is useful as the filters are applied in the order they are listed, and the output of each filter is passed to the next one. ### Previewing filters output With more advanced filter logic, you can preview the output of the filters by clicking on the **Preview** button and pass the variable value to see how the filters will be applied. ### Raw Liquid.js syntax You can also apply raw Liquid.js syntax to the variable by clicking on the **Raw** button which will reveal the raw Liquid.js content that will be applied to the variable. ## Adding logic with Liquid Filters Novu supports Liquid filters to add dynamic and conditional content to your notifications. Below are examples of how to use the top 10 Liquid filters in real-world notification templates. Learn more about the Liquid Templating Language. ### `capitalize` Use `capitalize` to ensure proper formatting for user names. ```liquid Hello {{ subscriber.firstName | capitalize }}, Welcome to Novu! We're excited to have you on board. ``` **Output**:\ `Hello John, Welcome to Novu! We're excited to have you on board.` ### `upcase` Use `upcase` for emphasizing specific information like workspace names. ```liquid Your workspace {{ payload.workspaceName | upcase }} has been successfully created. ``` **Output**:\ `Your workspace TEAM ALPHA has been successfully created.` ### `downcase` Use `downcase` for consistent email formatting or usernames. ```liquid Hi {{ subscriber.email | downcase }}, We've sent a confirmation to your inbox. ``` **Output**:\ `Hi john.doe@example.com, We've sent a confirmation to your inbox.` ### `date` Use `date` to format subscription or event dates. ```liquid Your subscription will renew on {{ payload.renewalDate | date: "%B %d, %Y" }}. ``` **Output**:\ `Your subscription will renew on December 31, 2024.` ### `truncate` Use `truncate` to shorten long content like notification messages. ```liquid New comment on your post: {{ payload.commentText | truncate: 20 }} Click here to read more. ``` **Output**:\ `New comment on your post: Great work on your... Click here to read more.` ### `truncatewords` Use `truncatewords` to limit the number of words in a preview. ```liquid {{ subscriber.firstName }}, here's a preview of the article: {{ payload.articleExcerpt | truncatewords: 5 }} ``` **Output**:\ `John, here's a preview of the article: Novu is a flexible and...` ### `replace` Use `replace` to dynamically update template content. ```liquid Hi {{ subscriber.firstName }}, Your {{ payload.subscriptionType | replace: "basic", "premium" }} subscription is active. ``` **Output**:\ `Hi John, Your premium subscription is active.` ### `split` Use `split` to parse tags or interests. ```liquid You have new updates in {{ payload.tags | split: "," | join: ", " }}. ``` **Input**:\ `"announcements,updates,offers"` **Output**:\ `You have new updates in announcements, updates, offers.` ### `join` Use `join` to list multiple items in a human-readable way. ```liquid Hello {{ subscriber.firstName }}, You have the following items pending: {{ payload.tasks | join: ", " }}. ``` **Input**:\ `["Upload documents", "Confirm email", "Schedule meeting"]` **Output**:\ `Hello John, You have the following items pending: Upload documents, Confirm email, Schedule meeting.` ### `default` Use `default` to provide fallback values. ```liquid Hi {{ subscriber.nickname | default: subscriber.firstName }}, Your account settings are updated. ``` **Output (when nickname is null)**:\ `Hi John, Your account settings are updated.` } href="https://liquidjs.com/filters/overview.html?utm_source=Novu" description="Learn more about 40+ filters supported by LiquidJS" /> ## Previewing and testing notification templates When your notification template is ready, use the **Preview** mode to visualize how your notification will look. You can: * **Test dynamic payload data:** Provide sample data to see how your template renders with different values. * **Send test notifications:** Save your template, return to the workflow canvas, and run a test with real trigger data. file: ./content/docs/framework/typescript/steps/chat.mdx # Chat Learn how to use the chat step to send messages to chat platforms like Slack, Discord, and Microsoft Teams The `chat` step allows you to send messages to various chat platforms like Slack, Discord, and Microsoft Teams. ## Example Usage ```tsx await step.chat('chat', async () => { return { body: 'A new post has been created', }; }); ``` ## Chat Step Output | Property | Type | Required | Description | | -------- | ------ | -------- | ------------------------------------------ | | body | string | Yes | The message to be sent to the chat channel | ## Chat Step Result The `chat` step does not return any result object. file: ./content/docs/framework/typescript/steps/custom.mdx # Custom Learn how to use the custom step to execute arbitrary code in your workflow The `custom` step allows you to execute arbitrary code within your workflow and persist the results for use in subsequent steps. This is useful for integrating with external services, performing complex calculations, or implementing custom business logic. ## Example Usage ```tsx const result = await step.custom( 'fetch-user-data', async () => { const response = await fetch('https://api.example.com/users/123'); const userData = await response.json(); return { name: userData.name, email: userData.email, preferences: userData.preferences, }; }, { outputSchema: { type: 'object', properties: { name: { type: 'string' }, email: { type: 'string' }, preferences: { type: 'object' }, }, required: ['name', 'email'], }, } ); // Use the result in a subsequent step await step.email('welcome-email', async () => { return { subject: `Welcome ${result.name}!`, body: `We'll send updates to ${result.email}`, }; }); ``` ## Custom Step Options | Property | Type | Required | Description | | ------------ | ---------- | -------- | ------------------------------------------------------- | | outputSchema | JSONSchema | No | JSON Schema definition for validating the step's output | The output schema is used to validate the step's return value and provide TypeScript type inference. If not provided, the return type will be `unknown`. ## Custom Step Result The custom step returns whatever value your code returns, validated against the outputSchema if provided. This result can be used in subsequent steps within the same workflow. file: ./content/docs/framework/typescript/steps/delay.mdx # Delay Learn how to use the delay step to pause workflow execution for a specified duration 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. ## Example Usage ```tsx // Wait for 24 hours await step.delay('reminder-delay', async () => { return { amount: 24, unit: 'hours', }; }); // Send a follow-up email await step.email('follow-up', async () => { return { subject: 'How are you liking our product?', body: 'We noticed you signed up yesterday. How has your experience been so far?', }; }); ``` ## Delay Step Output | Property | Type | Required | Description | | -------- | ------------------------------------------------------------------ | -------- | ------------------------------------ | | amount | number | Yes | The number of time units to delay | | unit | 'seconds' \| 'minutes' \| 'hours' \| 'days' \| 'weeks' \| 'months' | Yes | The time unit for the delay duration | ## Delay Step Result | Property | Type | Description | | -------- | ------ | ---------------------------------------- | | duration | number | The total delay duration in milliseconds | The delay step can be skipped conditionally using the `skip` option, allowing you to implement dynamic delay logic based on your workflow's needs. ## Conditional Delay Example ```tsx await step.delay( 'premium-user-delay', async () => { return { amount: 24, unit: 'hours', }; }, { // Skip the delay for premium users skip: async () => user.isPremium, } ); ``` file: ./content/docs/framework/typescript/steps/digest.mdx # Digest Learn how to use the digest step to aggregate multiple events into a single notification The `digest` step allows you to collect multiple events over a specified time period and combine them into a single notification. This is useful for reducing notification fatigue and providing better context to your users. ## Example Usage ```tsx const { events } = await step.digest('daily-summary', async () => { return { amount: 1, unit: 'days', }; }); await step.email('digest-email', async () => { return { subject: `Daily Summary (${events.length} updates)`, body: `You have ${events.length} new updates today`, }; }); ``` ## Digest Step Output | Property | Type | Required | Description | | --------- | ------------------------------------------------------------------ | -------- | ---------------------------------------------- | | amount | number | Yes | The number of time units to collect events for | | unit | 'seconds' \| 'minutes' \| 'hours' \| 'days' \| 'weeks' \| 'months' | Yes | The time unit for the digest period | | cron | string | No | The cron expression to use for the digest | | digestKey | string | No | The key to use for digesting events | Use either cron or amount-unit. Using both will result in an error. ## Digest Step Result | Property | Type | Description | | -------- | -------------- | -------------------------------------------------- | | events | DigestEvent\[] | Array of events collected during the digest period | ## DigestEvent Type | Property | Type | Description | | -------- | ------ | ------------------------------------------- | | id | string | The unique identifier of the digested event | | time | Date | The timestamp when the event was triggered | | payload | object | The original payload passed to the event | The digest step result can only be used in subsequent steps within the same workflow. You cannot access digest information in step controls. ## Using Digest Events You can use the digested events to create rich, aggregated notifications. Here's an example: ```tsx import { ActivityDigestEmail } from './ActivityDigestEmail'; const { events } = await step.digest('activity-digest', async () => { return { amount: 1, unit: 'hours', }; }); await step.email('digest-notification', async () => { const activities = events.map((event) => ({ type: event.payload.type, user: event.payload.userName, action: event.payload.action, })); return { subject: `Activity Summary (${events.length} updates)`, body: render(), }; }); ``` ## 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(), }), } ); ``` Changes to the workflow content after triggering will not affect existing digested events. The content is determined at the time of event digestion. file: ./content/docs/framework/typescript/steps/email.mdx # Email Learn how to use the email step to send email notifications The `email` step allows you to send email notifications to your users. This step supports HTML content and can be used with React Email for building beautiful email templates. ## Example Usage ```tsx await step.email('welcome-email', async () => { return { subject: 'Welcome to Our Platform', body: "Hello and welcome! We're excited to have you on board.", }; }); ``` ## Email Step Output | Property | Type | Required | Description | | ----------- | ------------- | -------- | --------------------------------------------------- | | subject | string | Yes | The subject line of the email | | body | string | Yes | The content of the email. Can be plain text or HTML | | attachments | Attachment\[] | No | Array of files to attach to the email | | from | string | No | Override the default from address | | replyTo | string | No | Set a reply-to address for the email | ## Email Step Result The `email` step does not return any result object. file: ./content/docs/framework/typescript/steps/inApp.mdx # In-App Learn how to use the In-App step to send notifications to your web or mobile app's inbox The `inApp` step allows you to send a message to your user's `` for your web or a mobile app. ## Example Usage ```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, }, }; }); ``` ## Return Value ```tsx const { seen, read, lastSeenDate, lastReadDate } = await step.inApp('inbox', resolver); ``` ## In-App Step Output ### body * **Type**: `string` * **Required**: Yes * **Description**: The body of the inbox notification. The main content of the notification. ### subject * **Type**: `string` * **Required**: No * **Description**: The subject of the inbox notification. This property communicates the subject of the notification to the user. ### avatar * **Type**: `string` * **Required**: No * **Description**: The avatar shown in the inbox notification. When specified, overrides the avatar of the actor initiating the notification. ### redirect * **Type**: `object` * **Required**: No * **Description**: The redirect object is used to define the URL to visit when interacting with the notification. This property can be omitted in place of an `onNotificationClick` [handler](/platform/inbox/react/components#handle-notification-click) implemented in the `` component. * **Properties**: * `url` (string, required): The URL to visit when clicking on the notification. * `target` (string): The target attribute specifies where the new window or tab will be opened. Defaults to `_blank`. Supported values: `_self, _blank, _parent, _top, _unfencedTop`. ### primaryAction * **Type**: `object` * **Required**: No * **Description**: Define a primary action to be shown in the inbox notification. The primary action will appear with an accent color. * **Properties**: * `label` (string, required): The label of the action. * `redirect` (object): * `url` (string, required): The URL to visit when clicking on the notification action button. * `target` (string): The target attribute specifies where the new window or tab will be opened. Defaults to `_blank`. Supported values: `_self, _blank, _parent, _top, _unfencedTop`. ### secondaryAction * **Type**: `object` * **Required**: No * **Description**: Define a secondary action to be shown in the inbox notification. The secondary action will appear with a muted color. * **Properties**: * `label` (string, required): The label of the action. * `redirect` (object): * `url` (string, required): The URL to visit when clicking on the notification action button. * `target` (string): The target attribute specifies where the new window or tab will be opened. Defaults to `_blank`. Supported values: `_self, _blank, _parent, _top, _unfencedTop`. ### data * **Type**: `object` * **Required**: No * **Description**: Custom data to be sent with the notification. This data can be used to [customize the notification item rendered](/platform/inbox/react/styling#render-notification-component) in the ``. ## In-App Step Result ### seen * **Type**: `boolean` * **Required**: Yes * **Description**: A flag indicating if the notification has been seen. This property is useful when conditionally delivering notifications in subsequent steps via the `skip` function. A notification is marked as seen when the user views the notification. ### read * **Type**: `boolean` * **Required**: Yes * **Description**: A flag indicating if the notification has been read. A notification is marked as read when the user confirms the notification. ### lastSeenDate * **Type**: `date | null` * **Required**: Yes * **Description**: The date the notification was last seen. This corresponds to the date the `seen` property was set to `true`, or `null` if the notification has not been seen. ### lastReadDate * **Type**: `date | null` * **Required**: Yes * **Description**: The date the notification was last read. This corresponds to the date the `read` property was set to `true`, or `null` if the notification has not been read. file: ./content/docs/framework/typescript/steps/index.mdx # Steps Learn about the Novu Framework step interface and its configuration options import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; ## Examples ```tsx await step.email('stepId', resolver, { controlSchema: z.object({ subject: z.string(), components: z.array(z.object({ type: z.enum(['text', 'button']), content: z.string(), })), }), }); ``` ```tsx await step.email('skipped-step', async () => ({ subject: 'Hello, world!', body: 'My email message', }), { skip: async (controls) => true, }); ``` ```tsx await step.inApp( 'without-sanitization', async () => ({ body: 'My in-app message', data: { link: '/pipeline/123?active=true&env=prod', }, }), { // Prevent the `&` character in `data.link` from // being converted to `&` disableOutputSanitization: true, } ); ``` ```tsx await step.email('provider-override', resolver, { providers: { slack: ({ controls, outputs }) => { return { text: 'A new post has been created', blocks: [{ type: 'section', text: { type: 'mrkdwn', text: 'A new post has been created', }, }], }; } } }); ``` ```tsx await step.email('provider-passthrough', resolver, { providers: { sendgrid: ({ controls, outputs }) => { return { _passthrough: { body: { ip_pool_name: 'my-ip-pool', }, headers: { 'Authorization': 'Bearer my-api-key', }, query: { 'queryParam': 'queryValue', }, } }; } } }); ``` ## Channel Steps Interface All channels follow the same shared interface: ### stepId * **Type**: `string` * **Required**: Yes * **Description**: This is the unique identifier for the step in the workflow context. It is used to reference and display the step in the dashboard interface. ### resolver * **Type**: `Promise` * **Required**: Yes * **Description**: This is an async function that returns the content of the step which called `Outputs`. Each channel has its own output schema. ### options * **Type**: `StepOptions` * **Description**: Additional step configuration. ## Options Object This is an optional configuration object that defines: [Controls Schema](/framework/controls), [Provider Overrides](#provider-overrides), skip and other configurations. ### skip * **Type**: `(controls: InferProperties) => boolean | Promise` * **Description**: A function that returns a boolean value to skip the step. This is helpful when you want to use previous step results or other custom logic to skip the step from executing. ### controlSchema * **Type**: `JsonSchema | ZodSchema` * **Description**: This defined the UI Controls exposed in the dashboard for the step. They can be nested and of any JSON Schema supported structure. ### providers * **Type**: `ProvidersOverride` * **Description**: This object used to access and override the underlying deliver providers SDKs. This is useful when you want to customize the content of the notification with properties that are unique to the provider. ### disableOutputSanitization * **Type**: `boolean` * **Default**: `false` * **Description**: A flag to disable output sanitization for the step. This is useful when you want to return unescaped HTML content to the provider or the `` component. ## Providers Overrides Object This object used to access and override the underlying deliver providers SDKs. This is useful when you want to customize the content of the notification with properties that are unique to the provider. An example of this is the `slack` provider, which allows you to customize the content of the notification with Slack `blocks` to create a rich notification experience. ```typescript type ProvidersOverride = { [key: ProviderEnum]: ProviderCallback; }; type ProviderCallback = ( params: ProviderOverridesParams ) => ProviderOverrideOutput | Promise; type ProviderOverridesParams = { controls: StepControls; output: StepOutput; }; interface ProviderOverrideOutput { // A map of the properties used by the Provider. // These properties are strongly typed and validated // against the underlying provider SDK. [key in KnownProviderKey]: KnownProviderValue; // The passthrough object is used to pass through // the original request to the provider. // These properties are not validated. _passthrough?: { body: Record; headers: Record; query: Record; }; } ``` The `_passthrough` object and the known provider values are deeply merged prior to sending the request to the provider, with the `_passthrough` object taking precedence. file: ./content/docs/framework/typescript/steps/push.mdx # Push Learn how to use the push step to send notifications to mobile and web apps The `push` step allows you to send notifications to mobile and web applications through various push notification providers. ## Example Usage ```tsx await step.push('new-message', async () => { return { title: 'New Message', body: 'You have received a new message from John', data: { messageId: '123', senderId: '456', }, }; }); ``` ## Push Step Output | Property | Type | Required | Description | | -------- | ------ | -------- | ------------------------------------------------------------ | | title | string | Yes | The notification title that appears in the push notification | | body | string | Yes | The main content of the push notification | | data | object | No | Additional data to be sent with the notification | | image | string | No | URL of an image to display in the notification | | icon | string | No | URL of an icon to display in the notification | ## Push Step Result The `push` step does not return any result object. ## Provider Configuration To send push notifications, you need to configure the appropriate push provider credentials. See our [Push Provider Documentation](/platform/integrations/push) for detailed setup instructions. file: ./content/docs/framework/typescript/steps/sms.mdx # SMS Learn how to use the SMS step to send text messages to users The `sms` step allows you to send text messages to your users' mobile devices through various SMS providers. ## Example Usage ```tsx await step.sms('verification', async () => { return { body: 'Your verification code is: 123456', }; }); ``` ## SMS Step Output | Property | Type | Required | Description | | -------- | ------ | -------- | ----------------------------------- | | body | string | Yes | The text message content to be sent | ## SMS Step Result The `sms` step does not return any result object. file: ./content/docs/platform/inbox/react/headless.mdx # Headless Learn how to use the Novu Inbox API to build your own inbox The headless version of Novu’s notification library package provides users with a lightweight solution for integrating notification functionality into their web applications. With just the essential API methods, users can easily incorporate our notification system into any framework or vanilla JavaScript project, without being constrained by our default UI or dependencies. The SDK includes real-time notifications through a WebSocket connection and can be safely used across web browsers. ### Install the SDK ```bash npm i @novu/js ``` ```bash pnpm add @novu/js ``` ```bash yarn add @novu/js ``` ```bash bun add @novu/js ``` ### Initialize the SDK ```typescript import { Novu } from "@novu/js"; export const novu = new Novu({ applicationIdentifier: 'YOUR_APPLICATION_IDENTIFIER', subscriberId: 'YOUR_SUBSCRIBER_ID', }); ``` ### Fetch notifications ```tsx const response = await novu.notifications.list({ limit: 30, }); const notifications = response.data.notifications; ``` Display notifications in your UI. ## Realtime Notifications Events are emitted when notifications are received, and when the unread notificatons count changes. `novu.on()` is used to listen to these events. ```tsx novu.on("notifications.notification_received", (data) => { console.log("new notification =>", data); }); novu.on("notifications.unread_count_changed", (data) => { console.log("new unread notifications count =>", data); }); ``` For the full list of methods available, see the [API Reference](/platform/sdks/javascript). file: ./content/docs/platform/inbox/react/hooks.mdx # Hooks Learn how to build a custom notifications UI using React hooks powered by Novu import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; The `@novu/react` package offers an interface that enables you to build a custom notifications UI using React hooks that are powered by real-time data from the Novu services. These hooks are designed for use in both mobile and web applications, offering a flexible approach to building a custom notifications UI tailored to your application's requirements. ## Getting Started Follow these steps to get started with building your custom inbox UI: ```bash npm install @novu/react ``` To implement the [NovuProvider](/platform/sdks/react/hooks/novu-provider) component, you need to place it in your application's code at the tree level where you want the hooks to be accessible. ```tsx import { NovuProvider } from '@novu/react'; function App() { return ( {/* Your app components */} ); } ``` You can find the `applicationIdentifier` in the Novu Dashboard under the [API keys page](https://dashboard.novu.co/api-keys). The `subscriberId` is the unique identifier of the user in your application, learn more about subscribers [here](/platform/concepts/subscribers). For example, you can create a custom popover UI with a bell icon that shows the unread notifications count and a list of notifications. ```tsx const YourCustomInbox = () => { return ( ); }; ``` ### Bell Button with Unread Count The `BellButton` component fetches the unread notifications count and renders the count value in the indicator: ```tsx import { useCounts } from '@novu/react'; function BellButton() { const { counts } = useCounts({ filters: [{ read: false }] }); const unreadCount = counts?.[0].count ?? 0; return ( ); } ``` ### Notifications List The `NotificationsList` component retrieves and displays the notifications list with infinite scrolling functionality: ```tsx import { useNotifications } from '@novu/react'; function NotificationsList() { const { notifications, error, isLoading, isFetching, refetch, fetchMore, hasMore, } = useNotifications(); if (isLoading) return
Loading...
; if (error) return
Error: {error.message}
; return (
{notifications.map((notification) => ( ))} {hasMore && ( )}
); } ``` #### Notification Item The `NotificationItem` component renders each notification item. When any action is performed on the `notification` instance (e.g., "read" button is clicked), the SDK will optimistically update the notification, which will trigger a rerender of the `useNotifications` hook. ```tsx const NotificationItem = ({ notification }) => { return (
{notification.isRead && }

{notification.subject}

{notification.body}

); }; ```
Learn more about the Hook interfaces in the [React SDK](/platform/sdks/react/hooks/novu-provider) documentation. file: ./content/docs/platform/inbox/react/localization.mdx # Localization Learn how to localize the pre built Inbox component ## Localization Prop The `localization` prop can be used to change the copywriting of the Inbox to a different language for your users or change the wording to suit your product. See the list of [available keys](https://github.com/novuhq/novu/blob/next/packages/js/src/ui/config/defaultLocalization.ts#L1), or use the fully typed TS auto complete to find the key you need. ```tsx import { Inbox } from '@novu/react'; function Novu() { return ( `${notificationCount > 99 ? '99+' : notificationCount} new ${ notificationCount === 1 ? 'notification' : 'notifications' }`, // Individual notification actions 'notification.actions.read.tooltip': 'Mark as read', 'notification.actions.unread.tooltip': 'Mark as unread', 'notification.actions.archive.tooltip': 'Archive', 'notification.actions.unarchive.tooltip': 'Unarchive', // Preferences section 'preferences.title': 'Preferences', 'preferences.emptyNotice': 'No notification specific preferences yet.', 'preferences.global': 'Global Preferences', 'preferences.workflow.disabled.notice': 'Contact admin to enable subscription management for this critical notification.', 'preferences.workflow.disabled.tooltip': 'Contact admin to edit', // Set locale locale: 'en-US', // Dynamic localization for workflow names dynamic: { // use the workflowId as a key to localize the workflow name 'comment-on-post': 'Post comments', } }} /> ); } ``` file: ./content/docs/platform/inbox/react/migration-guide.mdx # Migration Guide 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 version. [@novu/react](/platform/sdks/react) Inbox react component and [@novu/js](/platform/inbox/headless/get-started) headless package is compatible with [@novu/framework](/platform/workflow/overview) based workflows only. With old UI based workflows, our old [@novu/notification-center](https://v0.x-docs.novu.co/notification-center/client/react/get-started) component should be used. 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 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; + isArchived: boolean; + readAt?: string | null; + archivedAt?: string | null; + avatar?: string; + tags?: string[]; createdAt: 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 migrating to `@novu/react`, install the package via npm: ```bash npm install @novu/react ``` ## Basic Usage * Old Implementation ```tsx import { NovuProvider, PopoverNotificationCenter, NotificationBell, } from '@novu/notification-center'; function Novu() { return ( {({ unseenCount }) => } ); } export default Novu; ``` * New 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. Utilize the `Inbox` and `Notifications` components to achieve this functionality. * Old Implementation ```tsx import { NovuProvider, NotificationCenter } from '@novu/notification-center'; function Novu() { return ( ); } export default Novu; ``` * New 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. * Old Implementation ```tsx import { NovuProvider, PopoverNotificationCenter } from '@novu/notification-center'; import CustomBell from './CustomBell'; // Your custom bell icon component function Novu() { return ( {({ unseenCount }) => ( )} ); } export default Novu; ``` * New 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. * Old Implementation ```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; ``` * New 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. * Old Implementation ```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; ``` * New 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 Customize the avatar displayed alongside notifications by specifying an avatar `URL` at the workflow definition level using `@novu/framework`. ```ts import { workflow } from '@novu/framework'; workflow('welcome-notification', async (step) => { await step.inApp('inbox', async () => ({ subject: 'Welcome to Our Service!', body: 'We are thrilled to have you onboard.', avatar: 'https://example.com/path-to-your-avatar-image.png', })); }); ``` ## Popover Positioning For advanced positioning and styling of the notifications popover, integrate third-party popover libraries such as Radix UI. * Old Implementation ```tsx import { PopoverNotificationCenter, NotificationBell, NovuProvider, } from '@novu/notification-center'; function Novu() { return ( {({ unseenCount }) => } ); } export default Novu; ``` * New 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. * Old Implementation ```tsx import { NovuProvider, PopoverNotificationCenter, NotificationBell, } from '@novu/notification-center'; function Novu() { return ( { return ( { e.preventDefault(); handleNotificationClick(); }}> {notification.content} ); }}> {({ unseenCount }) => { return ; }} ); } export default Novu; ``` * New 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). * Old Implementation ```tsx import { NovuProvider, PopoverNotificationCenter, NotificationBell, } from '@novu/notification-center'; function Novu() { return ( {({ unseenCount }) => } ); } export default Novu; ``` * New 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 * Old Implementation 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; ``` * New Implementation with `@novu/react` 1. Define multiple workflows with relevant tags. ```ts import { workflow } from '@novu/framework'; workflow( 'security-alerts', async (step) => { await step.inApp('inbox', async () => ({ subject: 'Security Alert', body: 'A new login attempt was detected.', })); }, { tags: ['security'] } ); workflow( 'promotional-offers', async (step) => { await step.inApp('inbox', async () => ({ subject: 'Exclusive Offer!', body: 'Get 50% off on your next purchase.', })); }, { tags: ['promotions'] } ); ``` 2. Assign relevant tags to each workflow to categorize notifications appropriately. ```tsx import { Inbox } from '@novu/react'; const tabs = [ { label: 'All Notifications', value: [] }, { label: 'Security', value: ['security'] }, { label: 'Promotions', value: ['promotions'] }, ]; function Novu() { return ( ); } export default Novu; ``` ## Localization Customize the language and text content of the notification components using the localization prop. ```tsx import { Inbox } from '@novu/react'; function Novu() { return ( ); } export default Novu; ``` ## HMAC Encryption The process remains the same as before. See the detailed guide [here](/platform/inbox/react/production#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; ``` file: ./content/docs/platform/inbox/react/multiple-tabs.mdx # Tabs undefined Novu enables the creation of a multi-tab experience for the Inbox, allowing you to filter and display notification lists within distinct UI tabs. ## Defining tabs Define the `tabs` prop to display multiple tabs in the Inbox component. Each tab object should include a `label` and `filter` properties. The `label` property represents the text displayed on the specific tab. The `filter` property is an object that serves as the "filter criteria" for the notifications displayed in the list. The filter should include the `tags` which is an array of strings. The notifications are filtered and matched by comparing the [workflow](/platform/workflow/overview#tags) `tags` field with the tab `filter.tags` array. Each notification is a part of a specific [workflow](/platform/concepts/workflows). You can combine tags from different workflows to create a customized tab experience. The example below demonstrates how to define multiple tabs in the Inbox component. ```tsx import { Inbox } from '@novu/react'; const tabs = [ { label: 'All Notifications', filter: { tags: [] }, }, { label: 'Newsletter', filter: { tags: ['newsletter'] }, }, { label: 'React', filter: { tags: ['react'] }, }, ]; function InboxNovu() { return ( ); } ``` Here tab `filter.tags` are taken from workflow tags. Tags can be configured in the [workflow](/platform/workflow/tags) editor. file: ./content/docs/platform/inbox/react/preferences.mdx # Preferences Learn how to manage and customize subscriber notification preferences in your React application. By default, Novu will show the subscriber preferences cog icon on the Inbox component. Users can enable/disable any active channel in the workflow using subscriber preferences or they can update the preference globally for all workflows under the `Global Preferences`. Preferences ### Hide global preferences Global preferences can be hidden by using following css style in root css file. ```css .nv-workflowContainer:first-child { display: none; } ``` file: ./content/docs/platform/inbox/react/production.mdx # Going to production Learn how to prepare your React notification inbox for production deployment including HMAC encryption and security best practices. ## HMAC Encryption When Novu's user adds the Inbox to their application they are required to pass a `subscriberId` which identifies the user's end-customer, and the application Identifier which is acted as a public key to communicate with the notification feed API. A malicious actor can access the user feed by accessing the API and passing another `subscriberId` using the public application identifier. HMAC encryption will make sure that a `subscriberId` is encrypted using the secret API key, and those will prevent malicious actors from impersonating users. ### Enabling HMAC Encryption In order to enable Hash-Based Message Authentication Codes, you need to visit the admin panel In-App settings page and enable HMAC encryption for your environment. 1. Next step would be to generate an HMAC encrypted subscriberId on your backend: ```tsx import { createHmac } from 'crypto'; const hmacHash = createHmac('sha256', process.env.NOVU_SECRET_KEY) .update(subscriberId) .digest('hex'); ``` 2. Then pass the created HMAC to your client side application forward it to the component: ```tsx import { Inbox } from '@novu/react'; ; ``` If HMAC encryption is active in In-App provider settings and `subscriberHash` along with `subscriberId` is not provided, then Inbox will not load file: ./content/docs/platform/inbox/react/styling.mdx # Styling Learn how to style the pre built Inbox component import { TypeTable } from 'fumadocs-ui/components/type-table'; ## Customization Hierarchy The Inbox component is built to allow for multiple layers of styling, which allows the specificity required to style the Inbox to meet the requirements of your use case. Depending on the level of customization you need, you can choose to style the inbox using one of the following approaches: * [Appearance Prop](#appearance-prop) * [Variables](#variables) - Global primitives such as buttons, popovers, drop-downs and etc... * [Elements](#elements) - Style individual elements * [Custom notification rendering](#render-notification-component) - Render a custom notification item with complete control * [Custom Composition](/platform/inbox/react/components/overview#composition) - Compose our components for custom layouts You can see Styling in action on the [Inbox Playground](https://inbox.novu.co) with common themes like Notion, Reddit and more! ## Appearance Prop The `appearance` prop can be used to customise the Inbox. It has three main keys: `baseTheme`, `variables` and `elements`. * **Variables**: Global styling variables that apply to multiple elements within the inbox. * **Elements**: Elements are the individual UI components that make up the Inbox. * **Base theme**: This is the base theme applied to the ``. It has the same keys as `appearance`. Used for applying base themes like `dark`. ### Variables Variables are used to define global styling properties that can be reused throughout the inbox. You might want to use variables to the styling of multiple components at once, for example, if you want to change the border radius of all the components at once, you can do so by updating the `colorPrimary` variable, which will modify the CTA buttons, unseen counter and etc... Appearance Variables #### Styling Variables You can override the default elements by passing your own styles or CSS classes to the `elements` object. ```tsx import { Inbox } from '@novu/react'; const appearance = { variables: { colorBackground: 'yellow', }, }; ; ``` ### Elements The `elements` object allows you to define styles for these components. Each key corresponds to a component, and the value is an object containing `style properties` or you can also pass your `css classes`. Here's a list of available elements that can be styled using the `elements` object in your appearance configuration: #### Finding element selectors You can inspect the elements you want to customize using the browser's dev tools, each element has a unique selector that you can use to style starting with `nv-`. Strip the `nv-` prefix when and add it to the `elements` object. For example, to style the `nv-notificationPrimaryAction__button` element, you can add the following to the `elements` object: ```tsx const appearance = { elements: { notificationPrimaryAction__button: { backgroundColor: 'red', }, }, }; ``` Finding Element Selectors Any selector that appears before the 🔔 emoji, can be targeted via the elements property in the appearance prop (stripping the `nv-` prefix). You can also use TS autocomplete to find the available elements. ### Customizing icons The `icons` property in the `appearance` prop lets you customize any icon used in the Inbox component. This is useful when you want to: * Match the bell icon with your host app's navbar icon set. – Use your own icon library, such as [react-icons](https://react-icons.github.io/react-icons/) or [Material Icons](https://mui.com/material-ui/material-icons/). * Modify all icons to fit your application's visual style. For each icon you want to customize, provide a function that returns your custom icon as a React component. ```tsx import { Inbox } from '@novu/react'; import { RiSettings3Fill, RiArrowDownLine, RiNotification3Fill} from 'react-icons/ri'; const appearance = { icons: { cogs: () => , arrowDown: () => , bell: () => , }, }; export function Novu() { return ( ); } ``` #### List of customizable icon keys Use these keys in the `appearance.icons` property to customize specific icons in the Inbox component: | Icon Key | Description | | -------------------- | ----------------------------------------------------- | | `arrowDown` | Down arrow used in drop-downs and expandable sections | | `arrowDropDown` | Drop-down arrow in menus and selectors | | `arrowLeft` | Left arrow used in pagination and navigation | | `arrowRight` | Right arrow used in pagination and navigation | | `bell` | Notification bell icon in the header | | `chat` | Chat channel icon in notification preferences | | `check` | Checkmark icon used for selected items | | `clock` | Date/time display for notifications | | `cogs` | Settings/preferences icon in the header | | `dots` | More options menu (three dots) in notification items | | `email` | Email channel icon in notification preferences | | `inApp` | In-app channel icon in notification preferences | | `markAsArchived` | Icon for archiving notifications | | `markAsArchivedRead` | Icon for marking notifications as archived and read | | `markAsRead` | Icon for marking notifications as read | | `markAsUnread` | Icon for marking notifications as unread | | `markAsUnarchived` | Icon for unarchiving notifications | | `push` | Push notification channel icon in preferences | | `sms` | SMS channel icon in notification preferences | | `trash` | Delete/remove icon for notifications | | `unread` | Indicator for unread notifications | | `unsnooze` | Icon for unsnoozed notifications | You can inspect the Inbox component using your browser’s developer tools to discover icon keys. Icon elements have class names that start with `nv-` and include a 🖼️ emoji for easier identification. The part following `nv-` is the icon key. For example, an element with the class `nv-cogs` has the icon key `cogs`. ### Dark theme No need to implement a custom dark theme. Just import our premade `dark` theme and use it via the `baseTheme` option in `appearance`. ```tsx import { Inbox } from '@novu/react'; import { dark } from '@novu/react/themes'; import { useTheme } from 'next-themes'; export function Novu() { const { resolvedTheme } = useTheme(); return ( ); } ``` ### Bring your own CSS You can override the default elements by passing your own styles or CSS classes to the `elements` object. #### Using Tailwind CSS You can also use Tailwind CSS classes to style the Inbox components. You can pass the classes directly to the `elements` object. ```tsx import { Inbox } from '@novu/react'; const appearance = { elements: { bellIcon: 'p-4 bg-white rounded-full', notification: 'bg-white rounded-lg shadow-sm hover:shadow-md hover:bg-gray-50', }, }; export function Novu() { return ( ); } ``` #### Using CSS Modules You can also use `CSS Modules` to style the Inbox components. Here's how you can do it: * Create a CSS module file `(e.g. styles.module.css)` with the styles you want to apply to the Inbox components. ```css .bellIcon { padding: 1rem; background-color: white; border-radius: 50%; } .bellIcon:hover { background-color: #f9fafb; } .notification { background-color: white; border-radius: 0.5rem; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); } .notification:hover { background-color: #f9fafb; } ``` * Import the CSS module file and pass the classes to the elements object. ```tsx import { Inbox } from '@novu/react'; import styles from './styles.module.css'; const appearance = { elements: { bellIcon: styles.bellIcon, notification: styles.notification, }, }; export function Novu() { return ( ); } ``` #### Using Styles Object You can also use a styles object to style the Inbox components. You can pass the styles directly to the `elements` object. ```tsx import { Inbox } from '@novu/react'; const appearance = { elements: { bellIcon: { padding: '1rem', backgroundColor: 'white', borderRadius: '50%', }, notification: { backgroundColor: 'white', borderRadius: '0.5rem', boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)', }, }, }; export function Novu() { return ( ); } ``` #### Render subject and body with bold text By default, the Inbox notification text for the subject and body is rendered in a normal font weight. To highlight important words or phrases within your notification messages, you can wrap the desired subject or body text in double asterisks (\*\*). Here's an example of how you can do this using the Novu Framework: ```js await step.inApp('inbox', async () => { return { subject: '**A new member joined the team!**', body: '**John Doe** joined the team! Say hello and help them feel at home', }; }); ``` Note: When rendering the custom notification component, you will need to parse the text and apply the bold styling accordingly. Currently, the Inbox component only support bold text. Other text formatting options will be available in the future. To learn, refer to the [Novu Framework in-app step documentation](/framework/typescript/steps/inApp). ## Render notification component You can render your own custom notification item component by passing a `renderNotification` prop to the `Inbox` or `Notifications` component. This lets you style and render more complex notification items. ```tsx import { Inbox } from '@novu/react'; import { CustomNotification } from './CustomNotification'; { return ; }} />; ``` ## The Inbox styles placement The {``} component automatically injects its styles into the `` tag of the HTML document when rendered in the standard DOM. However, when rendered inside a shadow DOM, it detects the shadow context and injects styles into the shadow root instead. This ensures proper style encapsulation and prevents leakage or conflicts with global styles without any additional configuration. file: ./content/docs/platform/inbox/react/tabs.mdx # Tabs Learn what tabs are and how to filter multiple tabs in the Novu Inbox component. Tabs in the Novu Inbox enable you to organize and display different sets of notifications within distinct UI tabs. This creates a structured, filterable interface for end users to easily navigate their in-app notification feed. Each tab is defined using the `tabs` prop of the {``} component. Every tab object include the following properties: * **`label`**: A string representing the tab's visible title. * **`filter`**: An object defining the filter criteria for notifications displayed in that tab. The `filter` property supports two filtering options: * **Tags**: Filter based on the `tags` assigned to a workflow in the workflow editor. * **Data attributes**: Filter based on values in the notification payload (`data`). ![Filtering tabs by tags and data attributes](/images/inbox/filter-tabs-by-tags-and-data-attributes.png) ## Filter tabs by tags Each notification is associated with a workflow, and workflows can be tagged in the workflow editor. These tags can then be used to group notifications from one or more workflows under a single tab by referencing shared tag names. To use tags effectively in tab filters, assign clear and descriptive tags to your workflows in the workflow editor. For example, use tags like `promotions` to group marketing messages or `security` to collect alerts related to user safety. ```tsx import { Inbox } from '@novu/react'; export default function InboxWithFilters() { const tabs = [ { label: 'All Notifications', filter: { tags: [] }, }, { label: 'Promotions', filter: { tags: ['promotions'] }, }, { label: 'Security Alerts', filter: { tags: ['security', 'alert'] } }, ]; return ( ); } ``` ## Filter tabs by data attributes Each notification can include a `data` payload that contains custom metadata such as priority, category, or activity type. This method is most useful when you want to segment notifications by metadata embedded in the payload rather than by workflow configuration. You can use these [data attributes](/platform/inbox/react/components/inbox#data-object) to group notifications under a single tab by filtering based on specific values in the payload. To use data attributes effectively in tab filters, make sure your notification payloads are consistently structured and contain clearly defined fields. For example, you might include `priority: "high"` to identify urgent alerts or `type: "login"` to track user sign-ins. The data `object` supports only scalar values, such as strings, numbers, booleans, or null. String values are limited to 256 characters. ```tsx import { Inbox } from '@novu/react'; export default function InboxWithFilters() { const tabs = [ { label: 'High Priority', filter: { data: { priority: 'high' }, }, }, { label: 'User Activity', filter: { data: { type: 'login' }, }, }, ]; return ( ); } ``` ## Combining tags and data You can also combine both tags and data in a single filter to create highly specific tabs tailored to your use case. ```tsx import { Inbox } from '@novu/react'; export default function InboxWithFilters() { const tabs = [ { label: 'High Priority', filter: { tags: ['alert'], // [\!code highlight] data: { priority: 'high' } // [\!code highlight] }, }, ]; return ( ); } ``` file: ./content/docs/platform/integrations/email/adding-email.mdx # Adding Email Channel Learn how to add the Email channel to your application import { Card } from 'fumadocs-ui/components/card'; import { CodeIcon } from '@/components/icons/overview'; The Email channel enables you to send email notifications to users for events like password resets, onboarding, or system alerts. By default, the Email channel is enabled and configured with Novu's default provider. If it is disabled, notifications sent to this channel will not be processed. 1. Go to the Novu Dashboard and click **"Integrations"** on the left sidebar 2. Click **"Add a provider"** 3. Locate the **Email** channel and select the provider you want to use and click **"Next"** 4. Select for which environment you want to add the Provider 5. (Optional) Add Conditions to activate the provider only under certain conditions, **useful for tenant-based providers** 6. Click **"Create"** 7. Add your Email provider credentials: * **From**: Displayed as the sender of the email (ensure compliance with local regulations) * **Sender Name**: The name that will be used to send the email * Provider-specific credentials such as API key / Auth token, Account SID, username, or password 8. Save the configuration by clicking **"Update"** 1. Go to the Novu Dashboard and click **"Workflows"** on the left sidebar. 2. Click the **"Add a Workflow"** button. 3. Add a step and select **"Email"** as the channel. 4. Configure the email content, such as subject, message body, and any dynamic variables. Novu's server-side SDKs make integrating Novu's REST APIs straightforward, letting you focus on implementing workflows without dealing with repetitive code. } href="/platform/sdks/server" /> Ensure your configuration is working by sending a test notification. 1. Go to the Novu Dashboard, navigate to the "Workflows" section, and locate your configured workflow. 2. Click **"Test Workflow"** and provide sample data (e.g., user ID, email address). 3. Verify the email delivery in the Novu Logs or your email provider dashboard. file: ./content/docs/platform/integrations/email/index.mdx # E-mail Learn how to configure the Email channel import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; 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. ## Key Features * **Multi-Provider Support**: Integrate any major provider like SendGrid, SES, or Mailgun * **Failover Mechanisms**: Automatically retry with a backup provider to ensure reliability * **Customizable Templates**: Leverage templates with dynamic placeholders to personalize messages * **Delivery Insights (Coming Soon)**: Track delivery status, open rates, and more in the Novu dashboard ## Common Use Cases * **Transactional Emails**: Password resets, account verification, purchase confirmations * **System Alerts**: Security notifications, system updates * **Engagement Emails**: Onboarding, reminders, promotional updates Novu can be used to deliver email messages to your subscribers using a unified delivery API. You can easily integrate your favorite email provider using the built-in integration store. ## Configuring email providers When creating an email provider integration you will be asked to provide additional fields alongside the provider-specific credentials: * **Sender name** - Will be displayed as the sender of the message * **From email address** - Emails sent using Novu will be sent using this address For some email providers including SendGrid you will have to authenticate the **From email address** to make sure you will send email messages using an authorized address. ## Sending Email Overrides The overrides field supports an email property. The email overrides field have properties like `to`, `from`, `senderName` etc ```javascript import { Novu } from '@novu/node'; const novu = new Novu(''); novu.trigger('', { to: { subscriberId: '', }, overrides: { email: { to: ['to@novu.co'], from: 'from@novu.co', senderName: 'Novu Team', text: 'text version of email using overrides', replyTo: 'no-reply@novu.co', cc: ['1@novu.co'], bcc: ['2@novu.co'], }, }, }); ``` It's very important to know that Novu merges the `to` field in the email overrides with the subscriber email. It DOES NOT REPLACE IT. ## Sending Email attachments You can easily send attachments with the Novu API by passing the attachments array when triggering an Email based workflow. Attachment file can either be in the `buffer` or `base64` format. There is total limit of 20 mb for all attachments included and email size. ```javascript import { Novu } from '@novu/node'; const novu = new Novu(''); novu.trigger('', { to: { subscriberId: '', }, payload: { attachments: [ { // buffer format file: fs.readFileSync(__dirname + '/data/novu.jpeg'), name: 'novu.jpeg', mime: 'image/jpeg', }, { // base64 format file: 'iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFUlEQVR42mNkYPhfz0AEYBxVSF+FAP5FDvcfRYWgAAAAAElFTkSuQmCC', name: 'blue.png', mime: 'image/png', } ], }, }); ``` Use [https://eu.api.novu.co/v1/events/trigger](https://eu.api.novu.co/v1/events/trigger) api endpoint for the EU region. ```bash curl -L -X POST 'https://api.novu.co/v1/events/trigger' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: ApiKey ' \ --data-raw '{ "name": "workflow_trigger_identifier", "to": [ { "subscriberId": "subscriber_id", "email": "email_address" } ], "payload": { "attachments": [ { "file": "iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII", "name": "transparent.png", "mime": "image/png" }, { "file": "iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFUlEQVR42mNkYPhfz0AEYBxVSF+FAP5FDvcfRYWgAAAAAElFTkSuQmCC", "name": "blue.png", "mime": "image/png" } ] } }' ``` ## Using different email integration In Novu integration store, multiple email channel type provider integrations can be active at the same time. But only one provider integration can be primary at a time. This primary integration will be used as a provider to deliver the email by default. If you want to use a different active provider integration then you can use the `integrationIdentifier` email overrides field. If there are 4 active email integrations with these identifiers: 1. sendgrid-abcdef 2. sendgrid-ghijkl 3. brevo-abcdef 4. mailersend-abcdef Here, if `sendgrid-abcdef` is primary integration and you want to use `brevo-abcdef` with this trigger then you can use `integrationIdentifier` email overrides field as below: ```javascript import { Novu } from '@novu/node'; const novu = new Novu(''); novu.trigger('', { to: { subscriberId: '', }, overrides: { email: { integrationIdentifier: "brevo-abcdef" }, }, }); ``` Integration identifier is similar to Provider identifier but it is different than Provider Id. It is unique for each integration. You can find the `integrationIdentifier` in the integration store page. Looking to integrate an email provider? Check out our [provider integrations](/platform/integrations/email). ``` ``` file: ./content/docs/platform/integrations/email/writing-email-template.mdx # Writing Email Template Learn how to write an email template in Novu. This document will guide you through the process of creating an email template using the Novu email editor, all supported blocks and how to use them ## Email Editor The Email Editor is a `WYSIWYG` editor that allows you to create and edit email templates. It has two fields: `Subject` and `Body`. Email editor * **Subject** - Title of the email. It supports variables and can be customized based on the subscriber properties and payload variables. * **Body** - Main content of the email. It is made of blocks. ## Email Editor Blocks Email editor body is made of blocks. A block can be added by clicking on the plus icon on the top left corner of the editor or by adding a forward slash `/`. In both the cases, a popover will appear with the list of supported blocks. Click on the desired block to add it to the editor. Menu option besides plus (+) icon can be used to **duplicate** or **delete** the block. Email blocks The Email Editor supports the following blocks: * **Text** - Regular text * **Heading 1** - Large heading (H1) * **Heading 2** - Medium heading (H2) * **Heading 3** - Small heading (H3) * **Bullet List** - Bullet list (bullet points like •) * **Numbered List** - Numbered list (numeric digits like 1,2,3) * **Image** - Full widh image with absolute url, image position can be customized * **Section** - Create a section to group content together * **Column** - Creates columns to group content together, useful for responsive design * **Divider** - Separates the content, adds a line to highlight the separation * **Spacer** - Spacer to add space between two blocks (available in sm, lg and xl sizes) * **Button** - Call to action button to link to a page or url (can be customised with text, url, color and size and background color) * **Hard Break** - Adds a line break * **Blockquote** - Adds a blockquote * **Repeat** - Can be used for interation on array of data * **Show** - Can be used to conditionally show content based on a condition, use eye icon to toggle the visibility of the content ### Repeat Block `Repeat` block is synonym with the javascript language `for` loop. It can be used to iterate over an array of data and render a block for each item in the array. Use + icon or / to add a `Repeat` block. Checkout the below video on how to use `Repeat` block