diff --git a/.dockerignore b/.dockerignore
index e1b68d77f..ddaee79ef 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,6 +1,12 @@
+**/.git
+**/.pnpm-store
+**/build
+**/dist
+**/.build
+**/*.log
+**/coverage
**/Dockerfile
**/.dockerignore
-**/client/build
**/var
**/vendor
**/public/bundles
@@ -16,7 +22,6 @@
/doc
/report
/tmp
-**/dist
/uploader/client/index.html
/expose/client/index.html
/databox/client/index.html
diff --git a/.env b/.env
index 123a22abb..701c60a10 100644
--- a/.env
+++ b/.env
@@ -294,8 +294,10 @@ NOVU_VERSION=2.1.0
NOVU_API_HOST=api-novu.${PHRASEA_DOMAIN}${HTTPS_PORT_PREFIX}
NOVU_API_URL="${NOVU_API_URL:-https://${NOVU_API_HOST}}"
-NOVU_FRONT_URL=https://novu.${PHRASEA_DOMAIN}${HTTPS_PORT_PREFIX}
+NOVU_DASHBOARD_URL=https://novu.${PHRASEA_DOMAIN}${HTTPS_PORT_PREFIX}
NOVU_WS_URL=https://ws-novu.${PHRASEA_DOMAIN}${HTTPS_PORT_PREFIX}
+NOVU_STUDIO_URL=https://studio-novu.${PHRASEA_DOMAIN}${HTTPS_PORT_PREFIX}
+NOVU_BRIDGE_URL=https://bridge-novu.${PHRASEA_DOMAIN}${HTTPS_PORT_PREFIX}
NOVU_REDIS_HOST=redis
NOVU_REDIS_PORT=6379
diff --git a/dashboard/client/config-compiler.js b/dashboard/client/config-compiler.js
index 69c4da6b2..ee41b6b6d 100644
--- a/dashboard/client/config-compiler.js
+++ b/dashboard/client/config-compiler.js
@@ -35,7 +35,9 @@
'UPLOADER_CLIENT_URL',
'ZIPPY_URL',
'SOKETI_USAGE_URL',
- 'NOVU_FRONT_URL',
+ 'NOVU_DASHBOARD_URL',
+ 'NOVU_STUDIO_URL',
+ 'NOVU_BRIDGE_URL',
];
const e = {};
diff --git a/dashboard/client/src/Dashboard.tsx b/dashboard/client/src/Dashboard.tsx
index 508aeb4aa..8ed67619e 100644
--- a/dashboard/client/src/Dashboard.tsx
+++ b/dashboard/client/src/Dashboard.tsx
@@ -48,7 +48,9 @@ export default function Dashboard({}: Props) {
RABBITMQ_CONSOLE_URL,
TRAEFIK_CONSOLE_URL,
SOKETI_USAGE_URL,
- NOVU_FRONT_URL,
+ NOVU_DASHBOARD_URL,
+ NOVU_STUDIO_URL,
+ NOVU_BRIDGE_URL,
} = config.env;
const roles = user?.roles ?? [];
@@ -268,14 +270,36 @@ export default function Dashboard({}: Props) {
)}
- {NOVU_FRONT_URL && (
+ {NOVU_DASHBOARD_URL && (
- Novu
+ Novu Dashboard
+
+
+ )}
+ {NOVU_BRIDGE_URL && (
+
+
+ Novu Bridge
+
+
+ )}
+ {NOVU_STUDIO_URL && (
+
+
+ Novu Studio
)}
diff --git a/dashboard/client/src/config.ts b/dashboard/client/src/config.ts
index caa1339fa..cef3b5d84 100644
--- a/dashboard/client/src/config.ts
+++ b/dashboard/client/src/config.ts
@@ -27,7 +27,9 @@ declare global {
UPLOADER_CLIENT_URL: string;
ZIPPY_URL: string;
SOKETI_USAGE_URL: string;
- NOVU_FRONT_URL: string;
+ NOVU_DASHBOARD_URL: string;
+ NOVU_BRIDGE_URL: string;
+ NOVU_STUDIO_URL: string;
};
} & WindowConfig;
}
diff --git a/databox/client/package.json b/databox/client/package.json
index efd2efa28..3b3eea8bb 100644
--- a/databox/client/package.json
+++ b/databox/client/package.json
@@ -26,6 +26,7 @@
"@mui/lab": "5.0.0-alpha.173",
"@mui/material": "^5.16.7",
"@mui/x-tree-view": "^6.17.0",
+ "@novu/framework": "^2.5.2",
"@tanstack/react-query": "^5.59.0",
"@toast-ui/react-image-editor": "^3.15.2",
"ace-builds": "^1.36.2",
@@ -66,6 +67,8 @@
"tui-image-editor": "^3.15.3",
"uuid": "^9.0.1",
"web-vitals": "^2.1.4",
+ "zod": "^3.23.8",
+ "zod-to-json-schema": "^3.23.5",
"zustand": "^4.5.5"
},
"scripts": {
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index 7fe3a431f..402070974 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -145,7 +145,7 @@ services:
tty: true
hostname: local
ports:
- - 4000:4000
+ - "0.0.0.0:4000:4000"
environment:
- APP_ENV
- SSH_AUTH_SOCK=/ssh-auth-sock
@@ -250,6 +250,7 @@ services:
- api-notify.${PHRASEA_DOMAIN}:${PS_GATEWAY_IP}
- novu.${PHRASEA_DOMAIN}:${PS_GATEWAY_IP}
- api-novu.${PHRASEA_DOMAIN}:${PS_GATEWAY_IP}
+ - bridge-novu.${PHRASEA_DOMAIN}:${PS_GATEWAY_IP}
- phraseanet.${PHRASEA_DOMAIN}:${PS_GATEWAY_IP}
report-api:
@@ -282,6 +283,58 @@ services:
- ./cypress/cypress.config.js:/cypress/cypress.config.js
- /tmp/.X11-unix:/tmp/.X11-unix
+ novu-api:
+ environment:
+ - NODE_TLS_REJECT_UNAUTHORIZED=0
+
+ novu-worker:
+ environment:
+ - NODE_TLS_REJECT_UNAUTHORIZED=0
+
+ novu-bridge:
+ command:
+ - sh
+ - -c
+ - pnpm dev
+ environment:
+ - NODE_TLS_REJECT_UNAUTHORIZED=0
+ volumes:
+ - ./novu/bridge:/usr/src/app
+
+ novu-studio:
+ image: ${REGISTRY_NAMESPACE}novu-studio:${DOCKER_TAG}
+ build:
+ context: ./novu
+ dockerfile: studio/Dockerfile
+ profiles:
+ - novu
+ command:
+# - ash
+ - sh
+ - -c
+ - pnpm exec ./packages/novu/dist/src/index.js dev --headless --dashboard-url ${NOVU_DASHBOARD_URL} --port 443 --origin ${NOVU_BRIDGE_URL} --tunnel ${NOVU_BRIDGE_URL} --studio-host=0.0.0.0
+# - pnpm exec ./packages/novu/dist/src/index.js sync --api-url ${NOVU_API_URL} --bridge-url ${NOVU_BRIDGE_URL}/api/novu --secret-key ${NOVU_SECRET_KEY}
+ environment:
+ - NOVU_DASHBOARD_URL
+ - NOVU_BRIDGE_URL
+ - NOVU_SECRET_KEY
+ - NOVU_API_URL
+ - NOVU_API_HOST
+ - NODE_TLS_REJECT_UNAUTHORIZED=0
+ - NODE_ENV=development
+ networks:
+ - internal
+ labels:
+ - "traefik.enable=true"
+ - "traefik.project_name=${COMPOSE_PROJECT_NAME}"
+ - "traefik.http.routers.novu-studio.rule=Host(`studio-novu.${PHRASEA_DOMAIN}`)"
+ - "traefik.http.services.novu-studio.loadbalancer.server.port=2022"
+ extra_hosts:
+ - api-novu.${PHRASEA_DOMAIN}:${PS_GATEWAY_IP}
+ - bridge-novu.${PHRASEA_DOMAIN}:${PS_GATEWAY_IP}
+ volumes:
+ - ./novu/novu:/usr/src/app
+
volumes:
dev:
driver: local
diff --git a/docker-compose.yml b/docker-compose.yml
index 03193e9b1..d18f843a0 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -658,7 +658,9 @@ services:
- SENTRY_RELEASE
- CLIENT_ID=${DASHBOARD_CLIENT_ID}
- SOKETI_USAGE_URL
- - NOVU_FRONT_URL
+ - NOVU_DASHBOARD_URL
+ - NOVU_STUDIO_URL
+ - NOVU_BRIDGE_URL
labels:
- "traefik.http.routers.dashboard.rule=Host(`dashboard.${PHRASEA_DOMAIN}`)"
@@ -1113,7 +1115,7 @@ services:
environment:
API_ROOT_URL: ${NOVU_API_URL}
DISABLE_USER_REGISTRATION: 'false'
- FRONT_BASE_URL: ${NOVU_FRONT_URL}
+ FRONT_BASE_URL: ${NOVU_DASHBOARD_URL}
MONGO_URL: ${NOVU_MONGO_URL}
MONGO_MIN_POOL_SIZE: ${NOVU_MONGO_MIN_POOL_SIZE}
MONGO_MAX_POOL_SIZE: ${NOVU_MONGO_MAX_POOL_SIZE}
@@ -1134,6 +1136,8 @@ services:
- "traefik.http.services.novu-api.loadbalancer.server.port=3000"
extra_hosts:
- ws-novu.${PHRASEA_DOMAIN}:${PS_GATEWAY_IP}
+ - bridge-novu.${PHRASEA_DOMAIN}:${PS_GATEWAY_IP}
+ - studio-novu.${PHRASEA_DOMAIN}:${PS_GATEWAY_IP}
novu-worker:
image: ghcr.io/novuhq/novu/worker:${NOVU_VERSION}
@@ -1158,6 +1162,8 @@ services:
NOVU_SECRET_KEY: ${NOVU_SECRET_KEY}
extra_hosts:
- ws-novu.${PHRASEA_DOMAIN}:${PS_GATEWAY_IP}
+ - bridge-novu.${PHRASEA_DOMAIN}:${PS_GATEWAY_IP}
+ - studio-novu.${PHRASEA_DOMAIN}:${PS_GATEWAY_IP}
novu-ws:
image: ghcr.io/novuhq/novu/ws:${NOVU_VERSION}
@@ -1177,13 +1183,17 @@ services:
REDIS_PORT: ${NOVU_REDIS_PORT}
REDIS_PASSWORD: ${NOVU_REDIS_PASSWORD}
JWT_SECRET: ${NOVU_JWT_SECRET}
+ NOVU_SECRET_KEY: ${NOVU_SECRET_KEY}
labels:
- "traefik.enable=true"
- "traefik.project_name=${COMPOSE_PROJECT_NAME}"
- "traefik.http.routers.novu-ws.rule=Host(`ws-novu.${PHRASEA_DOMAIN}`)"
- "traefik.http.services.novu-ws.loadbalancer.server.port=3002"
+ extra_hosts:
+ - bridge-novu.${PHRASEA_DOMAIN}:${PS_GATEWAY_IP}
+ - studio-novu.${PHRASEA_DOMAIN}:${PS_GATEWAY_IP}
- novu-web:
+ novu-dashboard:
image: ghcr.io/novuhq/novu/web:${NOVU_VERSION}
depends_on:
- novu-api
@@ -1193,18 +1203,42 @@ services:
- internal
environment:
REACT_APP_API_URL: ${NOVU_API_URL}
+ REACT_APP_ENVIRONMENT: production
+ REACT_APP_WIDGET_EMBED_PATH: ${NOVU_DASHBOARD_URL}/embed.umd.min.js
REACT_APP_IS_SELF_HOSTED: 'true'
REACT_APP_WS_URL: ${NOVU_WS_URL}
- REACT_APP_WEB_CONTEXT_PATH: ${NOVU_FRONT_URL}/
- REACT_APP_API_CONTEXT_PATH: ${NOVU_API_URL}/
- REACT_APP_WIDGET_CONTEXT_PATH: ${NOVU_FRONT_URL}/
- REACT_APP_WS_CONTEXT_PATH: ${NOVU_WS_URL}/
command: ['/bin/sh', '-c', 'pnpm run envsetup:docker && pnpm run start:static:build']
labels:
- "traefik.enable=true"
- "traefik.project_name=${COMPOSE_PROJECT_NAME}"
- - "traefik.http.routers.novu-web.rule=Host(`novu.${PHRASEA_DOMAIN}`)"
- - "traefik.http.services.novu-web.loadbalancer.server.port=4200"
+ - "traefik.http.routers.novu-dashboard.rule=Host(`novu.${PHRASEA_DOMAIN}`)"
+ - "traefik.http.services.novu-dashboard.loadbalancer.server.port=4200"
+ extra_hosts:
+ - bridge-novu.${PHRASEA_DOMAIN}:${PS_GATEWAY_IP}
+ - studio-novu.${PHRASEA_DOMAIN}:${PS_GATEWAY_IP}
+
+ novu-bridge:
+ image: ${REGISTRY_NAMESPACE}novu-bridge:${DOCKER_TAG}
+ build: ./novu/bridge
+ profiles:
+ - novu
+ networks:
+ - internal
+ environment:
+ - NOVU_SECRET_KEY
+ - NEXT_PUBLIC_NOVU_SECRET_KEY=${NOVU_SECRET_KEY}
+ - NEXT_PUBLIC_NOVU_APPLICATION_IDENTIFIER=${NOVU_APPLICATION_IDENTIFIER}
+ - NEXT_PUBLIC_NOVU_SUBSCRIBER_ID=${NOVU_SUBSCRIBER_ID}
+ labels:
+ - "traefik.enable=true"
+ - "traefik.project_name=${COMPOSE_PROJECT_NAME}"
+ - "traefik.http.routers.novu-bridge.rule=Host(`bridge-novu.${PHRASEA_DOMAIN}`)"
+ - "traefik.http.services.novu-bridge.loadbalancer.server.port=4000"
+ extra_hosts:
+ - api-novu.${PHRASEA_DOMAIN}:${PS_GATEWAY_IP}
+ - bridge-novu.${PHRASEA_DOMAIN}:${PS_GATEWAY_IP}
+ - studio-novu.${PHRASEA_DOMAIN}:${PS_GATEWAY_IP}
+ - novu.${PHRASEA_DOMAIN}:${PS_GATEWAY_IP}
volumes:
db:
diff --git a/infra/dev/hosts.txt b/infra/dev/hosts.txt
index 1fd14bde6..0a3906ca5 100644
--- a/infra/dev/hosts.txt
+++ b/infra/dev/hosts.txt
@@ -31,4 +31,5 @@ ${IP} keycloak2.${PHRASEA_DOMAIN}
${IP} novu.${PHRASEA_DOMAIN}
${IP} ws-novu.${PHRASEA_DOMAIN}
${IP} api-novu.${PHRASEA_DOMAIN}
+${IP} bridge-novu.${PHRASEA_DOMAIN}
# ${PHRASEA_DOMAIN}>
diff --git a/infra/docker/dev/entrypoint.d/update-cert.sh b/infra/docker/dev/entrypoint.d/update-cert.sh
new file mode 100644
index 000000000..23d7759e2
--- /dev/null
+++ b/infra/docker/dev/entrypoint.d/update-cert.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+update-ca-certificates
diff --git a/novu/.dockerignore b/novu/.dockerignore
new file mode 100644
index 000000000..54e94a95c
--- /dev/null
+++ b/novu/.dockerignore
@@ -0,0 +1,13 @@
+**/.git
+**/.pnpm-store
+**/build
+**/dist
+**/.build
+**/*.log
+**/coverage
+**/Dockerfile
+**/.dockerignore
+**/node_modules
+**/.idea
+**/.gitignore
+**/.github
diff --git a/novu/bridge/.dockerignore b/novu/bridge/.dockerignore
new file mode 100644
index 000000000..69b8934fc
--- /dev/null
+++ b/novu/bridge/.dockerignore
@@ -0,0 +1,8 @@
+/Dockerfile
+.dockerignore
+node_modules
+.idea
+/bin
+/.gitignore
+/.github
+/dist
diff --git a/novu/bridge/.eslintrc.json b/novu/bridge/.eslintrc.json
new file mode 100644
index 000000000..bffb357a7
--- /dev/null
+++ b/novu/bridge/.eslintrc.json
@@ -0,0 +1,3 @@
+{
+ "extends": "next/core-web-vitals"
+}
diff --git a/novu/bridge/.github/workflows/novu.yml b/novu/bridge/.github/workflows/novu.yml
new file mode 100644
index 000000000..8b62471c7
--- /dev/null
+++ b/novu/bridge/.github/workflows/novu.yml
@@ -0,0 +1,27 @@
+name: Novu Sync
+
+on:
+ workflow_dispatch:
+
+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://dashboard.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
diff --git a/novu/bridge/.gitignore b/novu/bridge/.gitignore
new file mode 100644
index 000000000..fd3dbb571
--- /dev/null
+++ b/novu/bridge/.gitignore
@@ -0,0 +1,36 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+.yarn/install-state.gz
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/novu/bridge/Dockerfile b/novu/bridge/Dockerfile
new file mode 100644
index 000000000..30980ad9d
--- /dev/null
+++ b/novu/bridge/Dockerfile
@@ -0,0 +1,20 @@
+FROM node:20-alpine3.20
+
+RUN apk add --no-cache g++ make py3-pip
+
+ENV NX_DAEMON=false
+
+# Install global dependencies
+RUN npm --no-update-notifier --no-fund --global install pm2 pnpm@9.11.0 && \
+ pnpm --version
+
+# Set non-root user
+USER 1000
+
+WORKDIR /usr/src/app
+
+COPY --chown=1000:1000 . .
+
+RUN pnpm install --frozen-lockfile
+
+CMD ["pnpm", "dev"]
diff --git a/novu/bridge/README.md b/novu/bridge/README.md
new file mode 100644
index 000000000..fe81da467
--- /dev/null
+++ b/novu/bridge/README.md
@@ -0,0 +1,31 @@
+# Novu Bridge App
+
+This is a [Novu](https://novu.co/) bridge application bootstrapped with [`npx novu init`](https://www.npmjs.com/package/novu)
+
+## Getting Started
+
+To run the development server, run:
+
+```bash
+npm run dev
+# or
+yarn dev
+# or
+pnpm dev
+# or
+bun dev
+```
+
+By default, the [Next.js](https://nextjs.org/) server will start and your state can be synchronized with Novu Cloud via the Bridge Endpoint (default is `/api/novu`). Your server will by default run on [http://localhost:4000](http://localhost:4000).
+
+## Your first workflow
+
+Your first email workflow can be edited in `./app/novu/workflows.ts`. You can adjust your workflow to your liking.
+
+## Learn More
+
+To learn more about Novu, take a look at the following resources:
+
+- [Novu](https://novu.co/)
+
+You can check out [Novu GitHub repository](https://github.com/novuhq/novu) - your feedback and contributions are welcome!
diff --git a/novu/bridge/app/api/dev-studio-status/route.ts b/novu/bridge/app/api/dev-studio-status/route.ts
new file mode 100644
index 000000000..5d5e635a4
--- /dev/null
+++ b/novu/bridge/app/api/dev-studio-status/route.ts
@@ -0,0 +1,32 @@
+export async function GET() {
+ try {
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), 3000);
+
+ const response = await fetch("http://localhost:2022/.well-known/novu", {
+ signal: controller.signal,
+ headers: {
+ Accept: "application/json",
+ },
+ });
+
+ clearTimeout(timeoutId);
+
+ if (response.ok) {
+ const data = await response.json();
+ if (data.port && data.route) {
+ return Response.json({ connected: true, data });
+ }
+ }
+
+ return Response.json({
+ connected: false,
+ error: await response.text(),
+ });
+ } catch (error) {
+ return Response.json({
+ connected: false,
+ error: error instanceof Error ? error.message : "Unknown error",
+ });
+ }
+}
diff --git a/novu/bridge/app/api/events/route.ts b/novu/bridge/app/api/events/route.ts
new file mode 100644
index 000000000..ba897d9d2
--- /dev/null
+++ b/novu/bridge/app/api/events/route.ts
@@ -0,0 +1,32 @@
+export async function POST(request: Request) {
+ try {
+ const body = await request.json();
+
+ const response = await fetch("https://api.novu.co/v1/telemetry/measure", {
+ headers: {
+ Accept: "application/json",
+ "Content-Type": "application/json",
+ Authorization: `ApiKey ${process.env.NOVU_SECRET_KEY}`,
+ },
+ method: "POST",
+ body: JSON.stringify({
+ event: body.event,
+ data: body.data,
+ }),
+ });
+
+ if (response.ok) {
+ return Response.json({ success: true });
+ }
+
+ return Response.json({
+ connected: false,
+ error: await response.text(),
+ });
+ } catch (error) {
+ return Response.json({
+ connected: false,
+ error: error instanceof Error ? error.message : "Unknown error",
+ });
+ }
+}
diff --git a/novu/bridge/app/api/novu/route.ts b/novu/bridge/app/api/novu/route.ts
new file mode 100644
index 000000000..916b0029f
--- /dev/null
+++ b/novu/bridge/app/api/novu/route.ts
@@ -0,0 +1,7 @@
+import { serve } from "@novu/framework/next";
+import { welcomeOnboardingEmail } from "../../novu/workflows";
+
+// the workflows collection can hold as many workflow definitions as you need
+export const { GET, POST, OPTIONS } = serve({
+ workflows: [welcomeOnboardingEmail],
+});
diff --git a/novu/bridge/app/api/trigger/route.ts b/novu/bridge/app/api/trigger/route.ts
new file mode 100644
index 000000000..0459ac751
--- /dev/null
+++ b/novu/bridge/app/api/trigger/route.ts
@@ -0,0 +1,24 @@
+import { NextResponse } from "next/server";
+import { welcomeOnboardingEmail } from "../../novu/workflows";
+
+export async function POST() {
+ try {
+ await welcomeOnboardingEmail.trigger({
+ to: process.env.NEXT_PUBLIC_NOVU_SUBSCRIBER_ID || "",
+ payload: {},
+ });
+
+ return NextResponse.json({
+ message: "Notification triggered successfully",
+ });
+ } catch (error: unknown) {
+ const errorMessage =
+ error instanceof Error ? error.message : "Unknown error occurred";
+ console.error("Error triggering notification:", errorMessage);
+
+ return NextResponse.json(
+ { message: "Error triggering notification", error: errorMessage },
+ { status: 500 },
+ );
+ }
+}
diff --git a/novu/bridge/app/components/NotificationToast/Notifications.module.css b/novu/bridge/app/components/NotificationToast/Notifications.module.css
new file mode 100644
index 000000000..f35c3d40d
--- /dev/null
+++ b/novu/bridge/app/components/NotificationToast/Notifications.module.css
@@ -0,0 +1,80 @@
+.toast {
+ position: fixed;
+ background: linear-gradient(135deg, #ffffff 0%, #f8f9ff 100%);
+ border-radius: 16px;
+ padding: 18px 24px;
+ box-shadow:
+ 0 10px 25px rgba(0, 0, 0, 0.1),
+ 0 6px 12px rgba(0, 0, 0, 0.08),
+ 0 0 0 1px rgba(255, 255, 255, 0.5) inset;
+ z-index: 1000;
+ width: 90%;
+ max-width: 400px;
+ right: 24px;
+ top: 24px;
+ border: 1px solid rgba(0, 0, 0, 0.06);
+ animation: slideIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
+ backdrop-filter: blur(10px);
+ transform-origin: top right;
+}
+
+.toastContent {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ position: relative;
+ overflow: hidden;
+ font-weight: 600;
+ background: linear-gradient(90deg, #1a1a1a 0%, #404040 100%);
+ -webkit-background-clip: text;
+ color: transparent;
+ font-size: 1rem;
+ letter-spacing: -0.02em;
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+}
+
+.toastContent::before {
+ content: '';
+ position: absolute;
+ top: -50%;
+ left: -50%;
+ width: 200%;
+ height: 200%;
+ background: linear-gradient(45deg,
+ transparent 0%,
+ rgba(255, 255, 255, 0.1) 50%,
+ transparent 100%);
+ animation: shimmer 2s infinite;
+}
+
+@keyframes slideIn {
+ 0% {
+ transform: translateY(-120%) scale(0.9);
+ opacity: 0;
+ }
+
+ 100% {
+ transform: translateY(0) scale(1);
+ opacity: 1;
+ }
+}
+
+@keyframes shimmer {
+ 0% {
+ transform: translateX(-100%) rotate(45deg);
+ }
+
+ 100% {
+ transform: translateX(100%) rotate(45deg);
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .toast {
+ animation: none;
+ }
+
+ .toastContent::before {
+ animation: none;
+ }
+}
\ No newline at end of file
diff --git a/novu/bridge/app/components/NotificationToast/Notifications.tsx b/novu/bridge/app/components/NotificationToast/Notifications.tsx
new file mode 100644
index 000000000..43cdd43d2
--- /dev/null
+++ b/novu/bridge/app/components/NotificationToast/Notifications.tsx
@@ -0,0 +1,66 @@
+"use client";
+
+import { Novu } from "@novu/js";
+import { useEffect, useState } from "react";
+import { Inbox } from "@novu/nextjs";
+import styles from "./Notifications.module.css"; // You'll need to create this
+
+const NotificationToast = () => {
+ const novu = new Novu({
+ subscriberId: process.env.NEXT_PUBLIC_NOVU_SUBSCRIBER_ID || "",
+ applicationIdentifier:
+ process.env.NEXT_PUBLIC_NOVU_APPLICATION_IDENTIFIER || "",
+ });
+
+ const [showToast, setShowToast] = useState(false);
+
+ useEffect(() => {
+ const listener = ({ result: notification }: { result: any }) => {
+ console.log("Received notification:", notification);
+ setShowToast(true);
+
+ setTimeout(() => {
+ setShowToast(false);
+ }, 2500);
+ };
+
+ console.log("Setting up Novu notification listener");
+ novu.on("notifications.notification_received", listener);
+
+ return () => {
+ novu.off("notifications.notification_received", listener);
+ };
+ }, [novu]);
+
+ if (!showToast) return null;
+
+ return (
+
+
New In-App Notification
+
+ );
+};
+
+export default NotificationToast;
+
+const novuConfig = {
+ applicationIdentifier:
+ process.env.NEXT_PUBLIC_NOVU_APPLICATION_IDENTIFIER || "",
+ subscriberId: process.env.NEXT_PUBLIC_NOVU_SUBSCRIBER_ID || "",
+ appearance: {
+ elements: {
+ bellContainer: {
+ width: "30px",
+ height: "30px",
+ },
+ bellIcon: {
+ width: "30px",
+ height: "30px",
+ },
+ },
+ },
+};
+
+export function NovuInbox() {
+ return ;
+}
diff --git a/novu/bridge/app/favicon.ico b/novu/bridge/app/favicon.ico
new file mode 100644
index 000000000..718d6fea4
Binary files /dev/null and b/novu/bridge/app/favicon.ico differ
diff --git a/novu/bridge/app/fonts/GeistMonoVF.woff b/novu/bridge/app/fonts/GeistMonoVF.woff
new file mode 100644
index 000000000..f2ae185cb
Binary files /dev/null and b/novu/bridge/app/fonts/GeistMonoVF.woff differ
diff --git a/novu/bridge/app/fonts/GeistVF.woff b/novu/bridge/app/fonts/GeistVF.woff
new file mode 100644
index 000000000..1b62daacf
Binary files /dev/null and b/novu/bridge/app/fonts/GeistVF.woff differ
diff --git a/novu/bridge/app/globals.css b/novu/bridge/app/globals.css
new file mode 100644
index 000000000..e3734be15
--- /dev/null
+++ b/novu/bridge/app/globals.css
@@ -0,0 +1,42 @@
+:root {
+ --background: #ffffff;
+ --foreground: #171717;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --background: #0a0a0a;
+ --foreground: #ededed;
+ }
+}
+
+html,
+body {
+ max-width: 100vw;
+ overflow-x: hidden;
+}
+
+body {
+ color: var(--foreground);
+ background: var(--background);
+ font-family: Arial, Helvetica, sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+* {
+ box-sizing: border-box;
+ padding: 0;
+ margin: 0;
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+}
+
+@media (prefers-color-scheme: dark) {
+ html {
+ color-scheme: dark;
+ }
+}
diff --git a/novu/bridge/app/layout.tsx b/novu/bridge/app/layout.tsx
new file mode 100644
index 000000000..dca06aee7
--- /dev/null
+++ b/novu/bridge/app/layout.tsx
@@ -0,0 +1,33 @@
+import type { Metadata } from "next";
+import localFont from "next/font/local";
+import "./globals.css";
+
+const geistSans = localFont({
+ src: "./fonts/GeistVF.woff",
+ variable: "--font-geist-sans",
+ weight: "100 900",
+});
+const geistMono = localFont({
+ src: "./fonts/GeistMonoVF.woff",
+ variable: "--font-geist-mono",
+ weight: "100 900",
+});
+
+export const metadata: Metadata = {
+ title: "Create Next App",
+ description: "Generated by create next app",
+};
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/novu/bridge/app/novu/emails/novu-onboarding-email.tsx b/novu/bridge/app/novu/emails/novu-onboarding-email.tsx
new file mode 100644
index 000000000..6dcff5dca
--- /dev/null
+++ b/novu/bridge/app/novu/emails/novu-onboarding-email.tsx
@@ -0,0 +1,157 @@
+import React from "react";
+import {
+ Body,
+ Button,
+ CodeInline,
+ Column,
+ Container,
+ Head,
+ Heading,
+ Html,
+ Img,
+ Preview,
+ render,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+} from "@react-email/components";
+
+import { ControlSchema, PayloadSchema } from "../workflows";
+
+type NovuWelcomeEmailProps = ControlSchema & PayloadSchema;
+
+export const NovuWelcomeEmail = ({
+ components,
+ userImage,
+ teamImage,
+ arrowImage,
+ showHeader,
+}: NovuWelcomeEmailProps) => {
+ return (
+
+