> For the complete documentation index, see [llms.txt](https://docs.digit.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.digit.org/complaints-management/design/architecture/services/novu-bridge-service.md).

# Novu Bridge Service

## Overview

The central notification orchestrator for the DIGIT platform. It consumes domain events from Kafka, checks user consent, resolves the right template and provider from config-service, and triggers Novu (which delivers via Twilio to WhatsApp).

### Key Functionalities

* **Module-agnostic** — any module publishing domain events to Kafka can trigger notifications
* **Consent-first** — checks user preferences before sending; skips if consent not granted
* **Multi-locale** — resolves templates based on the user's preferred language
* **Provider strategy** — Twilio-specific logic via strategy pattern, extensible for new providers
* **Retry + DLQ** — failed events are retried, then moved to the dead-letter queue
* **Dispatch audit log** — every event logged with status, errors, and provider response
* **Diagnostic endpoints** — `_validate`, `_dry-run`, `_test-trigger` for debugging

## Pre-requisites

Before you proceed with the configuration, make sure the following prerequisites are met:

* Java 17
* PostgreSQL
* Kafka / Redpanda
* Novu self-hosted (API at port 3000)
* Config Service running
* User Preferences Service running

## Process Pipeline

```
 1. Validate event       (eventId, eventName, tenantId, workflow.toState)
 2. Derive context       (audience, recipient mobile, userId, locale)
 3. Resolve locale       (query user-preferences for preferred language)
 4. Check consent        (query user-preferences for WhatsApp consent)
 5. Resolve template     (query config-service _resolve for TemplateBinding)
 6. Resolve provider     (query config-service _search for ProviderDetail)
 7. Validate vars        (ensure all requiredVars present in event data)
 8. Format phone         (add country prefix for WhatsApp)
 9. Build overrides      (contentSid + contentVariables from paramOrder)
10. Trigger Novu         (POST /v1/events/trigger)
11. Persist log          (upsert to nb_dispatch_log)
```

## Data Model

**Table:** `nb_dispatch_log`

<table><thead><tr><th width="230.7421875">Column</th><th width="219.83984375">Type</th><th>Description</th></tr></thead><tbody><tr><td><code>id</code></td><td>UUID</td><td>Primary key</td></tr><tr><td><code>event_id</code></td><td>VARCHAR(64)</td><td>Domain event ID</td></tr><tr><td><code>reference_number</code></td><td>VARCHAR(256)</td><td>Business reference (e.g., complaint number)</td></tr><tr><td><code>module</code></td><td>VARCHAR(128)</td><td>Source module (e.g., <code>Complaints</code>)</td></tr><tr><td><code>event_name</code></td><td>VARCHAR(256)</td><td>Event name (e.g., <code>COMPLAINTS.WORKFLOW.APPLY</code>)</td></tr><tr><td><code>tenant_id</code></td><td>VARCHAR(256)</td><td>Tenant ID</td></tr><tr><td><code>channel</code></td><td>VARCHAR(64)</td><td>Notification channel (<code>WHATSAPP</code>)</td></tr><tr><td><code>recipient_value</code></td><td>VARCHAR(256)</td><td>Recipient's mobile number</td></tr><tr><td><code>template_key</code></td><td>VARCHAR(256)</td><td>Novu workflow ID used</td></tr><tr><td><code>status</code></td><td>VARCHAR(32)</td><td><code>SENT</code>, <code>FAILED</code>, <code>SKIPPED</code>, or <code>RECEIVED</code></td></tr><tr><td><code>attempt_count</code></td><td>INT</td><td>Number of delivery attempts</td></tr><tr><td><code>last_error_code</code></td><td>VARCHAR(128)</td><td>Error code if failed</td></tr><tr><td><code>last_error_message</code></td><td>TEXT</td><td>Error details</td></tr><tr><td><code>provider_response_jsonb</code></td><td>JSONB</td><td>Raw Novu API response</td></tr></tbody></table>

**Status values:**

<table><thead><tr><th width="273.12109375">Status</th><th>Meaning</th></tr></thead><tbody><tr><td><code>SENT</code></td><td>Novu trigger succeeded (201)</td></tr><tr><td><code>FAILED</code></td><td>Processing error</td></tr><tr><td><code>SKIPPED</code></td><td>User consent not granted</td></tr><tr><td><code>RECEIVED</code></td><td>Dry-run mode (no actual send)</td></tr></tbody></table>

### Domain Event Structure

Any module can trigger notifications by publishing this event to the configured Kafka topic:

```
{
  "eventId": "unique-uuid",
  "eventType": "DOMAIN_EVENT",
  "eventName": "COMPLAINTS.WORKFLOW.APPLY",
  "tenantId": "pg.citya",
  "module": "Complaints",
  "entityType": "COMPLAINT",
  "entityId": "PG-PGR-2026-03-25-043118",
  "workflow": { "toState": "PENDINGFORASSIGNMENT" },
  "stakeholders": [
    { "mobile": "9123456789", "userId": "user-uuid", "type": "CITIZEN" }
  ],
  "data": {
    "complaintNo": "PG-PGR-2026-03-25-043118",
    "status": "PENDINGFORASSIGNMENT",
    "serviceName": "Burning of garbage",
    "citizenName": "Jane Doe",
    "departmentName": "DEPT_3"
  }
}
```

**Required fields:** `eventId`, `eventName`, `tenantId`, `workflow.toState`, `stakeholders`

The `data` map must include all variables listed in the TemplateBinding's `requiredVars`.

### API Endpoints

**Base path:** `/novu-bridge/novu-adapter/v1/dispatch`

<table><thead><tr><th width="190.015625">Endpoint</th><th width="101.921875">Method</th><th>Description</th></tr></thead><tbody><tr><td><code>/_validate</code></td><td>POST</td><td>Full pipeline without sending — checks config, consent, vars</td></tr><tr><td><code>/_dry-run?send=true</code></td><td>POST</td><td>Full pipeline with optional actual send</td></tr><tr><td><code>/_test-trigger</code></td><td>POST</td><td>Direct Novu trigger, bypasses consent and config</td></tr></tbody></table>

#### Test Trigger Example

```
curl -X POST "http://<host>/novu-bridge/novu-adapter/v1/dispatch/_test-trigger" \
  -H "Content-Type: application/json" \
  -d '{
    "RequestInfo": {},
    "templateKey": "complaints-workflow-apply",
    "subscriberId": "pg.citya:user-uuid",
    "phone": "whatsapp:+916307817430",
    "payload": { "complaintNo": "TEST-001", "status": "PENDING" },
    "transactionId": "test-001",
    "contentSid": "HX350aa0b139780ea87f554276b1f68d6c",
    "contentVariables": { "1": "Service Name", "2": "TEST-001", "3": "25-Mar-2026" }
  }'
```

### Kafka Topics

<table><thead><tr><th width="345.8671875">Topic</th><th>Purpose</th></tr></thead><tbody><tr><td><code>complaints.domain.events</code> (configurable)</td><td>Input — domain events to process</td></tr><tr><td><code>novu-bridge.retry</code></td><td>Retry queue for failed events</td></tr><tr><td><code>novu-bridge.dlq</code></td><td>Dead-letter queue after all retries exhausted</td></tr></tbody></table>

## Setup

#### Database

Create a database (e.g., `egov`). Flyway auto-creates the `nb_dispatch_log` table.

#### Bootstrap Novu

Before starting Novu-bridge, bootstrap Novu with the Twilio integration and workflows.

**Step 1:** Ensure Novu self-hosted is running (API at `http://localhost:3000`).

**Step 2:** Get your Novu API key. Register via the API if you haven't already:

```
# Register (first time only)
curl -s -X POST http://localhost:3000/v1/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "Admin",
    "lastName": "User",
    "email": "admin@example.com",
    "password": "YourPassword@123",
    "organizationName": "your-org"
  }' | jq .token

# Get API key using the token from above
curl -s http://localhost:3000/v1/environments \
  -H "Authorization: Bearer <token>" | jq '.[].apiKeys'
```

**Step 3:** Edit `.env.novu` with your credentials:

```
cd novu-bridge/config
cp .env.novu .env.novu.local
vi .env.novu.local
```

Fill in these required values:

```
NOVU_BASE_URL=http://localhost:3000
NOVU_API_KEY=<your-novu-api-key>

TWILIO_ACCOUNT_SID=<your-twilio-account-sid>
TWILIO_AUTH_TOKEN=<your-twilio-auth-token>
TWILIO_WHATSAPP_FROM=whatsapp:+<your-sender-number>
```

Optional — Customise workflows to create:

```
NOVU_EVENT_WORKFLOWS=COMPLAINTS.WORKFLOW.APPLY,COMPLAINTS.WORKFLOW.ASSIGN,COMPLAINTS.WORKFLOW.RESOLVE,COMPLAINTS.WORKFLOW.REJECT,COMPLAINTS.WORKFLOW.REASSIGN,COMPLAINTS.WORKFLOW.REOPEN,COMPLAINTS.WORKFLOW.RATE
```

**Step 4:** Run the bootstrap script:

```
# Requires: curl, jq
NOVU_ENV_FILE=.env.novu.local bash bootstrap-novu-whatsapp.sh
```

**What it creates:**

* Novu environment (`digit-dev`)
* Twilio SMS integration with WhatsApp credentials
* One workflow per event listed in `NOVU_EVENT_WORKFLOWS`

**Step 5:** Verify:

```
# Check workflows
curl -s "http://localhost:3000/v2/workflows?limit=100" \
  -H "Authorization: ApiKey $NOVU_API_KEY" | jq '.workflows[].workflowId'

# Check Twilio integration
curl -s "http://localhost:3000/v1/integrations" \
  -H "Authorization: ApiKey $NOVU_API_KEY" | jq '.[] | {identifier, channel, active}'
```

**Updating Twilio credentials later:**

```
# Get integration ID
curl -s "http://localhost:3000/v1/integrations" \
  -H "Authorization: ApiKey $NOVU_API_KEY" | jq '.[] | select(.identifier=="twilio-whatsapp") | ._id'

# Update credentials
curl -X PUT "http://localhost:3000/v1/integrations/<integration-id>" \
  -H "Authorization: ApiKey $NOVU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "credentials": {
      "accountSid": "<new-sid>",
      "token": "<new-token>",
      "from": "whatsapp:+<new-number>"
    }
  }'
```

#### Kafka Topics

Create topics if they don't exist:

```
rpk topic create complaints.domain.events novu-bridge.retry novu-bridge.dlq --brokers <broker>
```

#### Running Locally

```
mvn clean package -DskipTests

NOVU_API_KEY=<your-key> java -jar target/novu-bridge-*.jar
```

#### Configuration

<table><thead><tr><th width="308.328125">Property</th><th width="268.09765625">Default</th><th>Description</th></tr></thead><tbody><tr><td><code>server.servlet.context-path</code></td><td><code>/novu-bridge</code></td><td>API context path</td></tr><tr><td><code>spring.kafka.bootstrap-servers</code></td><td><code>localhost:9092</code></td><td>Kafka broker</td></tr><tr><td><code>novu.bridge.kafka.input.topic</code></td><td><code>complaints.domain.events</code></td><td>Input topic</td></tr><tr><td><code>novu.bridge.kafka.retry.topic</code></td><td><code>novu-bridge.retry</code></td><td>Retry topic</td></tr><tr><td><code>novu.bridge.kafka.dlq.topic</code></td><td><code>novu-bridge.dlq</code></td><td>DLQ topic</td></tr><tr><td><code>novu.bridge.max.retries</code></td><td><code>3</code></td><td>Max retries before DLQ</td></tr><tr><td><code>novu.bridge.channel</code></td><td><code>WHATSAPP</code></td><td>Default channel</td></tr><tr><td><code>novu.bridge.default.locale</code></td><td><code>en_IN</code></td><td>Fallback locale</td></tr><tr><td><code>novu.bridge.config.host</code></td><td><code>http://digit-config-service.egov:8080</code></td><td>Config service URL</td></tr><tr><td><code>novu.bridge.preference.host</code></td><td><code>http://digit-user-preferences-service.egov:8080/user-preference</code></td><td>Preferences service URL</td></tr><tr><td><code>novu.bridge.preference.enabled</code></td><td><code>true</code></td><td>Enable/disable consent check</td></tr><tr><td><code>novu.base.url</code></td><td><code>http://novu-api.novu:3000</code></td><td>Novu API URL</td></tr><tr><td><code>novu.api.key</code></td><td>(env <code>NOVU_API_KEY</code>)</td><td>Novu API key</td></tr><tr><td><code>spring.datasource.url</code></td><td><code>jdbc:postgresql://localhost:5432/egov</code></td><td>Database URL</td></tr><tr><td><code>novu.bridge.dispatch.log.enabled</code></td><td><code>true</code></td><td>Enable dispatch logging</td></tr></tbody></table>

#### Helm Chart

Location: [`deploy-as-code/helm/charts/common-services/novu-bridge`](https://github.com/egovernments/Citizen-Complaint-Resolution-System/tree/master/devops/deploy-as-code/charts/common-services/novu-bridge)

### System Flow

```
                                    ┌──────────────────────┐
                            ┌──────▶│  User Preferences    │
                            │  (1)  │  - Check consent     │
                            │       │  - Get locale        │
                            │       └──────────────────────┘
┌────────────┐   Kafka   ┌──┴───────────┐                    ┌──────────────────────┐
│ Your       │──────────▶│  Novu Bridge  │───────────────────▶│  Config Service      │
│ Module     │  events   │               │  (2)               │  - Resolve template  │
└────────────┘           └───────┬───────┘                    │  - Get provider      │
                                 │                            └──────────────────────┘
                                 │ (3) Trigger
                                 ▼
                           ┌───────────┐   Twilio   ┌───────────┐
                           │   Novu    │───────────▶│ WhatsApp  │
                           │           │            │ User      │
                           └───────────┘            └───────────┘
```

1. **Your Module** publishes a domain event to Kafka
2. **Novu Bridge** checks consent (1), resolves template + provider (2), triggers Novu (3)
3. **Novu** delivers via Twilio to WhatsApp

### Resources

* [OpenAPI Spec](https://github.com/egovernments/Citizen-Complaint-Resolution-System/blob/master/docs/Novu_Adapter/novu-adapter.openapi.yaml)
* [Novu Bootstrap Script](https://github.com/egovernments/Citizen-Complaint-Resolution-System/blob/master/backend/novu-bridge/config/bootstrap-novu-whatsapp.sh)
* [Bootstrap Postman Collection](https://github.com/egovernments/Citizen-Complaint-Resolution-System/blob/master/backend/novu-bridge/config/Novu-Bootstrap.postman_collection.json)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.digit.org/complaints-management/design/architecture/services/novu-bridge-service.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
