Skip to main content

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.

Admin smoke-test surface, not a player app

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:

HelperToken sourceAlways addsUsed by
notifyPushPlatformFetchNOTIFY_PUSH_PLATFORM_TOKENAuthorization: Bearer, X-Source-App: lumio_admin_dashboardsubscription + browser-event paths
notifyFetchNOTIFY_EDITOR_TOKENAuthorization: Bearer, plus X-Notify-Client-Id / X-Admin-User when passedactivity, 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.

HandlerWhat it doesEvent posted
pushParses the payload, calls self.registration.showNotification(title, options)displayed
notificationclickCloses the toast, then opens the target — branch belowclicked (same-origin only)
notificationcloseReports dismissalclosed

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 fileMethod + Notify pathTokenExtra headers
client-config/route.tsGET /api/v1/webpush/client-configpush-platformX-Source-App
subscriptions/route.tsPOST /api/v1/service/webpush/subscriptionspush-platformX-Source-App
subscriptions/deactivate/route.tsPOST /api/v1/service/webpush/subscriptions/deactivatepush-platformX-Source-App
events/browser/route.tsPOST /api/v1/service/webpush/events/browserpush-platformX-Source-App
open/route.tsPOST /api/v1/service/webpush/events/browser, then 307 redirectpush-platformX-Source-App
activity/route.tsGET /api/v1/webpush/activityeditorX-Notify-Client-Id
[id]/route.tsGET /api/v1/webpush/notifications/:ideditorX-Notify-Client-Id
send/route.tsPOST /api/v1/webpush/sendeditorX-Notify-Client-Id, X-Admin-User
preview/route.tsPOST /api/v1/templates/web-push/previeweditorX-Admin-User

Notes on the data each route fills in:

  • subscriptions/route.ts sends tenant_id from the X-Notify-Client-Id header and player_id: 0 (the smoke-test value), with endpoint and keys coming from the browser's PushSubscription.
  • open/route.ts validates the target host against WEBPUSH_ALLOWED_TARGET_HOSTS before 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.

  1. Fetch the VAPID public key and config from /api/webpush-notifications/client-config.
  2. Register the worker: navigator.serviceWorker.register('/notify-sw.js').
  3. Subscribe: subscribeToPush(vapidKey) calls pushManager.subscribe({ userVisibleOnly: true, applicationServerKey }).
  4. 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.

VarSecret?Purpose
NOTIFY_API_URLNoBase URL of the Notify API the helpers call.
NOTIFY_PUSH_PLATFORM_TOKENYesBearer token for notifyPushPlatformFetch (subscriptions + browser events).
NOTIFY_EDITOR_TOKENYesBearer token for notifyFetch (/api/v1/tenants/, activity, send, preview, templates).
ADMIN_AUTH_SECRETYesSigns and verifies the dashboard admin session cookie checked by requireAdminRouteContext().
WEBPUSH_ALLOWED_TARGET_HOSTSNoComma-separated host allow-list the open route checks before a cross-origin redirect.

What to copy vs not

Copy the shape, not the smoke-test identity
  • 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 open cross-origin handoff: record clicked server-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, positive player_id from the signed-in player's session.
  • Align WEBPUSH_ALLOWED_TARGET_HOSTS to your own click-target domains, or cross-origin redirects will go nowhere.

See also