Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,10 @@ dist
.nx
.vscode
notiflo
target/
.git
.github
.claude
*.md
!package.json
!yarn.lock
25 changes: 3 additions & 22 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,8 @@ jobs:
- '.github/workflows/ci.yml'
node:
- 'apps/notiflo/src/**'
- 'apps/notiflo-web/**'
- 'libs/bridge/**'
- 'libs/pipeline/**'
- 'libs/analytics/**'
- 'package.json'
- 'yarn.lock'
- 'tsconfig.base.json'
Expand Down Expand Up @@ -107,7 +106,7 @@ jobs:
node-version: 20
cache: yarn
- run: yarn install --frozen-lockfile
- run: npx nx affected -t test --passWithNoTests
- run: npx nx affected -t test --passWithNoTests --exclude='engine-core'

nestjs-e2e:
needs: changes
Expand All @@ -127,29 +126,11 @@ jobs:
node-version: 20
cache: yarn
- run: yarn install --frozen-lockfile
- run: npx nx affected -t e2e
- run: npx nx e2e notiflo-e2e
env:
REDIS_URL: redis://localhost:6379
MONGOMS_VERSION: 8.0.4

load-test-smoke:
needs: changes
if: needs.changes.outputs.rust == 'true'
runs-on: ubuntu-latest
services:
redis:
image: redis:7-alpine
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
shared-key: rust-ci
- name: Run hot-path load test (smoke)
run: cargo run --release --bin load-test --no-default-features -- --conditions 1000 --ticks 10000 --redis-url redis://localhost:6379

bench:
needs: changes
if: needs.changes.outputs.rust == 'true'
Expand Down
123 changes: 93 additions & 30 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,45 @@ The core pipeline runs in Rust as a standalone binary. NestJS is the control pla
### Architecture

```
[Data Sources] → [Rust Pipeline Worker] → [Provider APIs]
WebSocket Ingest → Evaluate SendGrid
Kafka (Drift Sentinel) Twilio
Redis Queue → Deliver FCM
→ Event Log OneSignal

[NestJS Control Plane] ← Redis Streams (delivery events)
Config API (alerts, subscribers, templates, channels)
Dashboard API
MongoDB (persistence)

[Next.js Dashboard]
Overview, alerts, notifications, engine status
[Data Sources] notiflo-runtime (Rust) [Delivery]

Redis Stream ────────► Ingest (per-connector)
Evaluate
Drift Sentinel (<100ns)
Expression DSL
Rhai Script Sandbox
Template Render (Handlebars)
┌───────┴────────┐
▼ ▼
In-App HTTP Provider ──────► SendGrid, Twilio,
(0μs) │ FCM, Slack, Webhook
│ │
└───────┬───────┘
Event Log ──► Redis Stream (notiflo:events:delivery)
notiflo-api (NestJS)
Redis Stream Consumer
├── Persist to MongoDB
└── Broadcast via WebSocket (/ws/notifications)
notiflo-web (Next.js)
Live Dashboard + Notifications
Pipeline Metrics (polled from Rust /health)
```

**Rust binary (the worker):** Consumes streams, evaluates conditions via Drift Sentinel, delivers to providers, writes delivery events to Redis streams.
**Rust binary (`notiflo-runtime`):** Ingests data from pluggable Redis Stream connectors, evaluates conditions via Drift Sentinel, renders Handlebars templates, delivers notifications (in-app or HTTP), writes delivery events to Redis streams. Conditions and connectors are loaded from MongoDB and refreshed every 5s.

**NestJS (the server):** REST API for config CRUD, dashboard endpoints, reads delivery events from Redis streams for history/observability.
**NestJS (`notiflo-api`):** REST API for config CRUD, dashboard endpoints, consumes delivery events from Redis streams, persists to MongoDB, broadcasts to WebSocket clients. Polls Rust `/health` endpoint for pipeline metrics every 5s.

**Next.js (`notiflo-web`):** Live dashboard with WebSocket connection for real-time delivery events and pipeline metrics. Pipeline Performance panel, notifications feed with LIVE indicator, connectors management.

**They are two separate processes.** NestJS talks to users. Rust talks to data.

Expand Down Expand Up @@ -55,15 +76,22 @@ New strategies implement the `EvaluationStrategy` trait and register in `evaluat
├── apps/
│ ├── notiflo/ # NestJS control plane
│ │ └── src/app/
│ │ ├── alerts/ # Alert conditions CRUD + tick ingestion
│ │ ├── alerts/ # Alert conditions CRUD
│ │ ├── channels/ # Channel providers + registry
│ │ ├── connectors/ # Redis Stream connector management
│ │ ├── core/types/ # Channel + notification type definitions
│ │ ├── dashboard/ # Dashboard API + engine metrics
│ │ ├── notifications/ # Notification records
│ │ ├── notifications/ # Notification records + WebSocket gateway + Redis stream consumer
│ │ ├── organizations/ # Multi-tenant org management
│ │ ├── subscribers/ # Subscriber management
│ │ └── templates/ # Template engine (Handlebars)
│ └── notiflo-web/ # Next.js dashboard (as-is)
│ └── notiflo-web/ # Next.js live dashboard
│ ├── components/
│ │ ├── dashboard/ # PipelineMetrics, OverviewMetrics, ChannelHealth
│ │ ├── notifications/ # NotificationsList
│ │ └── connectors/ # ConnectorsList
│ ├── hooks/ # useWebSocket, useLiveMetrics, useLiveNotifications, useConnectors
│ └── pages/ # dashboard, notifications, alerts, connectors
├── libs/
│ ├── bridge/napi-bridge/ # @notiflo/bridge/napi-bridge — Rust addon wrapper + mock
│ └── engine/
Expand All @@ -75,9 +103,24 @@ New strategies implement the `EvaluationStrategy` trait and register in `evaluat
│ │ │ └── script_strategy.rs # Rhai sandbox
│ │ ├── benches/condition_bench.rs # Criterion benchmarks
│ │ └── src/napi_exports.rs # Node.js bridge (for config push)
│ ├── notiflo-runtime/ # Rust standalone binary — the hot path
│ │ └── src/
│ │ ├── main.rs # Binary entry point
│ │ ├── pipeline.rs # Pipeline orchestrator + PipelineStats (atomics)
│ │ ├── ingest/ # Redis Stream ingest (per-connector)
│ │ ├── delivery/ # Router, InAppProvider, HttpProvider
│ │ ├── event_log.rs # Redis Streams delivery event writer
│ │ ├── config.rs # Env config
│ │ ├── health.rs # /health endpoint with latency + throughput metrics
│ │ ├── subscriber_cache.rs # DashMap cache backed by MongoDB
│ │ ├── connector_loader.rs # MongoDB connector loader (hot-reload)
│ │ └── template.rs # Handlebars template renderer
│ └── shared-types/ # Rust shared types (traits, tick, condition)
├── scripts/
│ ├── seed.sh # Seeds demo org, subscribers, alerts, connector
│ └── ticker-simulator.sh # Random-walk price ticker
├── Cargo.toml # Rust workspace root
└── tsconfig.base.json # TS path aliases
└── docker-compose.yml # Full stack: mongo, redis, runtime, api, web, seed, ticker
```

### Path Aliases
Expand Down Expand Up @@ -135,17 +178,32 @@ npx nx serve notiflo-web # Start dashboard

---

## What Needs to Be Built (Core Pipeline)

### Rust standalone binary (`notiflo-runtime`)
1. **Ingest connectors** — WebSocket (tokio-tungstenite), Kafka (rdkafka), Redis queue (redis crate)
2. **Delivery layer** — async HTTP client (reqwest) for provider APIs, retry with backoff, dead letter to Redis stream
3. **Event log** — writes delivery events to Redis streams for NestJS to consume
4. **Config loader** — reads alert conditions from MongoDB or Redis, watches for changes

### NestJS changes
- Config push: write alert configs to Redis/MongoDB for Rust binary to read
- Dashboard: consume delivery events from Redis streams
## What's Built (Core Pipeline)

### Rust standalone binary (`notiflo-runtime`) — COMPLETE
1. **Ingest connectors** — Redis Stream ingest with per-connector tasks, hot-reloaded from MongoDB every 5s
2. **Delivery layer** — InAppProvider (0μs, no HTTP) + HttpProvider (reqwest, retry with backoff, dead letter queue)
3. **Event log** — Redis Streams writer (`notiflo:events:delivery`) with rendered_content for in-app
4. **Config loader** — reads alert conditions + connectors from MongoDB, periodic refresh
5. **Pipeline instrumentation** — lock-free atomics for eval latency, delivery latency, throughput
6. **Health endpoint** — `/health` exposes all metrics (avg/max eval latency, throughput tps, delivery count)
7. **Subscriber cache** — DashMap backed by MongoDB with auto-detect from available contact data

### NestJS — COMPLETE
- **Connectors module** — CRUD for Redis Stream connectors
- **Redis Stream consumer** — consumes delivery events, persists to MongoDB
- **WebSocket gateway** — standalone `ws.Server` on `/ws/notifications` (Fastify-compatible, no WsAdapter)
- **Metrics bridge** — polls Rust `/health` every 5s, broadcasts via WebSocket

### Next.js Dashboard — COMPLETE
- **Pipeline Performance** — 8-card grid with live throughput, eval latency, deliveries (via WebSocket)
- **Notifications feed** — live delivery events with LIVE/OFFLINE indicator
- **Connectors page** — manage Redis Stream connectors
- **WebSocket hooks** — `useWebSocket`, `useLiveMetrics`, `useLiveNotifications`

### Docker Compose — COMPLETE
- Full stack: MongoDB, Redis, Rust runtime, NestJS API, Next.js dashboard, seed data, ticker simulator
- `docker compose up` starts everything with health checks and proper ordering

### Design Principles
- Hot path stays in Rust — never crosses to Node
Expand All @@ -154,6 +212,11 @@ npx nx serve notiflo-web # Start dashboard
- Node consumes at its own pace, persists to MongoDB
- Burst-heavy, high-sleep pattern — optimize for the burst

### Known Gotchas
- **Fastify + WebSocket**: Fastify intercepts HTTP upgrade requests before NestJS WsAdapter. Use standalone `ws.Server` with `noServer: true` and manual `httpServer.on('upgrade', ...)` handler.
- **Serde empty objects**: `channelPreferences: {}` deserializes as `Some(HashMap::new())`, not `None`. Check `.map_or(false, |p| !p.is_empty())`.
- **Ticker drift**: Random-walk prices drift away from thresholds over time. Restart ticker with runtime for fresh crossings.

---

## User Preferences
Expand Down
35 changes: 20 additions & 15 deletions Dockerfile.api
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
FROM node:20-alpine As development
FROM node:20-alpine AS build

RUN apk update && apk add yarn curl bash
RUN apk update && apk add yarn curl bash python3 make g++

WORKDIR /usr/src/app
WORKDIR /app

COPY package*.json yarn.lock ./

RUN yarn install --ignore-engines --only=development
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile --ignore-engines

COPY . .

RUN yarn build notiflo --prod


# Override engine-core build target to a no-op — cargo is not available in this image.
# The NestJS app gracefully degrades without the Rust napi addon.
RUN node -e " \
const f = 'libs/engine/engine-core/project.json'; \
const p = JSON.parse(require('fs').readFileSync(f,'utf8')); \
p.targets.build = {executor:'nx:run-commands',options:{command:'echo engine-core build skipped in Docker'}}; \
require('fs').writeFileSync(f, JSON.stringify(p, null, 2));"

FROM node:20-alpine as production
RUN npx nx build notiflo --configuration=production

WORKDIR /usr/src/app
# Prune to production deps
RUN yarn install --production --frozen-lockfile --ignore-engines

COPY --from=development /usr/src/app/dist/apps/notiflo ./
FROM node:20-alpine

RUN yarn install --only=production
WORKDIR /app

RUN yarn add source-map-support
COPY --from=build /app/dist/apps/notiflo ./
COPY --from=build /app/node_modules ./node_modules

CMD node -r source-map-support/register main.js
CMD ["node", "main.js"]
4 changes: 2 additions & 2 deletions Dockerfile.runtime
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
FROM rust:1.78-slim-bookworm AS build
FROM rust:slim-bookworm AS build
RUN apt-get update && apt-get install -y pkg-config libssl-dev && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
COPY libs/engine/ libs/engine/
RUN cargo build --release --bin notiflo-runtime
RUN cargo build --release --bin notiflo-runtime --no-default-features

FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates libssl3 && rm -rf /var/lib/apt/lists/*
Expand Down
27 changes: 27 additions & 0 deletions Dockerfile.web
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
FROM node:20-alpine AS build

RUN apk update && apk add yarn curl bash python3 make g++

WORKDIR /app

COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile --ignore-engines

COPY . .
ENV API_PROXY_TARGET=http://notiflo-api:3000
RUN npx nx build notiflo-web --configuration=production

FROM node:20-alpine

WORKDIR /app

COPY --from=build /app/dist/apps/notiflo-web/.next ./.next
COPY --from=build /app/dist/apps/notiflo-web/public ./public
COPY --from=build /app/apps/notiflo-web/next.config.js ./
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/package.json ./

ENV API_PROXY_TARGET=http://notiflo-api:3000

EXPOSE 4200
CMD ["npx", "next", "start", "-p", "4200"]
Loading