Rotur notifications
OriginChats has two notification paths:
- A connected client can create a foreground browser notification when it receives a relevant
message_newevent. - Rotur can deliver Web Push to a registered service worker while the client is closed or suspended.
These paths complement each other. WebSocket events do not wake a closed browser, and Web Push should not duplicate an alert already handled by a focused tab.
The official client implementation is split across src/net/commands/message/new.ts, src/lib/services/push-manager.ts, src/net/api/notify.ts, and src/sw.ts in originchats-client.
originchats-osl can send the second path directly through Rotur for messages created with the WebSocket message_new command. It schedules delivery only after the message has been persisted, so a failed write never produces a push for a message that does not exist. Push delivery runs outside the WebSocket request and a Rotur outage does not turn a successful message_new into an error.
Foreground message handling
Section titled “Foreground message handling”Handle notification policy after applying the incoming message to client state. Do not notify when any of these conditions apply:
- the message is authored by the current user;
- the sender is blocked;
- the message or channel is muted;
- the relevant channel/thread is actively visible in a focused document;
- an ephemeral message is not meant to create durable unread state.
The official client defaults to mention/reply notifications, supports per-server and per-channel all, mentions, and none levels, and always treats a direct message as notification-worthy when it is not muted. It marks unread state before displaying the alert.
function showForegroundNotification( title: string, body: string, serverUrl: string, channel: string,) { if (Notification.permission !== 'granted') return;
const notification = new Notification(title, { body, tag: `${serverUrl}:${channel}`, }); notification.onclick = () => { window.focus(); notification.close(); openChannel(serverUrl, channel); };}Use a stable tag per server and channel. A single application-wide tag causes unrelated notifications to replace one another.
Ask for permission
Section titled “Ask for permission”Request notification permission only in response to a deliberate user action. A denied permission cannot be fixed by retrying; direct the user to their browser or operating-system settings.
const permission = await Notification.requestPermission();if (permission !== 'granted') return;Feature-detect all required browser APIs before offering background push:
const supported = 'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window;Web Push requires a secure context in production. Service workers and push subscriptions are unavailable in some embedded browsers and privacy modes, so foreground WebSocket notifications must remain a valid fallback.
Register Web Push with Rotur
Section titled “Register Web Push with Rotur”Use the constant source name originChats consistently. Rotur scopes endpoint registration and allowed senders by this source.
With the Rotur SDK
Section titled “With the Rotur SDK”import { Rotur } from 'rotur-sdk';
const SOURCE = 'originChats';const rotur = new Rotur({ token: roturToken });const { public_key: vapidPublicKey } = await rotur.push.vapidKeys();const registration = await navigator.serviceWorker.ready;const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: base64urlToBytes(vapidPublicKey),});const json = subscription.toJSON();
await rotur.push.register( subscription.endpoint, json.keys!.p256dh!, json.keys!.auth!, SOURCE, await deviceFingerprint(),);The fingerprint identifies this client installation to Rotur; it is not a cryptographic device identity. Keep it stable enough to update a changed endpoint, but do not build it from invasive or unnecessary fingerprinting inputs.
The official client checks registration first with rotur.push.check(SOURCE, fingerprint). It registers when missing and re-registers when the stored endpoint differs from the browser’s current subscription.
Without the SDK
Section titled “Without the SDK”The equivalent Rotur v2 calls use the user’s bearer token except for the public VAPID key:
| Method | Path | Authentication | Purpose |
|---|---|---|---|
GET |
/notify/vapid |
None | Get the application server public key |
GET |
/notify/check?source=…&fingerprint=… |
Bearer token | Check this device registration |
POST |
/notify/register |
Bearer token | Create or update an endpoint |
All paths are beneath https://api.rotur.dev/v2.
{ "endpoint": "https://push-service.example/subscription-id", "p256dh": "<base64url key>", "auth": "<base64url secret>", "source": "originChats", "fingerprint": "<stable installation fingerprint>"}Send the registration body as JSON with Authorization: Bearer <Rotur token>. Never send the Rotur token, subscription keys, or endpoint over the OriginChats WebSocket.
Authorize a server owner
Section titled “Authorize a server owner”Rotur requires the receiving user to allow a sender for a source. The official client uses the server.owner.name supplied by the OriginChats handshake:
await rotur.push.allowSender(serverOwnerUsername, 'originChats');Removing access uses:
await rotur.push.removeSender(serverOwnerUsername, 'originChats');Authorization is per Rotur username and source, not per OriginChats server URL. If one owner operates several servers, enabling or disabling that owner affects all of them. Clients must show this scope clearly rather than presenting it as an isolated per-server permission.
Treat the handshake owner as an asserted server identity. A client concerned about impersonation should apply the same server identity and validator-key checks used during connection setup before inviting the user to authorize that owner.
Send a background notification
Section titled “Send a background notification”Sending belongs in trusted infrastructure, not in a browser client. A sender needs a Rotur token with notifications:send; exposing that credential would let anyone impersonate the server’s notification sender.
With the SDK:
const sender = new Rotur({ token: serverOwnerToken });
await sender.push.sendMany(recipientUsernames, 'originChats', { title: `${authorUsername} in #${channel}`, body: truncatedMessagePreview, data: { source: publicServerUrl, channelName: channel, threadId, messageId, },});For one recipient, use rotur.push.send(username, source, options). Before batching, rotur.push.notifiableUsers('originChats') can return users who registered this source and allowed the authenticated sender. Intersect that set with actual message recipients; never broadcast to every notifiable user.
Without the SDK, send the same fields to Rotur v2:
{ "source": "originChats", "title": "alice in #general", "body": "A short message preview", "data": { "source": "https://chat.example.com", "channelName": "general", "threadId": null, "messageId": "550e8400-e29b-41d4-a716-446655440000" }, "users": [ "sophie" ]}Use POST https://api.rotur.dev/v2/notify/ with the sender’s bearer token. For one username, use POST /notify/{username} and omit users.
Only queue a push after the message has been persisted successfully. Recipient selection must reuse server authorization rules:
- exclude the author;
- include only users allowed to view the channel and thread;
- honor bans and membership state;
- select mention, role-mention, reply, or direct-message targets according to supported policy;
- avoid including sensitive message content when the recipient should receive only a generic alert.
The OSL server does not store each client’s local notification-level override. Its server-wide mentions mode is therefore the conservative default; all is an operator choice and sends to every eligible server member. A receiving user can disable background delivery by removing the server owner’s Rotur sender authorization.
Configure the OSL server
Section titled “Configure the OSL server”Background delivery is off by default. Enable it in the server database’s config.json:
{ "notifications": { "enabled": true, "mode": "mentions", "source": "originChats", "include_content": true, "max_preview_length": 160 }}Provide the sending account’s Rotur bearer token to the server process:
export ROTUR_NOTIFICATION_TOKEN='replace-with-server-owner-token'./originchats-osl use /absolute/path/to/server-dbThe token must have notifications:send, and its Rotur username must be the sender the client authorizes. ROTUR_TOKEN remains a compatibility fallback when ROTUR_NOTIFICATION_TOKEN is absent, but the dedicated variable is preferred. Never put either token in config.json.
| Setting | Behavior |
|---|---|
enabled |
Enables server-side Rotur delivery. |
mode: "mentions" |
Sends to direct user mentions, permitted role mentions, and reply targets whose reply ping was not disabled. |
mode: "all" |
Sends to every eligible server user. |
source |
Rotur registration source. Keep this as originChats for the official client. |
include_content |
Includes a shortened message body when true; otherwise uses a generic preview. |
max_preview_length |
Maximum number of message-content characters placed in the push body. |
Both modes exclude the author, banned users, unknown users, and anyone who cannot view the channel. Recipient usernames are deduplicated before the server uses Rotur’s batch endpoint. Transient network errors, 429, and 5xx responses receive up to three total attempts; other 4xx responses are not retried. If notifications are enabled without a token, startup logs a warning and message delivery continues normally.
Service-worker delivery
Section titled “Service-worker delivery”A service worker should parse JSON defensively, fall back to text, suppress duplicates when an appropriate window is already active, and keep navigation data separate from presentation text.
self.addEventListener('push', (event) => { if (!event.data) return;
event.waitUntil((async () => { const payload = event.data.json(); const windows = await self.clients.matchAll({ type: 'window', includeUncontrolled: true, }); if (windows.some((client) => client.focused)) return;
const data = payload.data ?? {}; await self.registration.showNotification(payload.title || 'OriginChats', { body: payload.body || '', icon: '/originchats-logo.jpg', badge: '/originchats-logo.jpg', tag: `${data.source ?? 'originchats'}:${data.channelName ?? 'inbox'}`, data, renotify: true, }); })());});On notification click, prefer an already-open window for the same server, send it a navigation message, and focus it. Only call openWindow() when no suitable client exists. Validate data.source before turning it into a route; do not navigate to an arbitrary external URL from push data.
Subscription lifecycle
Section titled “Subscription lifecycle”Push subscriptions can rotate without user action. Proper handling includes:
- comparing the subscription’s current endpoint with Rotur’s recorded endpoint;
- recreating the subscription when Rotur’s VAPID public key changes;
- responding to
pushsubscriptionchangeand re-registering through a live page; - retrying transient network and
5xxfailures with bounded exponential backoff; - deferring registration work while offline and resuming on the
onlineevent; - clearing cached registration state when the Rotur token changes or the user logs out;
- removing event listeners and polling intervals when the application is torn down.
Do not repeatedly request browser permission, create parallel subscriptions, or retry 401/403 responses without first fixing authentication or authorization.
Privacy and payload design
Section titled “Privacy and payload design”Push payloads pass through browser push infrastructure and appear on lock screens. Keep bodies short, avoid secrets and attachment URLs, and offer a generic-preview mode. Put only routing identifiers in data; fetch authoritative message content after the user opens the client.
Use these states separately in the UI:
| State | Meaning |
|---|---|
| Unsupported | Required browser APIs are unavailable |
| Permission required | Browser permission has not been requested |
| Blocked | Browser or OS permission is denied |
| Unregistered | Permission exists but Rotur has no current endpoint |
| Registered | Endpoint is current, but no server owner may be allowed |
| Enabled for owner | Registration exists and that Rotur sender is allowed |
This distinction makes failures diagnosable and prevents a checked toggle from implying delivery is guaranteed.
