HTTP Entry Points
Status
Active
Date
2026-04-28
Owners
- Platform Backend
Last Verified Commit
56362a7a
Audience-Level Entry Surfaces
The Public Ingress column names the HTTP listener that the
audience actually reaches over the network. The Domain Owner
column names the service that hosts the route handlers and owns the
business logic. They diverge for game providers: callbacks land on
gateway (the only public-internet listener for provider callbacks;
agent_service and admin_service also bind non-loopback in compose
but their public ingress is the back-office UI, not provider traffic
— see
servers_v2/tests/test_compose_contract.py::test_loopback_bound_services_pin_host_ip_to_127_0_0_1
for the exclusion list) and are reverse-proxied to game_service,
which is loopback-only. Treat Domain Owner as the place to read
code; never expose a non-ingress service directly to the public
network.
| Audience | Surface | Public Ingress | Domain Owner | Notes |
|---|---|---|---|---|
| players | root legacy paths and /api/v1/* | gateway | gateway (with downstream fan-out) | compatibility-preserving edge for web clients |
| agents | top-level /agent/* plus legacy aliases (/user/*, /home/*, /withdraw/*, /player/*) | agent_service | agent_service | agent frontend does not go through gateway; agent_service has an empty api_prefix so routes are mounted at the top level — there is no /api/v1/agent/* namespace |
| back-office admins | /api/v1/*, /ws, /internal/meta/* | admin_service | admin_service | Legacy /api/admin/* surface is split by status: ADR-009-removed families (auth, pushbullet, shooter, web/{rules,faq,config}, legacy admin/agents/agent-withdrawals/meta v2) return 404; un-migrated families that servers_v2 does not replace (/api/admin/{role,menu,config,i18n,bi}/*, /coin/*, /api/admin/legacy-catchall/*) return 410 Gone with Sunset + Deprecation headers via app/api/routes/legacy_sunset.py. See docs/architecture/migration-readiness.md and docs/runbooks/legacy-middle-server-retirement.md for the per-prefix posture. |
| game providers | provider callback prefixes (/HO, /mg, /wc, /bti, /bt1, /splus, /digitain) | gateway (proxies to game_service) | game_service | game_service is bound to 127.0.0.1 only -- gateway is the public listener. Callback auth and provider semantics live in game_service. /integration/* is the internal backing API used by gateway's /api/v1/game/* proxy routes (player surface) and is not provider-facing; it must never be exposed publicly. |
| internal service callers | /internal/* route families | (none, internal only) | owner service | protected by X-Internal-Service-Token; never reachable from the public network. |
Player Entry Surface
gateway is the only player-facing HTTP edge in servers_v2.
Primary route families:
/api/v1/user/*/api/v1/game/*/api/v1/finance/*/api/v1/records/*/api/v1/common/*- legacy root aliases such as
/user/*,/game/*,/finance/*
Compatibility evidence:
servers_v2/gateway/tests/test_player_route_contracts.pyservers_v2/gateway/tests/test_legacy_routes.py
Agent Entry Surface
agent_service serves the agent frontend directly. The service has an
empty api_prefix (servers_v2/agent_service/app/core/settings/app.py)
so routes are mounted at the top level — there is no /api/v1/
namespace.
Primary route families (mounted via app/api/routes/api.py):
/agent/*— auth (/agent/login,/agent/forget, …)/agent/home/*— dashboard/agent/withdraw/*— withdrawals (/agent/withdraw/init,/agent/withdraw/add)/agent/player/*— downstream-player views/agent/sub/*— sub-agent management/agent/bank/*— bank cards/agent/messages/*— messages/agent/coupon/*— coupons (/agent/coupon/grant,/agent/coupon/recycle)/agent/provider/*— game providers/agent/common/*— shared endpoints/agent/recovery/*— SMS-driven password reset
Legacy aliases (mounted from app/api/routes/legacy_routes.py at the top
level, include_in_schema=False) preserve paths the older agent frontend
already calls:
/user/login,/user/refresh_token,/user/forget,/user/top_info,/user/info,/user/edit/password,/user/edit/bank/*,/user/edit/phone/*/home/*,/withdraw/*,/player/*/provider/*(list / percentage / sub/percentage / income)/messages/*(list /{id}/ read / unread/count)/bank/list,/common/agent_min_withdrawal_limit/coupon/*(grant / recycle / logs / recycle/logs)- additional
/agent/*legacy reporting endpoints (/agent/agentStatistic,/agent/agentStatisticDaily,/agent/statisticDaily) distinguished from the new/agent/sub/*namespace by suffix
For the authoritative current list, regenerate the OpenAPI snapshot with
python3 tools/openapi/export_openapi.py and inspect
docs/reference/openapi/agent_service.json.
Compatibility evidence:
servers_v2/agent_service/tests/test_agent_integration.pyservers_v2/agent_service/tests/test_legacy_auth_flows.pyservers_v2/agent_service/tests/test_legacy_withdraw_flows.py
Admin Entry Surface
admin_service is the only intended back-office HTTP edge in servers_v2.
Primary route families:
/api/v1/*operational APIs/wsreal-time top-info websocket/internal/meta/*internal synchronization hooks
Removed by ADR-009 (now respond 404):
/api/admin/user/*legacy admin auth and session compatibility/api/admin/pushbullet/*and/api/admin/shooter/*recon compatibility/api/admin/web/rules/*,/api/admin/web/faq/*,/api/admin/web/config/*legacy web CMS compatibility- legacy admin v2 paths previously served by
legacy_admin_v2.py,legacy_agents_v2.py,legacy_agent_withdrawals_v2.py, andlegacy_meta_v2.py
Un-migrated legacy middle_server families (return 410 Gone with
Sunset + Deprecation headers, mounted via
app/api/routes/legacy_sunset.py; not 404 — the loud envelope
makes forgotten callers fail visibly rather than look like a deployment
glitch):
/api/admin/role/*— role management/api/admin/menu/*— menu management/api/admin/config/*— legacy config editing/api/admin/i18n/*— i18n management/api/admin/bi/*— BI push tooling/coin/*— coin-exchange passthrough/api/admin/legacy-catchall/{path:path}— replacement stub for the retiredPOST /{path:path}generic forwarder
Per-prefix ownership and migration posture are tracked in
docs/runbooks/legacy-middle-server-retirement.md;
the readiness matrix lives in
docs/architecture/migration-readiness.md.
Important note:
servers_v2intentionally does not reproduce the old genericmiddle_servercatch-allPOST /{path:path}forwarding behavior.- admin compatibility in v2 is explicit and route-by-route, not a broad proxy.
Provider Entry Surface
game_service owns business logic for the provider callback prefixes
listed below. Public-internet ingress for these prefixes is the
gateway listener (added to gateway's PUBLIC_PATH_PREFIXES allowlist
in servers_v2/gateway/app/middleware/auth.py so gateway forwards them
to game_service without a player JWT). game_service itself is bound
to 127.0.0.1 in compose; never expose its host port:
/HO/*/mg/*/wc/*/bti/*/splus/*/bt1/*/digitain/*
/integration/* is not in this list and is not provider-facing.
It is the internal backing API for gateway's player-side /api/v1/game/*
routes (e.g. /api/v1/game/list → /integration/games); see
servers_v2/gateway/CLAUDE.md for the full proxy table. Reachable only
over the loopback bind.
Compatibility evidence:
servers_v2/game_service/tests/test_legacy_contracts.py
Brand Resolution At The Edge
Per ADR-009, every external entry surface resolves a brand_id before
domain code runs:
-
gatewaycalls_extract_domain(request)to read the Host or Origin header, then looks up the Redis mapsdomain:agent:{host}anddomain:level:{host}. Both maps now resolve to a value containingbrand_id. The resolved brand is attached torequest.state.brand_idand propagated to downstream services via theX-Brand-Idrequest header. -
agent_serviceresolves brand from the agent frontend domain the same way; theagent_brandallow list determines whether the resolved brand is permitted for the authenticated agent. -
admin_serviceis brand-global only for catalog/global-control routes. Brand-scoped operational routes ship the operating brand either in the URL path (most writes, e.g.PUT /brands/{brand_id}/providers,PATCH /brands/{brand_id}/config/{key}) or in the request body (catalog-style allow-list writes whose primary key is(agent_id, brand_id)rather than the brand alone — today this isPOST /agent-brand). TheBrandResolutionMiddlewarepopulatesrequest.state.brand_idfrom theX-Brand-Idheader (with a Redis domain-map fallback). Two route-level dependencies inapp/api/deps.pyenforce the same matrix against the state brand:require_brand_path_matches_request(T17) — path-brand surface; attached viaDepends(...)on every brand-scoped path route.enforce_brand_body_matches_request— body-brand surface; called as a plain function from the route handler with the body'sbrand_idfield, because FastAPI'sDependscannot introspect parsed body shape. Used today byPOST /agent-brand.
The matrix below applies identically to both surfaces. "Candidate brand_id" means the path brand_id or the body's
brand_idfield depending on which dep is in play.state.brand_idabsent and operator is NOTsuper_admin: fail closed with the standard brand-required envelope (status=54).state.brand_idabsent and operator ISsuper_admin: allowed to act on the candidate brand (break-glass).state.brand_id == candidate brand_id: allowed.state.brand_id != candidate brand_idand operator ISsuper_admin: allowed and audited viaadmin_brand_header_jwt_mismatch_total{outcome="super_admin_allowed"}.state.brand_id != candidate brand_idand operator is NOTsuper_admin: hard-rejected403and audited viaadmin_brand_header_jwt_mismatch_total{outcome="rejected"}.
Error envelopes carry the surface name (
brand path check rejectedvsbrand body check rejected) so an operator reading a 403 can tell at a glance which dep fired.This is in addition to the existing CR6-D-M2 cross-check in
get_current_adminwhich compares header vs JWTbrand_id. -
game_serviceresolves brand from the outbound account namespace ({brand_code}_{account}) reverse-parsed from provider callback payloads. -
recon_servicehas no public HTTP callback surface; reconciliation signals (SMS, pushbullet) reachrecon_servicethrough internal collection paths and brand is resolved from the matchedplayer_deposit.brand_id. There is no edge brand resolution to perform on those signals. -
/internal/*callers must propagateX-Brand-Id. Internal handlers reject requests that arrive without a brand context for brand-scoped operations.
After login, JWT claims must contain brand_id. gateway rejects requests
whose JWT brand does not match the brand resolved from the request domain.
Internal Entry Surface
Internal service routes are currently grouped by owner:
player_service:/internal/players/*,/internal/common/*wallet_service:/internal/wallet/*rolling_service:/internal/rolling/*promotion_service:/internal/promotions/*recon_service:/internal/recon/*
Protection and exceptions:
- owner-service
/internal/*routes are intended for service-to-service callers only gatewayandadmin_serviceare responsible for preserving external compatibility while attaching per-caller internal auth (X-Caller-ServiceplusINTERNAL_SERVICE_TOKEN_<CALLER>); the legacy sharedINTERNAL_SERVICE_TOKENis only a Stage A/B fallback and must be absent or empty after the Stage C hard flip- explicit external webhook paths remain the exception, for example
wallet_servicePlisio callback handling that still lives under/internal/wallet/plisio/callbackfor compatibility reasons