Go implementation of the Dyson Network Ring notification service. Handles push notifications, email delivery, and Server-Sent Events (SSE) streaming for real-time notifications.
- Push Notifications: APNs (Apple), FCM (Google), UnifiedPush, and WebSocket relay
- Email Delivery: SMTP-based email sender
- SOP Streaming: Server-Sent Events plus a polling list for recent notifications
- gRPC API: High-performance notification delivery API
- HTTP API: REST endpoints for notifications, subscriptions, and preferences
- Background Jobs: In-process worker pool with graceful shutdown
- Retention: Configurable notification and soft-delete cleanup
- Go 1.22+
- PostgreSQL 14+
- (Optional) Redis for session/queue caching
Copy and edit the example config:
cp config.example.toml config.tomlKey settings:
[http]
host = "0.0.0.0"
port = 8080
[grpc]
host = "0.0.0.0"
port = 9090
useTLS = false
certFile = ""
keyFile = ""
[database]
dsn = "postgres://postgres:postgres@localhost:5432/dyson_ring?sslmode=disable"go build -o dyson-ring ./cmd
./dyson-ring serve -config config.tomlImport legacy data from another Postgres database into the current schema:
./dyson-ring migrate \
-config config.toml \
-source-dsn "postgres://old-user:old-pass@localhost:5432/old_db?sslmode=disable"-source-dsn also accepts PostgreSQL keyword/value DSNs like host=... port=... dbname=... user=... password=..., but quote the whole value so your shell passes it as one argument.
Useful flags:
./dyson-ring migrate -source-dsn "..." -batch-size 500
./dyson-ring migrate -source-dsn "..." -provider-map "0:apple,1:google,2:unifiedpush,3:sop,4:websocket"
./dyson-ring migrate -source-dsn "..." -priority-map "0:normal,1:normal,2:normal,3:normal,4:high,5:high,6:high,7:high,8:critical,9:critical,10:critical"
./dyson-ring migrate -source-dsn "..." -preference-map "0:normal,1:muted,2:nopush,3:noemail,4:rejected"The migration runs incrementally by batch and checks the target row count before continuing, so reruns resume from the current imported count.
cmd/ # CLI entrypoint
internal/
├── app/ # Runtime composition and lifecycle
├── config/ # Viper-backed configuration
├── db/ # Migration helpers
├── dispatch/ # Worker pool and job processing
├── email/ # SMTP email sender
├── flush/ # Invalid token flush buffer
├── grpcsvc/ # gRPC Ring service implementation
├── handler/ # HTTP handlers
├── model/ # Domain models
├── preference/ # Preference service
├── push/ # Push delivery (providers, SOP, deliverer)
├── retention/ # Cleanup jobs
├── server/ # Gin router setup
└── store/ # PostgreSQL data access
migrations/ # SQL migrations
config.example.toml
go.mod
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/notifications/count |
Unread count |
| GET | /api/notifications/ |
List notifications |
| POST | /api/notifications/all/read |
Mark all as read |
| POST | /api/notifications/send |
Send notification |
| Method | Endpoint | Description |
|---|---|---|
| PUT | /api/notifications/subscription |
Upsert subscription |
| GET | /api/notifications/subscription |
List subscriptions |
| GET | /api/notifications/subscription/current |
Get current device subscription |
| DELETE | /api/notifications/subscription/:id |
Delete subscription |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/notifications/preferences |
List preferences |
| GET | /api/notifications/preferences/:topic |
Get topic preference |
| PUT | /api/notifications/preferences/:topic |
Upsert preference |
| DELETE | /api/notifications/preferences/:topic |
Soft-delete preference |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/notifications/sop/subscription |
Register SOP subscription |
| GET | /api/notifications/sop/ |
List SOP subscriptions |
| GET | /api/notifications/sop/poll |
List recent SOP notifications for polling clients |
| GET | /api/notifications/sop/stream |
SSE stream (token via query/header) |
| Method | Endpoint | Description |
|---|---|---|
| GET | /health |
Liveness check |
| GET | /ready |
Readiness check (DB ping) |
Implements DyRingService:
SendEmail- Queue email deliverySendPushNotificationToUser- Send to single userSendPushNotificationToUsers- Fan out to multiple usersUnsubscribePushNotifications- Remove subscriptions by device ID
[app]
name = "dyson-ring"
env = "development" # or "production"
[http]
host = "0.0.0.0"
port = 8080
[grpc]
host = "0.0.0.0"
port = 9090
useTLS = true
certFile = "/app/keys/server.crt"
keyFile = "/app/keys/server.key"
[database]
dsn = "postgres://..."
max_conns = 10
[redis]
addr = "localhost:6379"
[worker]
count = 0 # 0 = runtime.NumCPU()
buffer = 100
[email]
server = "smtp.example.com"
port = 587
use_ssl = false
from_address = "notifications@example.com"
from_name = "Dyson Notifications"
subject_prefix = "[Dyson]"
[apple]
private_key_path = "/path/to/apns.p8"
private_key_id = "KEYID"
team_id = "TEAMID"
bundle_id = "com.dyson.app"
production = false
[google]
service_account_path = "/path/to/firebase.json"
[flush]
interval = "30s"
max_size = 100
[retention]
notification_days = 90
soft_delete_days = 30- SOP (if active stream connected)
- Apple (APNs)
- Google (FCM)
- UnifiedPush
- Device IDs with
:sopsuffix are normalized for grouping - One effective push target per physical device
- SOP preferred when connected
go test ./... -v# Tidy dependencies
go mod tidy
# Build
go build ./...
# Run tests
go test ./...docker build -t dyson-ring .- In-process queue means no cross-node job distribution
- SOP stream map is process-local
- Backpressure via channel blocking (not broker buffering)
- For horizontal scale later: swap dispatcher channel for Redis Streams/JetStream