@platform/sdk-notify
sdk-notify wraps the Platform Notification Service. Exports: send() for single notifications (202 Accepted, async delivery), sendBatch() for up to 500 recipients (async job when > 100), sendFromTemplate() with variable substitution, registerChannel() for pluggable adapter registration, listChannels(). WebSocket managed by sdk-core. RabbitMQ delivery + 5-retry exponential backoff. Rate: 100/min per tenant, 50/min per module.
@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-notifysend()
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 /notify/: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',
});
// Returns enqueued notification IDs immediately
// result: { ids: ['...'], count: 3 }| 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',
supportEmail: '[email protected]',
},
});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!,
fromEmail: '[email protected]',
fromName: 'Acme Corp',
},
});
// Registration is idempotent — same id = update existing recordChannel 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/notify/{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 /notify/:id |
@platform/sdk-events
sdk-events wraps the Platform Event Bus (Apache Kafka + RabbitMQ). Exports: publish() for domain events, subscribe() with built-in DLQ (3 strikes → dead-letter topic), onEvent() for browser Custom Events. Tenant isolation enforced by Gateway. Publish/subscribe RBAC via manifest.events.publishes[] and manifest.events.subscribes[].
@platform/sdk-files
sdk-files wraps the Platform File Storage (S3-compatible, SeaweedFS / AWS S3). Exports: upload() with antivirus staging-bucket scan, download(), getUrl() presigned with TTL, uploadImage() with bimg/libvips processing and crop editor, processImage(), getThumbnail() by preset. Progress callbacks. Soft delete + 30-day restore window.