Files
baya-monorepo/archive/docs/integration/topology.md
T
2026-08-02 20:01:31 +03:30

8.4 KiB

Topology — the runtime dependency graph

What talks to what at runtime, what breaks when each hop is down, and which file configures it. This is the answer to a deploy question; the deploy procedure is DEPLOY.md and every key is in config-matrix.md.

Last verified: 2026-07-30 against commit d3ec723, docker-compose.yml, deploy/Caddyfile and server/src/API/Baya.Web.Api/Program.cs.


Deployed

graph LR
  browser["Browser<br/>balinyaar.ir"]

  subgraph net["caddy_net (external docker network)"]
    caddy["Caddy<br/><i>pre-existing, not in this repo</i><br/>TLS terminator"]
    web["balinyaar-web:3000<br/>Next.js 16"]
    api["balinyaar-api:8080<br/>ASP.NET Core 10"]
    relay["balinyaar-otp-relay:5010<br/>Node, zero deps"]
    proxy["hysteria-client:8081<br/><i>pre-existing</i>"]
  end

  sql[("Remote SQL Server<br/>87.107.152.16:1433<br/><b>not containerised</b>")]
  vol[["api-object-storage<br/>docker volume"]]
  tg["api.telegram.org"]
  rails["External rails<br/>PSP · BNPL · Finnotech · Neshan · مودیان<br/><i>all mocked by default</i>"]

  browser -->|"HTTPS"| caddy
  caddy -->|"HTTP :3000"| web
  browser -->|"HTTPS api.balinyaar.ir<br/><b>every API call</b>"| caddy
  caddy -->|"HTTP :8080<br/>+ X-Forwarded-For"| api
  api -->|"TCP 1433"| sql
  api -->|"HTTP + X-Api-Key"| relay
  api --> vol
  api -.->|"per seam config"| rails
  relay -->|"HTTP CONNECT"| proxy
  proxy --> tg

  web -.->|"builds absolute URLs only<br/><b>no server-to-server calls</b>"| api

The single most important edge is the dotted one. The browser calls the API directly at https://api.balinyaar.ir; the web container does not proxy or fetch on the browser's behalf. That is why NEXT_PUBLIC_API_URL must be the public hostname, never the container name — a container name is unresolvable from a browser. It is also why every NEXT_PUBLIC_* change needs an image rebuild: the value is compiled into the bundle.

Every edge

# From → To Carries Breaks if down Configured in
1 browser → Caddy All HTTPS for both hostnames Everything. Caddy is the only TLS terminator and the only published port deploy/Caddyfile (pasted into the pre-existing Caddy)
2 Caddy → balinyaar-web:3000 The Next.js app shell, RSC payloads, static assets The site does not load. The API keeps working — they are independent hostnames Caddyfile · docker-compose.yml
3 browser → Caddy → balinyaar-api:8080 Every /api/v1/* call, bearer-authenticated The site loads and every screen shows its error state. No data, no login NEXT_PUBLIC_API_URL (build-time) · Cors:AllowedOrigins
4 Caddy → API, X-Forwarded-For The real client IP The rate limiter partitions every request onto Caddy's IP, so one noisy client 429s everyone ForwardedHeaders:KnownNetworks — the docker bridge ranges
5 API → remote SQL Server All application data + the Serilog sink Total outage. /healthz/ready fails and the API will not start ConnectionStrings:SqlServer / :logDb
6 API → balinyaar-otp-relay:5010 OTP codes, X-Api-Key authenticated Nobody can log in. Every other authenticated screen keeps working for existing sessions Seams__Sms__Telegram__BaseUrl (compose) · Seams:Sms:Telegram:ApiKey
7 relay → hysteria-client:8081 → Telegram The code delivery itself Same as 6 — codes are generated but never arrive. Fails at relay boot with a clear message, not silently per-OTP TELEGRAM_PROXY_URL
8 API → api-object-storage volume Verification documents, avatars Uploads fail; /healthz/ready fails (it does a real write probe). Without the volume, existing documents vanish on the next up --build Seams__ObjectStorage__RootPath + the named volume
9 API → external rails Payments, BNPL, KYC, geocoding, e-invoicing Nothing, by default — every rail is mock. Real behaviour begins the moment a Provider selector changes Seams:<rail>:Provider — see config-matrix.md
10 PSP / BNPL / transferor → API webhooks Payment capture, BNPL settlement, payout reconciliation Payments are taken but bookings are never confirmed — the webhook is what creates the booking webhook rate-limit policy, anonymous routes, per-provider signing secrets

Ports

Port Where it applies Note
443 The host Caddy. The only published port on the machine
3000 caddy_net only Next.js. Not published
8080 caddy_net only The API in-container
5002 A developer laptop only The local launchSettings.json port — plain HTTP, and nothing in the deployment uses it
5010 caddy_net only The OTP relay
1433 Outbound to 87.107.152.16 Remote SQL Server

5002 vs 8080 catches people out. Most prose in this repo says 5002 because that is the local port. The container binds 8080 (the ASP.NET default in a container) and the Caddyfile points there.

Startup order

depends_on: [otp-relay] puts the relay before the API, but that only orders container start, not readiness. What actually gates the API's boot:

  1. StartupSecretsGuard — refuses to start on a missing or placeholder secret, before any service reads config.
  2. A reachable SQL Server — required to start, full stop.
  3. In Development (which is what the deployment runs): apply migrations, then seed default users, payment gateways, the demo world and the demo lifecycle. All idempotent — a re-boot logs Demo world already seeded — no-op. First boot takes ~40 s against the remote DB.
  4. When deployed as a non-Development environment: DDL is a separate one-shot (dotnet Baya.Web.Api.dll migrate); boot only checks the schema is current and fails fast on a pending migration, then seeds roles.

The relay refuses to start without API_KEY (min 16 chars) or with an unreachable proxy — both fail loudly at boot rather than per-request.

Request pipeline inside the API

Order matters, and two placements are deliberate:

UseForwardedHeaders   ← first, so the resolved client IP is in place before anything reads it
UseSwaggerAndUi
UseRouting
UseCors               ← after routing, BEFORE the rate limiter and auth, so a pre-flight OPTIONS
UseRateLimiter           is answered rather than rejected as 429 or 401
UseAuthentication
UseAuthorization
MapControllers
UseMetrics · UseHealthChecks
ConfigureGrpcPipeline

What is not in the graph

Why
A database container The DB is remote and pre-provisioned. RUNBOOK.md's local-SQL-in-Docker path describes a different world — an unseeded one (contradiction C-5)
Redis ICacheService / IDistributedLock are single-process today. When a multi-instance deployment needs a real one, a redis readiness check gets added alongside it
A job runner The weekly payout batch is generated by an in-process IRecurringJob scheduler. No external cron, no queue
A message broker Nothing is asynchronous across a process boundary
An OTLP collector OpenTelemetry:Otlp:Endpoint is unset, so nothing is exported. Prometheus scrapes /metrics directly
A second API instance Single instance. Multi-instance needs the distributed lock to become real first

Local development

Same code, a different graph — no Caddy, no containers, and the same remote database:

localhost:3000  (npm run dev)  ──▸  localhost:5002  (dotnet run, plain HTTP)
                                         │
                                         ├──▸  87.107.152.16:1433   (the same remote DB)
                                         └──▸  127.0.0.1:5010       (the relay, if you run it)

Three things to know before the first run:

  • The API is plain HTTP locally. dotnet dev-certs https --trust is not needed and there is no certificate to trust (contradiction C-4).
  • You share the deployment's database. Booting in Development migrates and seeds against it. The seeders are idempotent, but you and the demo site are looking at the same rows.
  • If you run the relay locally, copy Seams:Sms:Telegram:ApiKey from appsettings.Development.json into your own telegram-otp-bot/.env. The value in .env.example is published and TelegramSmsSender deliberately rejects it.