Service map¶
The complete local stack as wired by /home/ubuntu/ebit/docker-compose.yml.
Five NestJS apps + two Next.js FEs + Postgres + two Redis + RabbitMQ +
the four-process observability stack (OTel Collector / Jaeger / Prometheus /
Loki / Grafana).
Generated 2026-04-25 from
docker-compose.yml,CLAUDE.md, the four referenced memories, and cross-checked against the existing flow docs in../flows/.
Top-level edges¶
Split into two pictures because client (player) and admin/ops traffic
hit different surfaces of ebit-api and live with different downstream
concerns. Both diagrams share the same infra column on the right;
they're separate so each can be scanned without crossing the other's
arrows.
A. Player path — ebit-fe ↔ ebit-api + ebit-rt + downstream¶
flowchart LR
subgraph cli["Player"]
user((Browser<br/>player))
end
subgraph fe["Frontend"]
ebit_fe["ebit-fe :3000<br/>Next.js dropbet"]
end
subgraph apps["Player-serving Nest apps"]
direction TB
ebit_api["ebit-api :4000<br/>NestJS REST + Swagger"]
ebit_rt["ebit-rt :4001<br/>socket.io /events"]
ebit_sr["ebit-speed-roulette :4004<br/>roulette state machine"]
end
subgraph infra["Datastores + messaging"]
direction TB
ebit_db[("Postgres :5432<br/>ebit-db")]
redis_cache[("Redis :6379<br/>cache · pwd=cache")]
redis_bot[("Redis :6380<br/>bot · pwd=bot")]
rabbitmq["RabbitMQ :5672<br/>vhost ft<br/>(stubbed)"]:::stub
end
%% Player traffic in
user -- "HTTPS :3000" --> ebit_fe
ebit_fe -- "HTTP :4000" --> ebit_api
ebit_fe -- "WSS :4001" --> ebit_rt
%% Apps → infra
ebit_api --> ebit_db
ebit_rt --> ebit_db
ebit_sr --> ebit_db
ebit_api --> redis_cache
ebit_rt --> redis_cache
ebit_sr --> redis_cache
ebit_api --> redis_bot
ebit_api -. "disabled=true" .-> rabbitmq
%% Inter-app RPC — AF-2 trace gap (red)
ebit_api == "ExternalControllerClient<br/>NO traceparent" ==> ebit_sr
ebit_sr == "walletClient RPC<br/>NO traceparent" ==> ebit_api
ebit_api == "BalanceUpdated pub/sub<br/>NO traceparent" ==> ebit_rt
classDef stub fill:#eee,stroke:#999,color:#666,stroke-dasharray:5 5;
linkStyle 11,12,13 stroke:#cc0000,stroke-width:2.5px;
Red thick edges = inter-app RPC over Redis pub/sub.
traceparentis not propagated by Nest's microservice transport (AF-2 in../weaknesses-register.md) — callee spans appear as orphan traces in Jaeger.
B. Admin / Ops path — ebit-admin-fe + ebit-bo + orphan ebit-bj¶
flowchart LR
subgraph cli["Admin / Ops"]
admin((Admin / Ops))
end
subgraph fe["Frontend"]
ebit_admin_fe["ebit-admin-fe<br/>:5173 host / :3003 compose<br/>Vite + React 19 SPA"]
end
subgraph apps["Admin / Ops Nest apps"]
direction TB
ebit_api_admin["ebit-api :4000<br/>admin endpoints (PermissionGuard)"]
ebit_bo["ebit-bo :4003<br/>backoffice + Swagger<br/>(separate process, ops tooling)"]
ebit_bj["ebit-bj :4002<br/>orphan blackjack"]:::orphan
end
subgraph infra["Datastores"]
direction TB
ebit_db[("Postgres :5432<br/>ebit-db")]
redis_cache[("Redis :6379<br/>cache · pwd=cache")]
end
%% Admin traffic in
admin -- "HTTPS :5173 / :3003" --> ebit_admin_fe
ebit_admin_fe -- "HTTP :4000" --> ebit_api_admin
%% Apps → infra
ebit_api_admin --> ebit_db
ebit_bo --> ebit_db
ebit_api_admin --> redis_cache
ebit_bo --> redis_cache
%% Orphan ebit-bj — runs in compose but no in-repo callers
ebit_bj -. "no in-repo callers" .-> ebit_db
ebit_bj -. "no in-repo callers" .-> redis_cache
classDef orphan fill:#fff3cd,stroke:#a07000,color:#000;
ebit-api :4000appears in both diagrams because the same Nest process serves both surfaces (the player REST handlers and the admin-guarded/admin/*controllers).ebit-bois a separate Nest app on:4003for ops tooling — it has its own Swagger and route tree, used by internal operators (not byebit-admin-fe).ebit-bjruns on:4002but no in-repo client calls it; dropbet's blackjack flows route throughebit-api's/casino/games/house/blackjack/*instead (AF-4 in../weaknesses-register.md).
Legend:
- Solid arrow — active traffic on the local stack today.
- Dashed arrow — provisioned but no traffic. RabbitMQ is the only such
edge: apps/api/src/fast-track/rabbitmq/fast-track.rmq.module.ts:8 hard-codes
disabled = true and replaces the producer with a no-op stub
(per project_otel_microservice_transport_gap.md and the inline compose
comment that names all 11 silently-dropped call sites).
- Red link — Redis pub/sub edge that does not propagate W3C
traceparent. Spans on the callee side appear as orphan traces in Jaeger.
Per memory project_otel_microservice_transport_gap.md, this is systemic to
every @ExternalControllerClient edge — speed-roulette today, bj if ever
wired up.
- Yellow node — ebit-bj is architecturally orphaned (memory
project_ebit_bj_orphan.md). The container runs and exposes :4002, but no
in-repo FE reaches it — dropbet's blackjack UI calls
ebit-api's /casino/games/house/blackjack/* instead.
Observability sidecars¶
graph LR
apps[(All 5 NestJS apps<br/>+ ebit-fe + ebit-admin-fe)]
apps -->|OTLP/HTTP :4318| otelc[otel-collector :4317/4318]
filelog["/var/lib/docker/containers/<br/>JSON log files"] -->|filelog receiver| otelc
otelc -->|otlphttp/jaeger| jaeger[Jaeger v2 :16686<br/>Badger backend]
otelc -->|spanmetrics → prometheus| prom[Prometheus :9090]
otelc -->|loki exporter| loki[Loki :3100]
prom --> grafana[Grafana :3003]
loki --> grafana
jaeger --> grafana
The Collector is the single OTLP gateway — Jaeger no longer accepts OTLP
directly; it's reached over the bridge network only. See
tracing-flow.md for the full flow with processors
and connectors.
Edge inventory¶
Every directed edge in the diagrams above, with protocol, auth, and trace
state. "Traced" means a span chain reaches Jaeger with W3C traceparent
intact across the hop.
| From → To | Protocol | Port | Auth | Traced? | Notes / cross-ref |
|---|---|---|---|---|---|
| Browser → ebit-fe | HTTPS | 3000 | none (page) / access_token cookie (API proxy) |
yes — browser RUM via @vercel/otel |
../flows/dropbet-sign-in.md §4.0 |
| Browser → ebit-admin-fe | HTTPS | 3001 | access_token cookie |
yes — admin-fe runs --require @opentelemetry/auto-instrumentations-node/build/src/register.js (compose NODE_OPTIONS) |
host-network mode — admin-fe SSR middleware calls NEXT_PUBLIC_API_URL during request processing |
| Browser → ebit-rt | WSS | 4001 | socket_token (auth handshake / cookie / header) |
partial — per-emit send /events span only; no connection / handshake / message-handler spans |
../flows/rt-websocket.md §6.1; gap also called out in ../audits/perf-trace-coverage-audit.md §WS |
| ebit-fe → ebit-api | HTTP | 4000 | access_token cookie + JWT validation |
yes — @vercel/otel propagates traceparent, app.enableCors({...credentials:true}) reflects header |
../flows/dropbet-sign-in.md §4.0 |
| ebit-admin-fe → ebit-api | HTTP | 4000 | access_token cookie + JWT + RolesGuard + (SuperAdmin) OtpGuard |
yes — same as ebit-fe | ../flows/admin-sign-in.md |
| ebit-api → Postgres | TCP (Prisma) | 5432 | user/password from DATABASE_URL |
yes — @prisma/instrumentation 6.5.0 emits prisma:client:operation + prisma:engine:db_query (per project_otel_spanmetrics_connector.md) |
every Prisma call wrapped |
| ebit-rt → Postgres | TCP (Prisma) | 5432 | same | yes | reads User, writes UserSession |
| ebit-bo / ebit-sr / ebit-bj → Postgres | TCP | 5432 | same | yes for sr; yes-via---require for bj/bo |
bj/bo main.ts doesn't share the pre-imports chain — compose adds NODE_OPTIONS=--require .../register.js to bootstrap OTel |
| ebit-api → Redis (cache :6379) | RESP | 6379 | password cache |
yes — @opentelemetry/instrumentation-ioredis per-command spans |
BullMQ enqueue surfaces here as EVALSHA |
| ebit-rt → Redis (cache) | RESP | 6379 | password cache |
yes | ONLINE_USERS_KEY zadd / pub/sub subscribe |
| ebit-api → Redis (bot :6380) | RESP | 6380 | password bot |
yes | bot fleet queues — separate Redis to isolate bot traffic from cache hot path |
| ebit-api → RabbitMQ | AMQP | 5672 | rabbitmq/rabbitmq vhost ft |
n/a — stubbed at disabled=true |
producer is a no-op (fast-track/rabbitmq/fast-track.rmq.module.ts:8); 4× emitTransaction in bet.service + 7× emitBonus in promo-effect.service silently drop |
| ebit-api → ebit-sr | Redis pub/sub (Nest microservice transport via @ExternalControllerClient) |
(uses cache Redis) | internal-only | NO — see project_otel_microservice_transport_gap.md |
callee spans are orphan traces in Jaeger |
| ebit-sr → ebit-api (walletClient) | Redis pub/sub | (uses cache Redis) | internal-only | NO — same gap | three-hop bet trace appears as three uncorrelated traces |
| ebit-api → ebit-rt | Redis pub/sub channel server_channel_event.* (e.g. BalanceUpdated) |
(uses cache Redis) | internal-only | partial — publish span on the api side; rt's MessagePattern handler is not auto-instrumented, then the send /events emit is its own root span |
../flows/dropbet-bet-place.md §4.3 (publish), ../flows/rt-websocket.md (rt side) |
| All NestJS apps → otel-collector | OTLP/HTTP | 4318 | none (internal bridge net) | n/a (this is the trace transport itself) | OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318 |
| ebit-fe + ebit-admin-fe (SSR) → otel-collector | OTLP/HTTP | 4318 (bridge) | none | n/a | server-side spans |
| ebit-fe (browser) → otel-collector | OTLP/HTTP | host-published 4318 | none — CORS allowlist localhost:{3000,3001,3003} |
n/a | NEXT_PUBLIC_OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 (host port; browser can't reach docker DNS) |
| Docker daemon → otel-collector | filelog (file scrape) | n/a | reads /var/lib/docker/containers as root |
n/a (logs only) | bridges EvoLogger/winston records that bypass the OTel logs SDK (per project_evologger_trace_correlation.md) |
| otel-collector → Jaeger | OTLP/HTTP | 4318 (bridge) | tls.insecure | yes (this is the trace sink) | Jaeger v2.17.0 with Badger backend at /opt/jaeger-data |
| otel-collector → Prometheus | scrape :8889/metrics |
8889 | none | metrics only | spanmetrics-derived series (traces_spanmetrics_calls_total, _duration_milliseconds_bucket) |
| otel-collector → Loki | HTTP push | 3100 | none | logs only | both OTLP-bridged pino + filelog winston records |
Traffic mode summary¶
- REST hot path — Browser → ebit-fe → ebit-api → Postgres + Redis. Fully traced end-to-end as of task #27 (browser RUM) + the manual
AuthService.loginspan work. - Websocket hot path — Browser → ebit-rt over WSS, server → client pushes triggered by Redis pub/sub from ebit-api. Trace coverage is partial: per-emit
send /eventsspans exist, but connection/handshake/auth-RPC are not part of the trace tree (see../flows/rt-websocket.md§6.1). - Game-server fan-out — ebit-api proxies speed-roulette via
@ExternalControllerClient. Three Jaeger traces, no parent linkage. Same architecture would apply to ebit-bj if anyone wired a proxy. - Async work — BullMQ on the cache Redis. Producer's
EVALSHAspan is parented to the request, but the consumer starts an orphan trace (queue context not propagated — also called out in../audits/perf-trace-coverage-audit.md). - External integration — RabbitMQ
ftvhost is provisioned but stubbed; all 11 producer call sites are no-ops today.