Skip to main content

@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' }
ParameterTypeRequiredDescription
userIdstring (ULID)Recipient user ID — the service resolves their channel subscriptions
channelstringAny registered channel: email, sms, telegram, slack, webhook, websocket
subjectstringChannel-specific subject line (email, push)
bodystringNotification body text or HTML
prioritystringlow / normal / high / critical. Default: normal
metadataobjectArbitrary 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>
LimitValue
Max recipients per batch call500
Async threshold> 100 recipients
Rate limit100 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

StepDescription
1. ConnectClient opens wss://notify.platform.io/ws
2. AuthenticateFirst message: { "type": "auth", "token": "<JWT>" }
3. ConfirmedServer 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

LimitValue
Messages per second per tenant200 (NOTIFY_WS_RATE_PER_TENANT)
Concurrent connections per tenant1,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 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:

ParameterValue
Max retries5 per notification
Backoff schedule30s → 60s → 120s → 240s → 480s (±10% jitter)
Timeout per attempt10 seconds per adapter call
After max retriesStatus failed — visible in Admin UI → Notifications → Failed
Manual retryPOST /api/v1/notifications/{id}/retry
Automatic fallbackNone — if email fails, SMS is NOT triggered automatically. Tenant controls channel selection.

Notification History and Retention

ParameterValue
Notification history90 days in PostgreSQL (NOTIFY_HISTORY_RETENTION_DAYS)
Failed notifications180 days — longer for investigating delivery problems
Long-term auditAudit Service (7 years) — every send() creates an audit.record

Error Reference

Error typeStatusWhen
channel-not-found404Specified channel is not registered for this tenant
user-not-found404userId does not exist in this tenant
rate-limit-exceeded429Tenant (100/min) or module (50/min) rate limit hit
batch-too-large400userIds.length > 500
template-not-found404templateId does not exist for this tenant
delivery-failed202Delivery did not fail at call time — check notification status via GET /notifications/:id