Skip to main content

Notification Service

The Notification Service provides real-time push notifications across Edd Cloud. It consumes events from other services via NATS, persists them in PostgreSQL, and delivers them to connected clients over WebSocket.

Features

  • Event-Driven Ingestion: Receives notification events from services via NATS JetStream
  • Persistent Storage: Stores all notifications in PostgreSQL for retrieval
  • Real-time Push: Delivers notifications to connected clients via WebSocket
  • REST API: Paginated listing, unread counts, and read status management
  • JWT Authentication: All endpoints require a valid JWT

Architecture

Message Flow

  1. A service (compute, storage, auth) publishes a protobuf-encoded notification to NATS on subject notify.{user_id}.
  2. The notification service consumer receives the message from the NOTIFICATIONS stream.
  3. The message is deserialized and inserted into the notifications table in PostgreSQL.
  4. If the target user has an active WebSocket connection, the notification is broadcast to all of their connected clients in real time.
  5. The NATS message is acknowledged after successful database insertion.

If the database insert fails, the message is NAK'd and will be redelivered (up to 5 attempts).

API Endpoints

REST

All REST endpoints require a Bearer JWT in the Authorization header.

MethodPathDescription
GET/api/notificationsList notifications for authenticated user
GET/api/notifications/unread-countGet count of unread notifications
POST/api/notifications/{id}/readMark a specific notification as read
POST/api/notifications/read-allMark all notifications as read
GET/api/notifications/mutesList user's muted notification scopes
PUT/api/notifications/mutesMute notifications for a category/scope
DELETE/api/notifications/mutesUnmute notifications for a category/scope
GET/healthzHealth check

WebSocket

PathDescription
GET /ws/notificationsReal-time notification stream

Pagination

The GET /api/notifications endpoint supports pagination via query parameters:

ParameterTypeDefaultMaxDescription
limitint20100Number of notifications to return
offsetint0-Number of notifications to skip

Notifications are returned ordered by created_at DESC (newest first).

Response Formats

Notification Object

{
"id": 42,
"user_id": "abc123",
"title": "Container Ready",
"message": "Container 'dev-box' is now running",
"link": "/compute/containers/abc",
"category": "compute",
"scope": "",
"read": false,
"created_at": "2026-02-08T14:30:00Z"
}

Unread Count

{
"count": 5
}

Mark Read / Mark All Read

{
"status": "ok"
}

Notification Mutes

GET /api/notifications/mutes — List all muted scopes for the authenticated user.

[
{
"id": 1,
"user_id": "abc123",
"category": "storage",
"scope": "my-files",
"created_at": "2026-02-08T14:30:00Z"
}
]

PUT /api/notifications/mutes — Mute a category/scope combination.

Request body:

{
"category": "storage",
"scope": "my-files"
}

DELETE /api/notifications/mutes — Unmute a category/scope combination.

Request body:

{
"category": "storage",
"scope": "my-files"
}

Both category and scope are required. Returns {"status": "ok"} on success.

WebSocket Connection

Clients connect to /ws/notifications to receive real-time push notifications. Authentication is provided via a token query parameter or a Bearer token in the Authorization header.

const ws = new WebSocket(
`wss://cloud.eddisonso.com/ws/notifications?token=${jwt}`
);

ws.onmessage = (event) => {
const notification = JSON.parse(event.data);
console.log(`[${notification.category}] ${notification.title}`);
};

Connection Limits

  • Maximum 5 concurrent WebSocket connections per user
  • Connections exceeding the limit are immediately closed
  • Origin validation restricts connections to *.cloud.eddisonso.com
  • Closed or failed connections are automatically cleaned up

NATS Integration

Stream Configuration

SettingValue
Stream nameNOTIFICATIONS
Subject patternnotify.>
RetentionLimits policy
Max messages1,000,000
Max bytes1 GB
Max age7 days
StorageFile

Consumer Configuration

SettingValue
Consumer namenotification-service
DurableYes
Ack policyExplicit
Ack wait30 seconds
Max redeliveries5
Deliver policyAll

Protobuf Message Format

Notifications are published as protobuf-encoded messages using the Notification message type:

message Notification {
EventMetadata metadata = 1;
string user_id = 2;
string title = 3;
string message = 4;
string link = 5;
string category = 6;
string scope = 7; // Optional scope within category (e.g., storage namespace)
}

Publishing from Other Services

Services use the publisher package to send notifications:

import "eddisonso.com/notification-service/pkg/publisher"

pub, err := publisher.New(natsURL, "my-service")
if err != nil {
log.Fatal(err)
}
defer pub.Close()

err = pub.Notify(ctx, userID, "Container Ready",
"Container 'dev-box' is now running",
"/compute/containers/abc",
"compute",
"", // scope (empty for non-scoped notifications)
)

The scope parameter enables per-scope muting. For storage notifications, pass the namespace name as the scope so users can mute notifications from specific namespaces.

Notification Categories

CategorySourceScopeExamples
computeCompute Service(none)Container started, container stopped
storageStorage Servicenamespace nameFile uploaded, file deleted, namespace deleted
authAuth Service(none)Service account created, service account deleted

Database Schema

CREATE TABLE notifications (
id BIGSERIAL PRIMARY KEY,
user_id TEXT NOT NULL,
title TEXT NOT NULL,
message TEXT NOT NULL,
link TEXT NOT NULL DEFAULT '',
category TEXT NOT NULL DEFAULT '',
scope TEXT NOT NULL DEFAULT '',
read BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_notifications_user_unread
ON notifications (user_id, read, created_at DESC);

CREATE TABLE notification_mutes (
id BIGSERIAL PRIMARY KEY,
user_id TEXT NOT NULL,
category TEXT NOT NULL,
scope TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, category, scope)
);

CREATE INDEX idx_notification_mutes_user
ON notification_mutes (user_id);

The composite index on (user_id, read, created_at DESC) optimizes the most common query patterns: listing a user's notifications and counting unread items.

The notification_mutes table stores per-user mute preferences. When a notification arrives with a matching (category, scope), it is silently dropped by the consumer.

Configuration

FlagDescriptionDefault
-addrHTTP listen address:8080
-log-serviceLog service gRPC address(disabled)
Env VariableDescriptionRequired
DATABASE_URLPostgreSQL connection stringYes
JWT_SECRETSecret for validating JWTsYes
NATS_URLNATS server URLNo (default: nats://nats:4222)

Health Check

GET /healthz -> 200 OK