# 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.