NotiFlow App
A production-ready standalone notification service. Deploy it, call the REST API from your backend, and let users connect via WebSocket — no library integration required.
Overview
notiflow-app is a self-contained notification delivery service built on top of notiflow-spring-boot-starter. It exposes a REST API for managing users, topics, API keys, and dispatching notifications, and delivers those notifications to end-user clients over persistent WebSocket connections.
X-API-Key/swagger-ui/index.html/admin.html for quick ops; full Vue 3 SPA for complete managementGetting Started
Prerequisites
- Java 21 JDK
- Maven (system
mvn) - PostgreSQL 16 — or use the included
docker-compose.yaml - The sibling projects must be installed locally first:
mvn installin bothnotiflow-coreandnotiflow-spring-boot-starter
Build & run
Default endpoints
| Resource | Default |
|---|---|
| HTTP port | 8080 |
| WebSocket | ws://host:8080/ws/notifications |
| Swagger UI | http://localhost:8080/swagger-ui/index.html |
| OpenAPI JSON | http://localhost:8080/v3/api-docs |
First-run setup
Liquibase seeds an admin account with setup_complete = false on first boot. You must set the admin password before any authenticated call:
Architecture
JPA-backed implementations
| Interface | Implementation | Table(s) |
|---|---|---|
NotificationQueue | JpaNotificationQueue | queued_notifications |
TopicService | JpaTopicService | topic_subscriptions |
DeliveryTracker | AuditingDeliveryTracker wrapping InMemoryDeliveryTracker | notification_audit |
Admin Login (JWT + refresh tokens)
Short-lived JWTs for API access paired with long-lived rotating refresh tokens stored as HttpOnly cookies.
| Token | Default lifetime | Property |
|---|---|---|
| Access (JWT) | 3600 s (1 h) | app.jwt.access-token-expiration |
| Refresh | 2 592 000 s (30 days) | app.jwt.refresh-token-expiration |
API Key Authentication
Issue keys from the admin panel and pass them via the X-API-Key header. Only the SHA-256 hash is stored — the raw key is shown once at creation.
WebSocket Token Authentication
Each user gets a bearer token (prefixed nfu_) when registered via POST /api/users. Pass it in the WebSocket URL query string. Tokens can be regenerated via POST /api/users/{id}/regenerate-token.
On connection WebSocketAuthenticator hashes the token with SHA-256, looks it up in app_users, and stores the user's externalId in the session. Inactive users are rejected with close code 1008 VIOLATED_POLICY.
REST API — Auth /api/auth
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /api/auth/status | Public | Check whether admin setup is complete |
POST | /api/auth/setup | Public | Set admin password — one-time, min 6 chars |
POST | /api/auth/login | Public | Authenticate; returns accessToken + expiresIn |
POST | /api/auth/refresh | Cookie | Rotate refresh token, get new access token |
POST | /api/auth/logout | Cookie | Revoke refresh token and clear cookie |
REST API — Users /api/users
Requires ROLE_ADMIN (JWT) or ROLE_API_CLIENT (API Key).
| Method | Path | Description |
|---|---|---|
POST | /api/users | Register user — body: externalId, name. Returns record + raw WebSocket token (shown once) |
GET | /api/users?page&size | Paginated list with online and connectedAt fields |
GET | /api/users/{id} | Single user by internal UUID |
PATCH | /api/users/{id} | Update name or active flag |
DELETE | /api/users/{id} | Delete user permanently |
POST | /api/users/{id}/regenerate-token | Invalidate existing token and issue a new one |
REST API — Topics /api/topics
Requires ROLE_ADMIN or ROLE_API_CLIENT. Topics are created implicitly on subscribe.
| Method | Path | Description |
|---|---|---|
GET | /api/topics?page&size | Paginated list with subscriber counts |
GET | /api/topics/{topic} | Subscriber count for a single topic |
GET | /api/topics/{topic}/subscribers | Paginated subscriber list (with online status) |
GET | /api/topics/{topic}/subscribers/search?q | Search subscribers by query string |
POST | /api/topics/{topic}/subscribe/{userId} | Subscribe user (externalId) to topic — idempotent |
DELETE | /api/topics/{topic}/subscribe/{userId} | Unsubscribe user from topic |
DELETE | /api/topics/{topic} | Remove topic and all its subscriptions |
GET | /api/topics/user/{userId} | All topic names a user is subscribed to |
REST API — Notifications /api/notifications
Requires ROLE_ADMIN or ROLE_API_CLIENT. All three endpoints share the same request body.
Request body (NotificationRequest)
| Field | Type | Required | Description |
|---|---|---|---|
title | String | Yes | Notification title |
message | String | Yes | Notification body |
type | String | No | Application-defined category (e.g. "alert", "info") |
priority | String | No | LOW, NORMAL, HIGH, or URGENT. Defaults to NORMAL |
| Method | Path | Description |
|---|---|---|
POST | /api/notifications/user/{userId} | Send to user by externalId. Queued to PostgreSQL if offline. |
POST | /api/notifications/topic/{topic} | Send to all subscribers. Online → WebSocket; offline → queued. |
POST | /api/notifications/broadcast | Send to all currently connected sessions. Offline users do not receive it. |
REST API — API Keys /api/admin/api-keys
Requires ROLE_ADMIN (JWT only).
| Method | Path | Description |
|---|---|---|
GET | /api/admin/api-keys | List all keys. rawKey is always null in list responses. |
POST | /api/admin/api-keys | Create key — body: {"name":"..."}. Response includes rawKey once. |
PATCH | /api/admin/api-keys/{id} | Enable or disable — body: {"active": false} |
DELETE | /api/admin/api-keys/{id} | Delete permanently |
POST | /api/admin/api-keys/{id}/regenerate-key | Issue new key value — old key immediately invalidated |
REST API — Stats /api/stats
Requires ROLE_ADMIN or ROLE_API_CLIENT.
| Method | Path | Response |
|---|---|---|
GET | /api/stats | onlineCount, queueTotal, topicCount |
GET | /api/stats/sessions?page&size | Paginated active sessions: userId, sessionId, connectedAt, subscribedTopics |
GET | /api/stats/queue?page&size&userId | Paginated persisted queue entries |
GET | /api/stats/queue/{userId} | All queued notifications for a user (non-destructive peek) |
Notification Flow (End-to-End)
Direct user notification
- Caller sends
POST /api/notifications/user/{userId} NotificationControllerconverts the request to aNotificationand callsNotificationService.sendToUser(userId, notification)- User online:
DefaultNotificationServicefinds the session inInMemorySessionRegistry, serializes the notification into anEnvelopeof typeNOTIFICATION, and sends it viaSession.getBasicRemote().sendText(...) - User offline:
JpaNotificationQueue.enqueue(userId, notification)inserts a row intoqueued_notifications - If
ackEnabled = true,AuditingDeliveryTracker.track(...)writes anotification_auditrow withsentAt = now() - On reconnect, queued notifications are flushed as
QUEUED-type envelopes - Client sends back
ACK→AuditingDeliveryTracker.acknowledge(...)stampsackedAt
Topic notification
- Caller sends
POST /api/notifications/topic/{topic} JpaTopicService.getSubscribers(topic)returns the set ofexternalIdvaluessendToUseris called for each subscriber — online delivers live, offline queues
Broadcast
- Caller sends
POST /api/notifications/broadcast SessionRegistry.getAll()is iterated and each live session receives the notification- Offline users do not receive broadcast notifications — no queue entry is created
WebSocket Client Guide
Connecting
Message types
| Type | Direction | Description |
|---|---|---|
NOTIFICATION | Server → Client | Live notification (user was online when sent) |
QUEUED | Server → Client | Notification flushed from offline queue on reconnect |
ACK | Client → Server | Payload: {"notificationId":"<id>"} |
PING | Server → Client | Heartbeat probe (every notiflow.heartbeat.ping-interval) |
PONG | Client → Server | Heartbeat response — must be sent to stay connected |
notiflow.queue.ttl (default 10m). Reconnecting within that window guarantees delivery. The per-user queue is capped at notiflow.queue.max-per-user (default 50) — oldest entries are dropped when the cap is reached.
Built-in Admin Panel
NotiFlow App ships a lightweight admin panel as a static resource served directly by the application — no separate deployment needed. Open it in a browser once the app is running:
The panel is a single vanilla-JS page that communicates with the REST API using the admin JWT cookie from POST /api/auth/login. It provides:
- Live stats — connected user count and queued message total, auto-refreshed every 5 seconds
- Send notifications — broadcast to all, send to a specific user ID, or send to a topic; choose type, title, message, and priority
- Topic management — subscribe a user to a topic, unsubscribe, and browse the current topic list with subscriber counts
- Response log — every API action is logged with its HTTP status and response body for quick debugging
refresh_token cookie set by POST /api/auth/login. Complete the first-run setup and log in via POST /api/auth/setup + POST /api/auth/login before opening the panel, or it will show an unauthenticated state.
Vue Admin Dashboard
notiflow-admin-dashboard is a standalone Vue 3 + Vite SPA with a full admin interface. It connects to the notiflow-app REST API and provides a richer management experience than the built-in panel.
Features
Development
Database Schema
All tables are created and versioned by Liquibase (classpath:db/changelog/db.changelog-master.yaml).
| Table | Key columns | Purpose |
|---|---|---|
admin_account | id, username, password_hash, setup_complete | Single admin credential record seeded by Liquibase |
api_keys | id, name, key_hash, key_prefix, active, created_at | SHA-256-hashed API keys for machine auth |
refresh_tokens | id, token_hash, admin_id, expires_at | Rotating refresh tokens linked to admin account |
app_users | id, external_id, name, active, token_hash | End-user registry; token_hash authenticates WebSocket connections |
topic_subscriptions | id, topic, user_id; unique(topic, user_id) | Persistent topic subscriptions by externalId |
queued_notifications | id, user_id, title, message, priority, metadata (jsonb), created_at | Durable store for offline users |
notification_audit | id, user_id, notification_id, sent_at, acked_at, failed_at | Delivery audit trail written by AuditingDeliveryTracker |
Configuration Reference
Environment variables
| Variable | Default | Required in production |
|---|---|---|
DB_URL | jdbc:postgresql://localhost:5432/notiflow | Yes |
DB_USERNAME | notiflow | Yes |
DB_PASSWORD | notiflow | Yes |
JWT_SECRET | change-me-in-production-at-least-32-bytes!! | Yes — min 32 bytes |
CORS_ORIGINS | http://localhost:5173 | Yes (browser clients) |
Docker Deployment
Included docker-compose.yaml
The repository includes a compose file that starts both PostgreSQL and the application:
JWT_SECRET with openssl rand -base64 32 — never commit it to source control.
Use real database credentials.
Terminate TLS at a reverse proxy (nginx / ALB) — the refresh_token cookie has Secure=true and will not transmit over plain HTTP.
WebSocket clients must connect via wss://.
Restrict CORS_ORIGINS to your actual frontend origin(s).
NotiFlow · Open Source · Built for Java