Skip to main content

Casino Integration

This page describes how Notify web push reaches casino players in the online-services-front React + Vite app. It covers only what is casino-specific: the React/Vite service worker, the online-services-player Node proxy that bridges the browser to notify-api, and the tenant_id / player_id mapping. The generic protocol (service worker handlers, subscribe flow, delivered payload, interaction events, error model) is identical to the Integration Guide — this page links out to it rather than re-deriving it.

note

The casino web-push client is being implemented. The online-services-front client (push UI, pushClient.ts, sw.js) and the online-services-player proxy routes are in progress; the contract below — proxy paths, identity injection, and headers — is the target both sides build against. The generic protocol lives in the Integration Guide, and Notify's routes/headers in the API Reference.

Target architecture

The bridge between the casino browser and Notify is the online-services-player Node service — the same gateway that already serves the in-app notification inbox. There is no PHP backend in this path. The proxy is the single trusted holder of the Notify push-platform token, injects the player's identity from the session JWT, and forwards every call server-to-server to notify-api.

The React app talks only to its own player proxy. Authenticated config/subscribe/deactivate calls carry the player's session Bearer JWT to /player/data/webpush/*; the proxy validates the JWT, injects identity plus the push-platform credentials, and forwards to notify-api. The service worker — which cannot hold secrets — reports lifecycle events to a public proxy route (/public/webpush/events/browser), where Notify's opaque interaction_token is what authenticates the event. This mirrors how the dashboard's Next backend brokers Notify — see Reference: Dashboard.

Proxy route table

These paths are the fixed contract. The browser-facing column is what the casino frontend calls; the proxy forwards each to the matching notify-api route after attaching push-platform headers.

Browser-facing route (player proxy)AuthForwards to notify
GET /player/data/webpush/client-configsession JWTGET /api/v1/webpush/client-config
POST /player/data/webpush/subscriptionssession JWTPOST /api/v1/service/webpush/subscriptions
POST /player/data/webpush/subscriptions/deactivatesession JWTPOST /api/v1/service/webpush/subscriptions/deactivate
POST /public/webpush/events/browsernone (public)POST /api/v1/service/webpush/events/browser

The three /player/data/webpush/* routes sit behind the same player-auth middleware as the in-app inbox. The /public/webpush/events/browser route is intentionally unauthenticated: the service worker has no session, and Notify verifies the event with the interaction_token it issued in the delivery payload. Ensure the public route allows cross-origin POST from casino origins.

Identity mapping

The proxy resolves identity from req.locals.player_auth and injects it into the notify body. The frontend must not send tenant_id or player_id — those fields are read client-side only to gate the subscribe UI, never trusted as input.

Notify fieldSource (server-side only)Type sent to notify
tenant_idreq.locals.player_auth.tenant_idJSON string
player_idreq.locals.player_auth.player_idJSON integer, >= 0 (coerce; may arrive as BigInt)

player_id is required and must be a non-negative integer. 0 is reserved for internal dashboard smoke tests (lumio_admin_dashboard); casino players are always positive. Reject guests and missing players at the proxy.

Notify builds the storage key from these two fields: it stores user_id = "<tenant_id>_<player_id>". The subscribe body the proxy sends looks like:

{
"tenant_id": "60188",
"player_id": 42753,
"endpoint": "…",
"keys": { "p256dh": "…", "auth": "…" }
}
warning

Delivery requires identity alignment. When a trigger flow calls send_webpush, Notify resolves subscriptions by the tenant_id + numeric player_id passed at send time and matches them against what was registered. Delivery succeeds only if the tenant_id and player_id the proxy injected at subscribe time equal the tenant_id + player_id the send path targets. A mismatch registers subscriptions fine but silently resolves zero subscriptions at delivery. Confirm the proxy's tenant_id matches the casino's client/tenant id and the value triggers send to.

Deactivate matches on the full identity tuple, so the proxy sends { tenant_id, player_id, endpoint } (not the endpoint alone) on /player/data/webpush/subscriptions/deactivate.

Auth: proxy to notify

The proxy is the only holder of the push-platform credentials. On every forwarded call it adds:

Authorization: Bearer <push-platform token>
X-Source-App: casino-ui

The token and source-app value come from the proxy's own config/secret store, never from the browser. casino-ui is the source-app identifier the casino caller uses (there is no online-services-front source app in Notify); the proxy must send the same value that the caller policy allows.

Caller policy

Notify authorizes these calls against the NOTIFY_API_PUSH_PLATFORM_CALLERS_JSON policy. It is a {"callers":[…]} envelope; each caller entry requires caller_id, token, allowed_source_apps, and allowed_routes, with an optional tenant_ids scope:

{
"callers": [
{
"caller_id": "online-services-player",
"token": "<generated>",
"allowed_source_apps": ["casino-ui"],
"allowed_routes": [
"GET /api/v1/webpush/client-config",
"POST /api/v1/service/webpush/subscriptions",
"POST /api/v1/service/webpush/subscriptions/deactivate",
"POST /api/v1/service/webpush/events/browser"
],
"tenant_ids": ["60188"]
}
]
}

Notes that have bitten integrations:

  • The token value the proxy sends as the Bearer token must equal a token in the policy. The env-var name on the proxy side is the proxy's own choice; only the value must match.
  • allowed_routes entries are matched as exact, case-sensitive "METHOD /path" strings (or "*"). An unknown route string fails at config load and makes Notify /health return 503.
  • X-Source-App is enforced unless allowed_source_apps is ["*"]. The value the proxy sends must be in the list.
  • Tenant scope is taken from the JSON body tenant_id on subscriptions/deactivate; browser-events derive the tenant from the interaction token; client-config skips the tenant check. With tenant_ids set, the proxy's injected tenant_id must be in the list. Omitting tenant_ids allows all tenants.

See Configuration & Secrets for the full caller-policy shape and VAPID/target-host settings.

Frontend (React + Vite)

Service worker at the site root

sw.js lives in websources/public/ so Vite serves it at the site root (/sw.js); a service worker can only control pages at or below its own path. Its handlers — pushshowNotification + displayed, notificationclickclicked + open, notificationcloseclosed — are identical to the Integration Guide. The SW posts every interaction event to ${playerApiUrl}/public/webpush/events/browser.

Subscribe flow

The push client fetches the VAPID public key from the proxy (never embedded in the bundle), subscribes via PushManager, then POSTs the raw subscription to the proxy. Authenticated calls carry the player's session Authorization: Bearer <access_token> from the auth state — the same header the in-app inbox uses. The body carries only the subscription (endpoint, keys); identity is injected by the proxy.

// online-services-front — push client (SKETCH; full handlers: Integration Guide)
export async function subscribeToNotify(accessToken: string): Promise<void> {
const reg = await navigator.serviceWorker.ready;

// 1. VAPID public key from our own player proxy, not the bundle.
const cfg = await fetch(`${playerApiUrl}/player/data/webpush/client-config`, {
headers: { Authorization: `Bearer ${accessToken}` },
}).then((r) => r.json());
const applicationServerKey = urlBase64ToUint8Array(cfg.public_key);

// 2. Browser-native subscribe.
const subscription = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey,
});

// 3. Send only the subscription. No tenant_id / player_id — the proxy
// injects identity from the session JWT.
const { endpoint, keys } = subscription.toJSON();
await fetch(`${playerApiUrl}/player/data/webpush/subscriptions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({ endpoint, keys }),
});
}

Cross-origin click handling

If a push target_url points to a different origin than the casino site, the service worker cannot record the clicked event and open the link atomically across origins. The remedy mirrors the dashboard /open handoff: a same-origin proxy route that allowlists target_url, records the clicked event (forwarding the opaque interaction_token), then redirects to the allowlisted URL. This is a follow-up rather than part of the initial subscribe/deliver path. See the Reference: Dashboard /open pattern.

Rollout checklist

  • The player proxy is the only holder of the push-platform token — never the browser. VAPID private-key provisioning is Notify platform-side only.
  • Casino is registered as a caller in NOTIFY_API_PUSH_PLATFORM_CALLERS_JSON: the X-Source-App value (casino-ui) is in allowed_source_apps, and all four routes are in allowed_routes.
  • tenant_id and a positive numeric player_id are injected server-side from the session JWT; any identity in the browser request body is ignored.
  • The injected tenant_id + player_id at subscribe time match what the trigger flow passes to send_webpush (otherwise delivery resolves zero subscriptions).
  • sw.js is served at the site root over HTTPS, and /public/webpush/events/browser allows cross-origin POST from casino origins.
  • Platform team confirms WEB_PUSH_DRY_RUN=false on the Notify sender (integrators do not set this) and sets NOTIFY_WEBPUSH_ALLOWED_TARGET_HOSTS for cross-origin click targets.

See also

PageWhat it covers
Integration GuideThe generic browser + backend protocol (full SW handlers, subscribe flow, events)
API ReferenceEvery Notify route, request/response, header, error code
Configuration & SecretsTokens, caller policy, VAPID, allowed target hosts
Reference: DashboardThe canonical working integration and /open pattern