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.
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) | Auth | Forwards to notify |
|---|---|---|
GET /player/data/webpush/client-config | session JWT | GET /api/v1/webpush/client-config |
POST /player/data/webpush/subscriptions | session JWT | POST /api/v1/service/webpush/subscriptions |
POST /player/data/webpush/subscriptions/deactivate | session JWT | POST /api/v1/service/webpush/subscriptions/deactivate |
POST /public/webpush/events/browser | none (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 field | Source (server-side only) | Type sent to notify |
|---|---|---|
tenant_id | req.locals.player_auth.tenant_id | JSON string |
player_id | req.locals.player_auth.player_id | JSON 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": "…" }
}
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
tokenin the policy. The env-var name on the proxy side is the proxy's own choice; only the value must match. allowed_routesentries are matched as exact, case-sensitive"METHOD /path"strings (or"*"). An unknown route string fails at config load and makes Notify/healthreturn 503.X-Source-Appis enforced unlessallowed_source_appsis["*"]. The value the proxy sends must be in the list.- Tenant scope is taken from the JSON body
tenant_idon subscriptions/deactivate; browser-events derive the tenant from the interaction token;client-configskips the tenant check. Withtenant_idsset, the proxy's injectedtenant_idmust be in the list. Omittingtenant_idsallows 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 — push → showNotification + displayed, notificationclick → clicked + open, notificationclose → closed — 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: theX-Source-Appvalue (casino-ui) is inallowed_source_apps, and all four routes are inallowed_routes. -
tenant_idand a positive numericplayer_idare injected server-side from the session JWT; any identity in the browser request body is ignored. - The injected
tenant_id+player_idat subscribe time match what the trigger flow passes tosend_webpush(otherwise delivery resolves zero subscriptions). -
sw.jsis served at the site root over HTTPS, and/public/webpush/events/browserallows cross-originPOSTfrom casino origins. - Platform team confirms
WEB_PUSH_DRY_RUN=falseon the Notify sender (integrators do not set this) and setsNOTIFY_WEBPUSH_ALLOWED_TARGET_HOSTSfor cross-origin click targets.
See also
| Page | What it covers |
|---|---|
| Integration Guide | The generic browser + backend protocol (full SW handlers, subscribe flow, events) |
| API Reference | Every Notify route, request/response, header, error code |
| Configuration & Secrets | Tokens, caller policy, VAPID, allowed target hosts |
| Reference: Dashboard | The canonical working integration and /open pattern |