# Runtime services — deployment topology

**Audit date:** 2026-07-10 · **Derivation:** every entry below is justified from the code — the DI seam
that depends on it, the config key that names it, or the package/startup wiring that talks to it. Nothing
is invented; "not needed" claims are backed by the absence of the package/code. Make-it-real steps live in
`dev/shared-working-context/reports/mocks-registry.md` (row references below).

**The shape in one sentence:** today the API binary talks to exactly **one external system — SQL Server**
(app DB + log DB); everything else (18 seams) is an in-process mock, so "deployment" today is one container
+ one database — and the table below is the roadmap of what must exist as each seam goes real.

> **Refinement-phase-8 update (2026-07-13):** services **8–14** and **16** below now have a **real HTTP adapter
> shipped behind their seam**, config-selected by a per-rail `Seams:*:Provider` selector (mock stays the default).
> "Depends via" now points at a real client, not just a planned one — provisioning the vendor account + credential
> and flipping the selector turns each on, no code change. What is still genuinely absent (no adapter): **Redis**
> (5), **Elasticsearch** (17), and the LLM **review-moderation classifier** (15, optional). MoH/INO/eNamad (16)
> stay **manual by design**. See the mocks-registry refinement-phase-8 banner for the provider tokens per rail.
>
> **Refinement-phase-9 update (2026-07-13) — observability (service 4):** the two overlapping metric stacks were
> consolidated onto **one OpenTelemetry stack** — prometheus-net was removed; metrics are scraped at `/metrics` via
> the OTel Prometheus exporter, and **distributed tracing** (ASP.NET Core + EF Core) was added, exporting **OTLP
> only when `OpenTelemetry:Otlp:Endpoint` is configured** (an MVP with Prometheus alone is unchanged). A request's
> `ApiResult.requestId` is its W3C trace id (support ↔ trace 1:1). Health checks split into `/healthz/live`
> (process) vs `/healthz/ready` (app DB + log DB [deployed] + an object-storage write probe); `/HealthCheck` stays
> as the aggregate. An **OTLP collector** (Grafana Tempo / Jaeger / OTEL Collector) becomes the optional new
> observability service when trace export is turned on. Elasticsearch (17) is still deferred.

## Service inventory

| # | Service | Purpose | Depends via (seam / config) | MVP? | Registry row |
| --- | --- | --- | --- | --- | --- |
| 1 | **SQL Server** (app DB `Baya`) | System of record — 12 schemas (`usr ops geo catalog verif search booking payments payouts reviews messaging partner`) | EF Core; `ConnectionStrings:SqlServer` | **Required now** | — |
| 2 | **SQL Server** (log DB `Baya_Logs`) | Serilog sink in deployed envs (Warning+, auto-created `log.LogEvents`) | `ConnectionStrings:logDb` | **Required now** (deployed) | — |
| 3 | **Reverse proxy / TLS** (nginx·caddy·traefik) | TLS termination, HTTP/1.1+2, forwarded headers | Kestrel config; JWE bearer | **Required now** | — |
| 4 | **Prometheus** (+ Grafana) · optional **OTLP collector** | Scrapes `/metrics` (one OTel stack, refinement-phase-9); traces export OTLP when configured | OTel metrics + tracing; `OpenTelemetry:Otlp:Endpoint` | **Recommended now** (collector optional) | — |
| 5 | **Redis** | `ICacheService` + `IDistributedLock` (money-path mutex) | `Seams:*` (keys TBD; none today) | Before >1 API instance | rows 14, 42 |
| 6 | **MinIO / S3 / ArvanCloud** | `IObjectStorage` — verification docs, avatars (REQ-006), invoice PDFs | `Seams:ObjectStorage:*` | Before real verification | row 13 |
| 7 | **Job scheduler** — in-process, SQL only (refinement-phase-7 **done**) | The recurring crons: booking-expiry, notification-retention, credential-expiry scan, no-show sweep, weekly payout-batch generation (Moadian/refund-settlement poll = Phase 8) | `RecurringJobSchedulerHostedService` + `IRecurringJob`s | **Done for single instance** (no new infra) | row 26 |
| 8 | **SMS gateway** (Kavenegar·Ghasedak·SMS.ir) | `ISmsSender` — OTP delivery (login is impossible without it) | `Seams:Sms:*` (to be added) | **Launch-critical** | row 12 |
| 9 | **PSP / IPG + Shaparak** (ZarinPal·Sadad·Vandar·Jibit) | `IPaymentProvider` + `IWebhookVerifier` + `ISettlementSplitProvider` (تسهیم) | encrypted `payment_gateways.config_json` | Real payments | rows 39–41 |
| 10 | **BNPL providers** (SnappPay·Digipay) | `IBnplProvider` / `IBnplProviderResolver` / `ICurrencyNormalizer` | `Seams:Bnpl:*`, `Seams:Currency:*`, gateway config | Optional at launch | rows 46–47 |
| 11 | **Bank-transfer rail** (Jibit·Vandar·Sadad payout API → PAYA/SATNA) | `IBankTransferProvider` — weekly nurse payouts | `Seams:BankTransfer:*` | Real payouts | row 23 |
| 12 | **سامانه مودیان** (tax e-invoicing) | `IMoadianClient` — legal invoice registration | `Seams:Moadian:*` + signing cert | Legal — soon after launch | row 45 |
| 13 | **KYC bridge vendor** (Finnotech-class) | `IShahkarVerifier` + `IIdentityKycProvider` + `IBankAccountOwnershipVerifier` | `Seams:Shahkar:*`, `Seams:IdentityKyc:*`, `Seams:BankOwnership:*` | Real verification + payout gate | rows 27, 28, 30 |
| 14 | **Geocoding** (Neshan) | `IGeocoder` — address → coordinates (EVV distance) | `Seams:Geocoding:*` | With real EVV | row 31 |
| 15 | **Review-moderation classifier** (LLM/API) | `IReviewModerationService` — AI pre-screen | `Seams:ReviewModeration:*` | Optional (human queue is default) | row 33 |
| 16 | **MoH / INO / eNamad** | `ICredentialVerifier` + `ILicenseVerificationService` — **manual admin process; no public B2B API exists** | `Seams:LicenseVerification:*` | Manual = the MVP design | rows 29, 50 |
| 17 | **Elasticsearch** | `INurseSearch` alt backend + outbox feeder | `Search:Backend` (non-`sql` throws today) | **Not MVP** — SQL search is real | rows 38, 43 |

**Explicitly not needed** (verified absent from `server/Directory.Packages.props` and code): message
broker (no RabbitMQ/Kafka), Redis today (no `StackExchange.Redis`), Hangfire/Quartz today, Elasticsearch
client (`Elastic.Clients.*`), any cloud SDK, any payment/SMS vendor SDK. The only externally-pointing
package beyond SQL Server is `Serilog.Sinks.Elasticsearch` — its wiring is commented out
(`Baya.Infrastructure.CrossCutting/Logging/LoggingConfiguration.cs:58-70`).

---

## Dependency graph

Solid edges are live today; dashed edges are behind a mocked seam (the arrow exists in code, the wire does
not). One line per node below the graph.

```mermaid
flowchart LR
  subgraph fe[Frontend]
    WEB["Next.js client"]
  end
  subgraph host["API host — Baya.Web.Api (:5002)"]
    API["ASP.NET Core API\nREST /api/v1 · /metrics · /HealthCheck"]
    JOBS["In-proc interval jobs\n(→ Hangfire/Quartz later)"]
  end
  subgraph data[Data & platform infra]
    SQL[("SQL Server 'Baya'\n12 schemas · migrations on boot")]
    LOG[("SQL Server 'Baya_Logs'\nSerilog sink, Warning+")]
    REDIS[("Redis — cache + dist. lock\n(in-proc today)")]
    S3[("MinIO / S3 — object storage\n(local disk today)")]
  end
  subgraph money["Money rails (all mocked today)"]
    PSP["PSP / IPG + Shaparak\ncapture · webhook · تسهیم"]
    BNPL["BNPL — SnappPay / Digipay"]
    BANK["PAYA / SATNA payout rail"]
    MOAD["سامانه مودیان e-invoicing"]
  end
  subgraph trust["Trust & identity rails (all mocked today)"]
    SMS["SMS gateway (OTP)"]
    KYC["Shahkar · e-KYC · استعلام شبا"]
    GEOC["Neshan geocoding"]
    MODAI["Review-moderation classifier"]
    MANUAL["MoH / INO / eNamad\n(manual admin review)"]
  end
  subgraph obs[Observability]
    PROM["Prometheus (+ Grafana)"]
  end
  WEB -->|HTTPS/JSON| API
  API --> SQL
  API --> LOG
  JOBS --- API
  API -.-> REDIS
  API -.-> S3
  API -.-> SMS
  API <-.-> PSP
  API <-.-> BNPL
  API -.-> BANK
  API -.-> MOAD
  API -.-> KYC
  API -.-> GEOC
  API -.-> MODAI
  MANUAL -.- API
  PROM -->|scrape /metrics| API
```

- **Next.js client** — the only API consumer; reads `NEXT_PUBLIC_API_URL` (root `CLAUDE.md`).
- **API** — single ASP.NET Core host; all seams resolve in-process today.
- **In-proc jobs** — the two `BackgroundService` sweeps (booking-request expiry 1 min, notification
  retention 24 h); the scheduler upgrade re-homes them (plan §4.1).
- **SQL Server `Baya`** — system of record; migrations + seeding run on every non-Testing boot.
- **SQL Server `Baya_Logs`** — deployed-env Serilog sink (auto-creates DB/table).
- **Redis** — target for `ICacheService`/`IDistributedLock`; nothing speaks Redis yet.
- **MinIO/S3** — target for `IObjectStorage`; local disk + `file://` URLs today.
- **PSP/IPG** — card capture, callback signatures, تسهیم settlement split.
- **BNPL** — provider-financed installments; settle/revert callbacks.
- **PAYA/SATNA** — weekly nurse payout batches + async reconciliation.
- **مودیان** — legal e-invoice registration (pending→registered poll).
- **KYC vendor** — Shahkar phone↔NID, identity+liveness, Sheba ownership (payout gate).
- **Neshan** — geocoding for address coordinates / EVV distance.
- **Moderation classifier** — optional AI pre-screen; human moderation is the default gate.
- **MoH/INO/eNamad** — human verification workflows, by design (no API exists).
- **Prometheus** — scrapes `/metrics`; health check results forwarded as gauges.

---

## Per-service notes

### 1–2 · SQL Server (`Baya` + `Baya_Logs`)

- **Evidence:** `UseSqlServer` at
  `server/src/Infrastructure/Baya.Infrastructure.Persistence/ServiceConfiguration/ServiceCollectionExtensions.cs:41`;
  sink at `…CrossCutting/Logging/LoggingConfiguration.cs:44-45` (schema `log`, auto-create); the 12 schemas
  via per-entity `ToTable(name, schema)` (e.g. `PaymentsConfig/LedgerEntryConfig.cs:18`,
  `PayoutsConfig/NursePayoutConfig.cs:19`).
- **Default:** `mcr.microsoft.com/mssql/server:2022-latest` (Developer for dev; licensed edition in prod).
  Both DBs fit one instance; `Baya_Logs` can move later.
- **Config:** `ConnectionStrings:SqlServer`, `ConnectionStrings:logDb` — **rotate + externalize first**
  (plan §1.1; live `sa` credentials are committed today).
- **Health/readiness (refinement-phase-9 — §7.2 landed):** split `/healthz/live` (process) vs `/healthz/ready`
  (app DB + `logDb` [deployed] + object-storage write probe); `/HealthCheck` stays as the aggregate
  (`Monitoring/Configurations/HealthCheckConfigurations.cs`). Boot runs
  `MigrateAsync` + 3 seeders (`Program.cs:99-104`) → the login needs DDL rights and concurrent multi-node
  boot races (plan §4.3).

### 3 · Reverse proxy / TLS

- **Evidence of need:** JWE bearer auth (`RequireHttpsMetadata` must be true in prod —
  `Identity/ServiceConfiguration/ServiceCollectionExtension.cs:139`); Kestrel `EndpointDefaults=Http2`
  breaks non-TLS HTTP/1.1 (`appsettings.json:29-33`, plan §1.5); the rate limiter partitions on
  `RemoteIpAddress` with **no ForwardedHeaders middleware**
  (`WebFramework/ServiceConfiguration/RateLimitingServiceExtension.cs:69`, plan §1.6) — the proxy must pass
  `X-Forwarded-For` *and* the app must be taught to honor it.
- **Default:** caddy 2 / nginx 1.27; terminate TLS, h2 to clients, HTTP/1.1 (or h2c) upstream once §1.5
  lands.

### 4 · Prometheus (+ Grafana) · optional OTLP collector

- **Evidence (refinement-phase-9 — §7.1 landed):** **one** OpenTelemetry stack — metrics scraped at `/metrics` via
  `UseOpenTelemetryPrometheusScrapingEndpoint()` (`Monitoring/Configurations/PrometheusMetricsConfigurations.cs`),
  plus `WithTracing` (ASP.NET Core + EF Core) exporting **OTLP only when `OpenTelemetry:Otlp:Endpoint` is set**
  (`OpenTelemetryConfigurations.cs`). The duplicate prometheus-net stack (`UseMetricServer`/`UseHttpMetrics`/
  `ForwardToPrometheus` + packages) was removed. A request's `ApiResult.requestId` is its W3C trace id.
- **Default:** `prom/prometheus:v2.53` + `grafana/grafana:11`. A **tracing backend / OTLP collector** (Grafana
  Tempo · Jaeger · OpenTelemetry Collector) is the optional new service — set `OpenTelemetry:Otlp:Endpoint` at it to
  turn trace + metric export on; Prometheus-scrape-only is an acceptable MVP.

### 5 · Redis

- **Evidence of the gap:** `MemoryCacheService` and `InProcessDistributedLock`
  (`CrossCutting/Seams/MemoryCacheService.cs:11`, `InProcessDistributedLock.cs:14`) — single-process only;
  the money-path lock convention `booking:{id}:payment|refund` is already in the handlers.
- **Default:** `redis:7-alpine` (AOF on). **Required the moment a second API instance runs** (shared cache
  invalidation generation-tokens + cross-instance money mutex). Config keys to be introduced with the swap
  (plan §4.2); none exist today.

### 6 · MinIO / S3 / ArvanCloud

- **Evidence:** `LocalDiskObjectStorage` writes under a temp root and returns `file://` URLs
  (`CrossCutting/Seams/LocalDiskObjectStorage.cs:20,56`); consumers: b6 verification documents
  (signed-URL upload flow), future avatars (REQ-006), invoice `PdfStorageKey`
  (`InvoicesConfig/InvoiceConfig.cs` — `pdf_storage_key` column).
- **Default:** `minio/minio:latest` (S3-compatible; ArvanCloud object storage is the Iran-hosted option).
- **Config:** `Seams:ObjectStorage:RootPath` today → bucket/endpoint/keys with the swap. Presigned PUT/GET
  with expiry is the contract the frontend already codes against.

### 7 · Job scheduler

- **Evidence:** two `PeriodicTimer` hosted services only
  (`Persistence/ServiceConfiguration/ServiceCollectionExtensions.cs:63,67`); the payout/expiry/no-show/
  Moadian crons are admin-manual with seeded-but-unread cadence keys (plan §4.1). **No `IJobScheduler`
  interface exists** — the registry name is aspirational.
- **Default:** Hangfire on the existing SQL Server (no new container) — dashboard behind admin auth; or
  Quartz with SQL persistence. Not a separate service to "spin up", but it changes the SQL footprint
  (schema) and ops (dashboard, retries).

### 8 · SMS gateway

- **Evidence:** `LoggingSmsSender` logs OTPs instead of sending (`CrossCutting/Seams/LoggingSmsSender.cs:16`)
  — **no real user can log in**; per-phone resend window + `otp` rate policy already enforced upstream.
- **Default:** Kavenegar / Ghasedak / SMS.ir (SaaS — API key, no container). Keys to add:
  `Seams:Sms:{ApiKey,SenderLine,BaseUrl}` (registry row 12). Template/pattern OTP send for deliverability.

### 9 · PSP / IPG + Shaparak (تسهیم)

- **Evidence:** `MockPaymentProvider` (instant success, `VerifyAsync` echoes the expected amount —
  `CrossCutting/Seams/MockPaymentProvider.cs:24`), `MockWebhookVerifier` (marker-based "signature" —
  `MockWebhookVerifier.cs:21`), `MockSettlementSplitProvider` (`MockSettlementSplitProvider.cs:13`). Real
  merchant credentials belong in the **encrypted** `payment_gateways.config_json` (seeded sandbox row:
  `Persistence/ServiceCollectionExtensions.cs:109-117` — environment-gate it, plan §1.4).
- **Default:** SaaS (ZarinPal / Sadad / Vandar / Jibit) — needs merchant + terminal registration and
  تسهیم (settlement-split) setup to registered IBANs; webhook endpoint is already public
  (`POST webhooks/payments/{provider}`) with upsert-first idempotency in place.

### 10 · BNPL providers

- **Evidence:** `MockBnplProvider` drives the full verb set + state machine
  (`CrossCutting/Seams/MockBnplProvider.cs:17-53`); callback endpoint `WebhooksBnplController` (signed,
  rate-limited). `Seams:Bnpl:*` + `Seams:Currency:TomanToIrrMultiplier` (`SeamOptions.cs:98-120,79`).
- **Default:** SnappPay and/or Digipay (SaaS, OAuth). **Do plan §2.1 first** — the revert-clearing path is
  currently unreachable, so real BNPL refunds would strand ledger state.

### 11 · Bank-transfer rail (PAYA/SATNA)

- **Evidence:** `MockBankTransferProvider` settles every instruction instantly
  (`CrossCutting/Seams/MockBankTransferProvider.cs:18-31`); the handler already chooses PAYA vs SATNA by
  `payout_satna_threshold_irr` (`platform_configs` seed row 22) and the irreversibility backstops
  (unconditional `UNIQUE(booking_id)` link, forward-only payout machine) are in place.
- **Default:** Jibit / Vandar / Sadad payout API (SaaS). Needs the source settlement account + the async
  `submitted → paid/failed` reconciliation callback the mock collapses (registry row 23, steps 3–4).

### 12 · سامانه مودیان

- **Evidence:** `MockMoadianClient` leaves invoices `pending` forever (`CrossCutting/Seams/MockMoadianClient.cs:21`);
  no reconciliation job exists (plan §6.5). VAT-on-commission + sequential invoice numbers are already
  correct server-side.
- **Default:** government SaaS — enrollment (memory/economic code) + a signing certificate; the certificate
  is a deploy-time secret.

### 13 · KYC bridge (Shahkar / e-KYC / استعلام شبا)

- **Evidence:** three deterministic mocks with magic-value failure cases
  (`MockShahkarVerifier.cs:26-37`, `MockIdentityKycProvider.cs:25`,
  `MockBankAccountOwnershipVerifier.cs:26`); handlers already persist `external_response_json` and treat
  shared-SIM as a handled state. The Sheba-ownership result gates first payouts (`matched_national_id`).
- **Default:** one Finnotech-class bridge covers all three استعلام‌ها (SaaS; API keys under
  `Seams:{Shahkar,IdentityKyc,BankOwnership}:*`).

### 14 · Geocoding (Neshan)

- **Evidence:** `MockGeocoder` jitters ±5 km around 8 hardcoded centroids (`MockGeocoder.cs:52`) — EVV
  distance checks are noise until this (and/or REQ-008's user pin) is real. `Seams:Geocoding:*` is one of
  only three seam sections present in `appsettings.json` (`:22-26`).
- **Default:** Neshan (Iran coverage; SaaS API key), rate-limit/retry per registry row 31.

### 15 · Review moderation

- **Evidence:** keyword-list mock; clean text stays in the human queue by default
  (`MockReviewModerationService.cs:23`; `Seams:ReviewModeration:*`). Human `ModerateReviewCommand` retains
  decision authority — so this can stay mocked indefinitely at low volume.
- **Default:** any text-moderation API / LLM endpoint when review volume outgrows the human queue.

### 16 · MoH / INO / eNamad — a process, not a service

- **Evidence:** `MockCredentialVerifier` always returns `RequiresManualReview`
  (`MockCredentialVerifier.cs:18`); `MockLicenseVerificationService` likewise
  (`MockLicenseVerificationService.cs:26`). The registry itself records **no public B2B API exists** —
  the admin review queue *is* the real implementation. Provision: admin staffing + the f15 console.

### 17 · Elasticsearch — deliberately later

- **Evidence:** `SqlNurseSearch` is the real MVP backend; `Search:Backend` ≠ `sql` throws at startup
  (`Persistence/ServiceConfiguration/ServiceCollectionExtensions.cs:74-78`); no ES client package.
- **Default when needed:** `elasticsearch:8.x` + the outbox feeder (registry rows 38/43; plan §8.1).

---

## Deployment notes (from the code, not aspiration)

1. **Boot ≠ migrate (refinement-phase-7).** DDL is a separate deploy step — `dotnet run -- migrate` applies
   migrations + idempotent seeders then exits. **Development** boot still migrates + seeds (incl. the
   Development-only sandbox gateway + demo world) for convenience; **deployed** boot only *checks* the schema is
   current (`EnsureSchemaUpToDateAsync`, fail-fast on a pending migration) and seeds roles/break-glass admin. So
   multi-instance boots no longer race on DDL and the runtime login needs no permanent DDL rights (plan §1.3/§1.4
   also landed — no committed `admin`/`qw123321`; sandbox gateway is Development-only).
2. **Environment files:** `appsettings.json` ≡ `appsettings.Development.json` (byte-identical); **no
   Production/Staging file exists.** All non-secret env differences ride on ~14 `Seams:*` groups whose
   defaults live in code (`SeamOptions.cs`), not in config files.
3. **HTTP posture:** mixed `Http1AndHttp2` Kestrel (refinement-phase-5), so gRPC shares the listener via ALPN;
   gRPC **reflection is Development-only** (refinement-phase-9 §7.5); TLS required for JWE sanity.
4. **Single-instance constraints today:** in-memory cache, in-proc money lock, the in-proc recurring-job
   scheduler (refinement-phase-7 — its per-tick lock is that same in-proc seam), per-instance rate-limit buckets.
   §4.3 (migrations split from boot) **landed**; scaling past one instance still requires §4.2 (Redis for the
   shared cache + the cross-instance lock the scheduler/money path use) first — the DB uniques keep money
   *correct* either way, but locks/cache/limits/scheduler-de-dup silently degrade.
5. **Logs (refinement-phase-9 — §7.3 landed):** deployed envs write **Information+** to `Baya_Logs` (framework
   categories held at Warning); **no PII/secrets** (the OTP code is no longer logged in any env). The dead
   Elasticsearch sink + package were removed; log-table retention is an ops/DBA task (or ship logs to the OTLP
   collector). Dev writes console + `logs/log.json`.
6. **Client:** the Next.js app needs `NEXT_PUBLIC_API_URL` pointing at the proxy; wire casing camelCase;
   snake_case routes.
