146 lines
8.4 KiB
Markdown
146 lines
8.4 KiB
Markdown
# 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](../../DEPLOY.md) and every key is in
|
|
[config-matrix.md](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
|
|
|
|
```mermaid
|
|
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](config-matrix.md#the-seam-selectors) |
|
|
| 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.
|