Reference — Dashboard
lumio_admin_dashboard is the canonical, working web-push integration. It is a Next.js App Router app, and every piece a product integrator needs — service worker, server-side bridge to Notify, subscribe flow — already exists and runs. It is also the only committed Notify browser integration in this workspace today. This page maps those real files so you can open them and copy the shape.
Source root: momentum-online/lumio_admin_dashboard.
The dashboard is an admin surface for testing and operating web push, so it hardcodes player_id: 0 when it creates a subscription. A real product integration must resolve a positive player_id from the signed-in player's session. Copy everything else here; do not copy player_id: 0. See Casino Integration for the positive-player_id pattern.
Request flow
Every browser-originated call goes to a same-origin Next.js route handler. That handler verifies the admin session, reads the tenant from the X-Notify-Client-Id header, then makes a server-to-server call to Notify with a server-only token. The browser never sees a Notify token.
Two server fetch helpers live in src/lib/inappNotifications/server/notifyServer.ts:
| Helper | Token source | Always adds | Used by |
|---|---|---|---|
notifyPushPlatformFetch | NOTIFY_PUSH_PLATFORM_TOKEN | Authorization: Bearer, X-Source-App: lumio_admin_dashboard | subscription + browser-event paths |
notifyFetch | NOTIFY_EDITOR_TOKEN | Authorization: Bearer, plus X-Notify-Client-Id / X-Admin-User when passed | activity, send, preview |
When X-Notify-Client-Id is present, requireAdminRouteContext() also calls GET /api/v1/tenants/ through notifyFetch() to verify the selected tenant is allowed. In practice that means the dashboard's web-push screens need both server tokens configured: NOTIFY_PUSH_PLATFORM_TOKEN for upstream push-platform calls and NOTIFY_EDITOR_TOKEN for tenant validation and admin routes.
Service worker
The service worker is public/notify-sw.js. It owns three lifecycle handlers and reports each as a browser event back to the dashboard's own /api/webpush-notifications/events/browser route.
| Handler | What it does | Event posted |
|---|---|---|
push | Parses the payload, calls self.registration.showNotification(title, options) | displayed |
notificationclick | Closes the toast, then opens the target — branch below | clicked (same-origin only) |
notificationclose | Reports dismissal | closed |
Every posted event carries event_id (random), delivery_id, interaction_token, event_type, occurred_at, and (on click) action_id. The fetch uses keepalive: true and credentials: 'same-origin' so it survives the page going away.
The click branch (same-origin vs cross-origin)
This is the key safety detail. Where the click is recorded depends on the target's origin.
For a cross-origin target the worker does not post clicked itself. It opens a handoff URL:
/api/webpush-notifications/open?target=...&delivery_id=...&interaction_token=...&action_id=...
The open route records the clicked event server-side and 307-redirects to the validated target. This keeps click attribution accurate while never letting an attacker turn the redirect into an open redirector (see the host allow-list below).
Backend route handlers
All handlers live under src/app/api/webpush-notifications/. Each verifies an admin session, reads the tenant via X-Notify-Client-Id, then calls Notify with one of the two helpers.
| Route file | Method + Notify path | Token | Extra headers |
|---|---|---|---|
client-config/route.ts | GET /api/v1/webpush/client-config | push-platform | X-Source-App |
subscriptions/route.ts | POST /api/v1/service/webpush/subscriptions | push-platform | X-Source-App |
subscriptions/deactivate/route.ts | POST /api/v1/service/webpush/subscriptions/deactivate | push-platform | X-Source-App |
events/browser/route.ts | POST /api/v1/service/webpush/events/browser | push-platform | X-Source-App |
open/route.ts | POST /api/v1/service/webpush/events/browser, then 307 redirect | push-platform | X-Source-App |
activity/route.ts | GET /api/v1/webpush/activity | editor | X-Notify-Client-Id |
[id]/route.ts | GET /api/v1/webpush/notifications/:id | editor | X-Notify-Client-Id |
send/route.ts | POST /api/v1/webpush/send | editor | X-Notify-Client-Id, X-Admin-User |
preview/route.ts | POST /api/v1/templates/web-push/preview | editor | X-Admin-User |
Notes on the data each route fills in:
subscriptions/route.tssendstenant_idfrom theX-Notify-Client-Idheader andplayer_id: 0(the smoke-test value), withendpointandkeyscoming from the browser'sPushSubscription.open/route.tsvalidates the target host againstWEBPUSH_ALLOWED_TARGET_HOSTSbefore redirecting. With no list configured it redirects nowhere — it cannot be abused as an open redirector.
Frontend subscribe flow
Driven by src/components/notifications/webpush/WebPushTestPanel.tsx, using the helpers in src/lib/webpushNotifications/serviceWorker.ts.
- Fetch the VAPID public key and config from
/api/webpush-notifications/client-config. - Register the worker:
navigator.serviceWorker.register('/notify-sw.js'). - Subscribe:
subscribeToPush(vapidKey)callspushManager.subscribe({ userVisibleOnly: true, applicationServerKey }). - POST the resulting subscription (
endpoint+keys) to/api/webpush-notifications/subscriptions, which forwards it to Notify.
Environment
All Notify tokens are server-only secrets — they are read only inside route handlers and the server fetch helpers, never shipped to the browser.
| Var | Secret? | Purpose |
|---|---|---|
NOTIFY_API_URL | No | Base URL of the Notify API the helpers call. |
NOTIFY_PUSH_PLATFORM_TOKEN | Yes | Bearer token for notifyPushPlatformFetch (subscriptions + browser events). |
NOTIFY_EDITOR_TOKEN | Yes | Bearer token for notifyFetch (/api/v1/tenants/, activity, send, preview, templates). |
ADMIN_AUTH_SECRET | Yes | Signs and verifies the dashboard admin session cookie checked by requireAdminRouteContext(). |
WEBPUSH_ALLOWED_TARGET_HOSTS | No | Comma-separated host allow-list the open route checks before a cross-origin redirect. |
What to copy vs not
- Copy the server bridge pattern: same-origin route handlers that hold the Notify token server-side and forward to Notify. Never expose a Notify token to the browser.
- Copy the service-worker shape: the three lifecycle handlers and the browser-event reporting with
keepalive: true. - Copy the
opencross-origin handoff: recordclickedserver-side and 307-redirect, with the target host checked against your allow-list. - Do NOT copy
player_id: 0. That is the admin smoke-test value. Resolve a real, positiveplayer_idfrom the signed-in player's session. - Align
WEBPUSH_ALLOWED_TARGET_HOSTSto your own click-target domains, or cross-origin redirects will go nowhere.