@platform/sdk-notify
@platform/sdk-notify wraps the Platform Notification Service. It
supports email, SMS, WebSocket, browser push, Telegram, Slack, Webhook
and any other channel via pluggable adapters. The SDK call always
returns immediately — actual delivery happens asynchronously via a
RabbitMQ-backed queue with automatic retry.
Installation
pnpm add @platform/sdk-notify
send()
Send a single notification through any registered channel. Returns
202 Accepted immediately — delivery is asynchronous:
import { kernel } from '@platform/sdk-core';
const result = await kernel.notify().send({
userId: '01j9pusr0000000000000001', // Recipient — platform resolves channel config
channel: 'email', // Registered channel identifier
subject: 'Your payout was processed', // Channel-specific (email subject)
body: 'You received $250.00. View your wallet for details.',
priority: 'normal', // 'low' | 'normal' | 'high' | 'critical'
metadata: {
transactionId: '01j9ptx0000000000000001',
},
});
// result: { notificationId: 'uuid', status: 'queued' }
| Parameter | Type | Required | Description |
|---|---|---|---|
userId | string (ULID) | ✅ | Recipient user ID — the service resolves their channel subscriptions |
channel | string | ✅ | Any registered channel: email, sms, telegram, slack, webhook, websocket |
subject | string | ☐ | Channel-specific subject line (email, push) |
body | string | ✅ | Notification body text or HTML |
priority | string | ☐ | low / normal / high / critical. Default: normal |
metadata | object | ☐ | Arbitrary key-value pairs attached to audit record |
send() always returns 202 Accepted. Use GET /notifications/:id
to poll delivery status.
sendBatch()
Send the same notification to multiple recipients:
const job = await kernel.notify().sendBatch({
userIds: ['01j9pusr...', '01j9pusr...', '01j9pusr...'],
channel: 'email',
subject: 'Platform maintenance window — April 25, 2026',
body: 'The platform will undergo scheduled maintenance from 02:00 to 04:00 UTC.',
priority: 'high',
});
// For <= 100 recipients: synchronous delivery, returns { notificationIds: [...] }
// For > 100 recipients: async job, returns { jobId: 'uuid', status: 'processing' }
Batch Job Progress
When userIds.length > 100, the service dispatches a background job:
// Poll job progress
const progress = await kernel.notify().getBatchJobStatus(job.jobId);
// {
// jobId: 'uuid',
// status: 'processing', // 'processing' | 'completed' | 'failed'
// sent: 847,
// failed: 3,
// total: 1200,
// }
HTTP equivalent:
GET https://api.septemcore.com/v1/notifications/jobs/{jobId}
Authorization: Bearer <access_token>
| Limit | Value |
|---|---|
| Max recipients per batch call | 500 |
| Async threshold | > 100 recipients |
| Rate limit | 100 notifications/minute per tenant · 50/minute per module |
sendFromTemplate()
Render a pre-registered template with variable substitution and send it:
// Step 1: Register the template (one-time, usually at module install)
await kernel.notify().registerTemplate({
id: 'payout-confirmation',
channel: 'email',
subject: 'Your payout of {{amountFormatted}} is on its way',
body: `
Hi {{firstName}},
Your payout of {{amountFormatted}} has been processed.
Transaction ID: {{transactionId}}
Expected arrival: {{arrivalDate}}
Questions? Contact support at {{supportEmail}}.
`,
});
// Step 2: Send from template
await kernel.notify().sendFromTemplate({
templateId: 'payout-confirmation',
userId: '01j9pusr0000000000000001',
variables: {
firstName: 'Alice',
amountFormatted: '$250.00',
transactionId: '01j9ptx0000000000000001',
arrivalDate: 'April 24, 2026',
},
});
Templates are stored in PostgreSQL. Template body supports
{{variableName}} substitution. Templates are scoped to the tenant —
one tenant's templates are never accessible to another.
WebSocket — Real-Time Notifications
The SDK manages the WebSocket connection to wss://notify.platform.io/ws
automatically. Module authors subscribe to channels and receive live
messages without managing the connection lifecycle:
// Subscribe to a named channel
const unsubscribe = kernel.notify().onMessage('dashboard.metrics', (msg) => {
updateMetricWidget(msg.payload);
});
// Clean up on component unmount
return () => unsubscribe();
// Send a broadcast to all subscribers of a channel (server-to-client push)
await kernel.notify().broadcast({
channel: 'dashboard.metrics',
payload: { activeUsers: 1240, revenueToday: 98500 },
});
WebSocket Protocol
| Step | Description |
|---|---|
| 1. Connect | Client opens wss://notify.platform.io/ws |
| 2. Authenticate | First message: { "type": "auth", "token": "<JWT>" } |
| 3. Confirmed | Server responds: { "type": "auth_ok", "userId": "...", "tenantId": "..." } |
| 4. Subscribe | { "type": "subscribe", "channel": "dashboard.metrics" } |
| 5. Receive | { "type": "notification", "channel": "...", "payload": {...}, "id": "uuid" } |
Invalid JWT → server closes with code 4401.
WebSocket is the only platform service that accepts direct client connections (bypassing the Gateway). Persistent connections are too expensive to proxy. JWT authentication applies regardless.
WebSocket Rate Limits
| Limit | Value |
|---|---|
| Messages per second per tenant | 200 (NOTIFY_WS_RATE_PER_TENANT) |
| Concurrent connections per tenant | 1,000 (NOTIFY_WS_MAX_CONNECTIONS_PER_TENANT) |
Exceeding limits → throttle + warning message. One overloaded tenant cannot degrade WebSocket performance for other tenants.
registerChannel()
Register a custom notification channel adapter. The channel appears in Settings → Notifications after registration:
await kernel.notify().registerChannel({
id: 'sendgrid',
name: 'SendGrid Email',
description: 'Deliver email via SendGrid API',
icon: 'https://cdn.example.com/sendgrid-icon.svg',
config: {
apiKey: process.env.SENDGRID_API_KEY!,
fromName: 'Acme Corp',
},
});
// Registration is idempotent — same id = update existing record
Channel registration is typically declared in module.manifest.json
(notificationChannels field) and executed automatically at module
install time. The programmatic call is available for runtime
configuration updates.
listChannels()
Retrieve all notification channels registered for the current tenant:
const channels = await kernel.notify().listChannels();
// [
// { id: 'websocket', name: 'WebSocket', type: 'builtin', enabled: true },
// { id: 'push', name: 'Browser Push', type: 'builtin', enabled: true },
// { id: 'sendgrid', name: 'SendGrid Email', type: 'plugin', enabled: true },
// { id: 'telegram', name: 'Telegram', type: 'plugin', enabled: false },
// ]
websocket and push are built-in channels that are always available.
All other channels require an adapter module to be installed and
configured.
Delivery Retry Policy
When a channel adapter fails (SendGrid down, Telegram API timeout), the Notification Service retries via RabbitMQ with exponential backoff:
| Parameter | Value |
|---|---|
| Max retries | 5 per notification |
| Backoff schedule | 30s → 60s → 120s → 240s → 480s (±10% jitter) |
| Timeout per attempt | 10 seconds per adapter call |
| After max retries | Status failed — visible in Admin UI → Notifications → Failed |
| Manual retry | POST /api/v1/notifications/{id}/retry |
| Automatic fallback | None — if email fails, SMS is NOT triggered automatically. Tenant controls channel selection. |
Notification History and Retention
| Parameter | Value |
|---|---|
| Notification history | 90 days in PostgreSQL (NOTIFY_HISTORY_RETENTION_DAYS) |
| Failed notifications | 180 days — longer for investigating delivery problems |
| Long-term audit | Audit Service (7 years) — every send() creates an audit.record |
Error Reference
| Error type | Status | When |
|---|---|---|
channel-not-found | 404 | Specified channel is not registered for this tenant |
user-not-found | 404 | userId does not exist in this tenant |
rate-limit-exceeded | 429 | Tenant (100/min) or module (50/min) rate limit hit |
batch-too-large | 400 | userIds.length > 500 |
template-not-found | 404 | templateId does not exist for this tenant |
delivery-failed | 202 | Delivery did not fail at call time — check notification status via GET /notifications/:id |