An investigation of the game client's feature toggle and configuration system uncovered a broader issue: business rule authorization (market exits, self-exclusion, purchase limits) is enforced almost entirely client-side. Backend services accept requests from any authenticated user regardless of market status, self-exclusion state, or purchase limits.
This document summarizes the findings and proposes a centralized permission service that all backend services call before allowing restricted actions.
A full design document covering the complete toggle inventory, permission model, Kotlin implementation, test strategy, and phased rollout plan is in progress and will be submitted as a PR to gp-game-client (docs/features/feature-toggle-and-config-migration.md).
The game client determines what a user can do based on their market, account status, and self-exclusion state. It hides UI elements accordingly. But the backend services behind those UI elements do not independently enforce these restrictions.
A user who calls backend APIs directly with a valid JWT can bypass all client-side restrictions.
| Restricted behavior | Client enforces? | Server enforces? | Risk |
|---|---|---|---|
| SC purchase in restricted market | Yes (hides UI) | No -- pok-store accepts claims without market check | HIGH |
| SC gameplay in restricted market | Yes (hides currency) | No -- gp-poker, blackjack, slots do not check | HIGH |
| Self-excluded user playing | Yes (modal) | No -- game servers do not check | HIGH |
| SC redemption in restricted market | Yes (hides UI) | No -- pam-pay-hub-app does not check | HIGH |
| Purchase limit exceeded | Yes (client calculates) | No -- pok-store does not call pok-player-account | HIGH |
| SC vault claim in restricted market | Yes (hides UI) | Partial -- checks account status, not market | MEDIUM |
| Self-excluded user in tournaments | Partial (modal) | Reactive only -- unregisters after exclusion, does not prevent new registration | MEDIUM |
| Bank verification before redemption | Yes (hides UI) | No -- pam-pay-hub-app does not check | MEDIUM |
pok-store: Auth (JWT/API key), package active/expired, claim limit. No market, region, phase, or purchase limit checks. A user in a blocked market can claim SC packages.
pok-player-account: Vault claims check user account status (BLOCKED, REMOVED, MUST_KYC_NOW, MUST_SOW_NOW) but not market exit phases or regional restrictions. Purchase limits are tracked but not enforced at purchase time (pok-store does not call pok-player-account).
gp-poker: playerJoined() adds the player to the table immediately. No market, phase, currency, or self-exclusion checks. Tournament registration only checks RUG conflicts, not self-exclusion.
pok-blackjack-server: Has MarketUtils and PlayerFeatureConfig with market/phase config, but only uses them for error message formatting after a wallet failure, not to prevent game initiation.
pok-slots-server: No market restriction or permission checks found.
pam-pay-hub-app: Redemption session creation checks player profile and token balance. No market restriction, bank verification, or regional checks.
pok-customer: Collects and stores residential address. Has a per-user account_restrictions table. No market-based enforcement on any endpoint.
A single service that answers "what can this user do?" All market data, phase logic, self-exclusion status, and purchase limit state are evaluated in one place. Backend services call it before allowing restricted actions.
Game Client (browser)
|
| Auth0 JWT
v
Express web tier --calls--> Permission Service --> { permissions, restriction }
| ^ ^ ^
| injects permissions | | |
v (for UI rendering) | | |
Browser renders UI | | |
| | | |
| Auth0 JWT | | |
v | | |
pok-store ------calls-----------+ | |
gp-poker -------calls------------------+ |
pam-pay-hub-app --calls-----------------------+
(etc.)
The Auth0 JWT provides identity (who is this user). The permission service provides authorization (what can this user do). Two questions, two systems.
16 action-oriented permissions. Each maps to a concrete backend operation.
| Permission | Description |
|---|---|
REGISTER |
Can register a new account |
LOGIN |
Can log in |
PURCHASE |
Can make any purchase (within limits) |
PURCHASE_SC |
Can purchase Sweeps Coin packages |
PLAY_POKER |
Can play any poker (GC or SC) |
PLAY_SC_POKER |
Can join SC poker tables/tournaments |
PLAY_CASINO |
Can play any casino game (GC or SC) |
PLAY_SC_CASINO |
Can play SC casino/slots games |
REDEEM_SC |
Can redeem SC for cash |
CANCEL_REDEMPTION |
Can cancel a pending SC redemption |
CLAIM_SC_VAULT |
Can claim SC vault |
CLAIM_GC_VAULT |
Can claim GC vault |
RECEIVE_SC_BONUS |
Can receive SC daily bonus, verification bonus, gift spins, prize draw |
RECEIVE_GC_BONUS |
Can receive GC daily bonus |
USE_BONUS_CODES |
Can redeem bonus codes |
VIEW_SC_HISTORY |
Can view SC transaction and gameplay history |
Three parent-child relationships provide hierarchy:
PLAY_POKER->PLAY_SC_POKER(self-exclusion denies the parent, market restrictions deny the child)PLAY_CASINO->PLAY_SC_CASINOPURCHASE->PURCHASE_SC(purchase limits deny the parent, market restrictions deny the child)
GET /permissions/{userId}?geoLocationState=ny
Response:
{
"permissions": {
"PURCHASE_SC": false,
"PLAY_SC_POKER": false,
"REDEEM_SC": true,
...
},
"restrictions": [
{
"type": "promo-play",
"phase": 2,
"market": { "isoEntry": "NY", "regionName": "New York" },
"denies": ["PURCHASE_SC", "PLAY_SC_POKER", "PLAY_SC_CASINO", ...]
}
]
}
Single-action query for backend services:
GET /permissions/{userId}?action=PURCHASE_SC
Response:
{
"allowed": false,
"reason": "Promo play phase 2 - SC purchases blocked"
}
Each service adds a single call before allowing a restricted action: extract userId from JWT, call permission service, check the relevant permission, reject with 403 if denied.
| Service | Restricted actions | Permission(s) to check |
|---|---|---|
| pok-store | SC package purchase/claim | PURCHASE_SC AND PURCHASE |
| pok-player-account | SC vault claim | CLAIM_SC_VAULT |
| pok-player-account | GC vault claim | CLAIM_GC_VAULT |
| pam-pay-hub-app | Create redemption session | REDEEM_SC |
| pam-pay-hub-app | Cancel redemption | CANCEL_REDEMPTION |
| gp-poker | Join table (any currency) | PLAY_POKER, and PLAY_SC_POKER if SC table |
| gp-poker | Register for tournament | PLAY_POKER, and PLAY_SC_POKER if SC tournament |
| pok-blackjack-server | Start game | PLAY_CASINO, and PLAY_SC_CASINO if SC game |
| pok-slots-server | Start game | PLAY_CASINO, and PLAY_SC_CASINO if SC game |
| pok-customer | Registration | REGISTER |
pok-customer is the proposed home. It already stores the user's residential address, has an account_restrictions table with per-user phase data, has auth infrastructure (JWT validation, API keys for service-to-service), and is already called by pok-player-account for user status checks.
Alternatively, a standalone service if pok-customer's responsibilities should not grow.
Backend enforcement is added wave by wave, prioritized by risk and regulatory exposure.
Wave 1 -- Self-exclusion (regulatory, zero server enforcement today):
- gp-poker: Check
PLAY_POKERon table join and tournament registration - pok-blackjack-server: Check
PLAY_CASINOon game start - pok-slots-server: Check
PLAY_CASINOon game start
Wave 2 -- Market restrictions on financial actions:
4. pok-store: Check PURCHASE_SC and PURCHASE before SC package claims
5. pam-pay-hub-app: Check REDEEM_SC before redemption session creation
6. pok-player-account: Check CLAIM_SC_VAULT before SC vault claims
Wave 3 -- Market restrictions on gameplay:
7. gp-poker: Add PLAY_SC_POKER check (SC table/tournament distinction)
8. pok-blackjack-server: Add PLAY_SC_CASINO check
9. pok-slots-server: Add PLAY_SC_CASINO check
Wave 4 -- Registration and remaining gaps:
10. pok-customer: Check REGISTER for new users in blocked markets
- API access control (JWT validation, role-based checks like
canClaimPackage) stays where it is. The permission service adds business rule authorization alongside existing access control, not instead of it. - Client-side UI rendering still happens. The client receives the permission response and uses it for UI visibility decisions. The difference is that the client no longer evaluates the rules -- it receives the result.
- Auth0 JWT issuance is unchanged. The JWT provides identity. The permission service provides authorization.