@rhinolabs/platform
v0.5.0
Published
SDK for Rhinolabs Platform services
Readme
@rhinolabs/platform
SDK for Rhinolabs Platform services.
pnpm add @rhinolabs/platformEntry points
One entry per service; bundlers tree-shake what you don't use (sideEffects: false).
| Import | Contents |
| --- | --- |
| @rhinolabs/platform | Cross-service — shared errors (PlatformError), SDK_VERSION |
| @rhinolabs/platform/notify | Everything Notify — NotifyApi types and contract, HTTP client, Hono push/feed routes, browser push controller, feed client, service worker handler |
| @rhinolabs/platform/notify/sw | Service-worker handler only (handleNotifyPush), dependency-free. Import this from your sw.js so its bundle never has to resolve hono or zod — the main ./notify entry pulls both. |
| @rhinolabs/platform/react | React layer for every service (runtime split, not a new service entry) — feed provider and hooks only; UI components stay in each app. Kept out of ./notify so worker bundles never pull in react/jsx-runtime; react is an optional peer dependency. |
Notify
Service binding (Cloudflare Workers)
The Notify binding implements NotifyApi: send, schedule, plus the scheduled, push, preferences, and feed namespaces.
import type { NotifyApi } from '@rhinolabs/platform/notify';
await env.Notify.send({
type: 'order.shipped',
recipient: { id: userId },
payload: { orderId },
});HTTP client
For consumers that cannot use a service binding. Same NotifyApi, errors are thrown as NotifyHttpError.
import { createNotifyHttp } from '@rhinolabs/platform/notify';
const notify = createNotifyHttp({ baseUrl, apiKey });Browser push
Three pieces, one per runtime:
1. Backend — notifyPushRoutes (requires the optional hono peer) mounts the endpoints the browser flow needs, backed by the Notify binding: GET /vapid-public-key, POST /subscriptions, DELETE /subscriptions. recipientId resolves the authenticated user; eligible gates by product rules (plan, device, …) and returns 403 when it fails.
import { notifyPushRoutes } from '@rhinolabs/platform/notify';
app.route(
'/api/push',
notifyPushRoutes({
recipientId: (c) => c.get('userId'),
eligible: (c) => isDesktop(c.req.header('user-agent')),
}),
);2. Frontend — createPushController drives the whole subscribe flow (permission prompt → service worker registration → PushManager.subscribe → save). You supply the three calls to the routes above:
import { createPushController } from '@rhinolabs/platform/notify';
const controller = createPushController({
getVapidKey: () => api.get('/api/push/vapid-public-key').then((r) => r.publicKey),
saveSubscription: (sub) => api.post('/api/push/subscriptions', sub),
removeSubscription: (endpoint) => api.delete('/api/push/subscriptions', { endpoint }),
});
await controller.getState(); // { subscribed, permission: NotificationPermission | 'unsupported' }
await controller.enable(); // prompts, registers /sw.js (configurable), subscribes, saves
await controller.disable();3. Service worker — ship a worker at the registered path (default /sw.js) and delegate to handleNotifyPush: it shows the notification, broadcasts { type: clientMessageType } to open windows (badge/feed refresh), and focuses or opens the payload's href on click.
import { handleNotifyPush } from '@rhinolabs/platform/notify/sw';
handleNotifyPush(self, { defaultTitle: 'MyApp', clientMessageType: 'inbox_changed' });Import from ./notify/sw, not ./notify: the sw entry has zero dependencies, so the worker bundle stays dependency-free regardless of the bundler's tree-shaking. Registration defaults to a classic script, so bundle the file (e.g. esbuild sw.ts --bundle --outfile=public/sw.js); alternatively pass serviceWorkerType: 'module' to createPushController and serve the worker as ESM directly.
In-app inbox (feed + realtime)
1. Backend — notifyFeedRoutes proxies the whole inbox surface behind your session auth: list, unread count, mark-read, and the realtime WebSocket upgrade (spliced browser↔Platform after the handshake):
import { notifyFeedRoutes } from '@rhinolabs/platform/notify';
app.route(
'/api/notifications',
notifyFeedRoutes({ recipientId: (c) => c.var.user.id }),
);2. Frontend — createFeedClient owns the socket lifecycle (reconnection with backoff, polling fallback, edge-answered heartbeat) plus the feed HTTP calls:
import { createFeedClient } from '@rhinolabs/platform/notify';
const feed = createFeedClient({ basePath: '/api/notifications' });
const stop = feed.subscribe(() => refetchInbox()); // first subscriber connects3. React (optional) — provider + hooks; the UI is yours:
import { NotifyFeedProvider, useFeed, useUnreadCount } from '@rhinolabs/platform/react';
<NotifyFeedProvider client={feed}>
<MyBell /> {/* const { unread } = useUnreadCount() */}
<MyPanel /> {/* const { items, loadMore, markRead, markAllRead } = useFeed() */}
</NotifyFeedProvider>