Files
baya-monorepo/docs/integration/config-matrix.md
T
2026-07-30 12:49:46 +03:30

18 KiB
Raw Blame History

Config matrix

Every configuration key on both sides of the seam, plus docker and the OTP relay, with where it is set and who reads it.

Last verified: 2026-07-30 against commit d3ec723. Built by mechanically enumerating every leaf key in both appsettings*.json, every environment: entry in docker-compose.yml, every assignment in the three client/.env* files and telegram-otp-bot/.env.example, and every process.env.* read under client/src/ — then diffing the sets. The gaps that diff found are in § What the diff found.


Configuration lives in files. dotnet user-secrets is not used.

The <UserSecretsId> was removed from Baya.Web.Api.csproj, so that store is not even read. A stale secrets.json on a developer machine is inert and can be deleted. Any instruction anywhere in this repo to set a Balinyaar value with dotnet user-secrets is stale — including the placeholder string SET_VIA_USER_SECRETS_OR_ENV, whose name is a historical artifact (see § The placeholder's name).

Every value is a file in git, which means the repository contains live credentials — a deliberate pre-launch trade for a demo deployment. Before onboarding real users they must be rotated and the secret half moved out of git: DEPLOY.md § Going to Production and, when Phase 5 writes it, docs/roadmap/pre-launch.md.

⚠️ Seams:FieldEncryption:Key and :HashKey are load-bearing and must never change. Every encrypted column in the database — phones, addresses, IBANs, clinical notes — was written with those exact values, and users.PhoneHash, which every login looks up, is derived from HashKey. Rotating either makes the existing data unreadable and locks every account out. The JWE keys (IdentitySettings:SecretKey/Encryptkey) are safe to rotate — that only signs everyone out.

There is no appsettings.Production.json

Only appsettings.json (placeholders) and appsettings.Development.json (real values) exist. The deployment runs ASPNETCORE_ENVIRONMENT=Development, so appsettings.Development.json is the production config. Creating an appsettings.Production.json today would change nothing until the environment name changes too.


Server — appsettings.json / appsettings.Development.json

appsettings.json holds a rejected placeholder for every secret; appsettings.Development.json holds the real value. Environment-variable overrides use the double-underscore form (Seams__Sms__Telegram__BaseUrl).

Key Set in Read by Required Default / committed value
ConnectionStrings:SqlServer both AddPersistenceServices, StartupSecretsGuard, readiness probe yes, always placeholder / 87.107.152.16,1433;Database=Baya
ConnectionStrings:logDb both Serilog sink, StartupSecretsGuard, readiness probe (deployed only) yes, always placeholder / …;Database=Baya_Logs
IdentitySettings:SecretKey both JWE signing yes when deployed placeholder / dev-only-…-not-for-production
IdentitySettings:Encryptkey both JWE AES-128 encryption yes when deployed placeholder / dev-only-16bytes
IdentitySettings:Issuer both token validation Balinyaar
IdentitySettings:Audience both token validation BalinyaarClient
IdentitySettings:NotBeforeMinutes both token validation 0
IdentitySettings:ExpirationMinutes both access-token lifetime 60but the client's cookie expires at 15 min, see api-contract.md
Seams:FieldEncryption:Key both IFieldEncryptor (process-wide singleton) yes when deployed placeholder / local-dev-field-encryption-key-not-for-production · IMMUTABLE
Seams:FieldEncryption:HashKey both deterministic lookup hashes incl. users.PhoneHash yes when deployed placeholder / local-dev-field-hash-key-not-for-production · IMMUTABLE
Seams:ObjectStorage:RootPath both + compose local-disk blob root when provider = local "" / compose sets /app/data/object-storage
Seams:Sms:Provider Dev only SMS seam selector and the OTP-capture gate in Program.cs telegram (default in code: mock)
Seams:Sms:Telegram:BaseUrl Dev + compose TelegramSmsSender when provider = telegram http://127.0.0.1:5010 / compose: http://balinyaar-otp-relay:5010
Seams:Sms:Telegram:ApiKey Dev only relay X-Api-Keymust equal the relay's API_KEY when provider = telegram 6a8dfaee… (rotated away from the published example)
Seams:Sms:Telegram:TimeoutSeconds Dev only HTTP timeout 10
Seams:Geocoding:ReturnNullCoordinates both mock geocoder false
Seams:Geocoding:LowConfidenceMarker both mock geocoder NO_GEO
Seams:Geocoding:ResolvedConfidence both mock geocoder 0.9
Cors:AllowedOrigins both AddCorsPolicies [] → falls back to http://localhost:3000 / the three real origins
ForwardedHeaders:KnownProxies both AddForwardedHeadersConfiguration []
ForwardedHeaders:KnownNetworks both ditto — required for the rate limiter to see the real client IP behind Caddy deployed [] / the three docker bridge ranges
AllowedHosts both host filtering *
Kestrel:EndpointDefaults:Protocols both Kestrel — Http1AndHttp2 is what lets gRPC share the port Http1AndHttp2

Keys the code reads that no file sets

Every one has a working default, so nothing is broken — but none is discoverable from the config files.

Key Read by Behaviour when unset Notes
OpenTelemetry:Otlp:Endpoint SetupOpenTelemetry OTLP export is not wired at all. Prometheus /metrics still works Opt-in by design, so no exporter spams an absent collector
Search:Backend AddPersistenceServices SqlNurseSearch Any value other than sql/empty throws at startup — Elasticsearch is deferred and fails loudly
Seed:AdminUsername / :AdminPassword / :AdminEmail SeedDataBase no break-glass admin is seeded The hardcoded admin/qw123321 was removed in refinement-phase-5 and the pre-commit hook blocks its return
Seams:<rail>:Provider (×11) AddCrossCuttingSeams mock The seam selectors, below

The seam selectors

SeamOptions binds the whole Seams section. Every rail defaults to its mock, so an unconfigured environment behaves exactly as before and a partial rollout is the normal case — real SMS and a real geocoder while payments stay mocked is three config keys.

Selector mock (default) → Real values
Seams:Sms:Provider log the OTP kavenegar smsir ghasedak · telegram (Development relay, not a gateway)
Seams:ObjectStorage:Provider local disk s3 (MinIO / ArvanCloud, path-style)
Seams:Geocoding:Provider deterministic point neshan
Seams:Shahkar:Provider designated test values finnotech
Seams:IdentityKyc:Provider designated test values finnotech
Seams:BankOwnership:Provider designated test values finnotech
Seams:Payments:Provider deterministic capture zarinpal sadad vandar jibit
Seams:Bnpl:Provider one mock provider realIBnplProviderResolver per provider_code
Seams:BankTransfer:Provider settles every payout jibit vandar sadad
Seams:Moadian:Provider stays pending moadian
Seams:Currency (no selector) TomanToIrrMultiplier = 10 a redenomination is a config change

Seams:Finnotech:{BaseUrl,ClientId,AccessToken} are shared by the three trust rails — they authenticate against one tenant, so the connection facts live once.

Each mock also carries test knobs whose only purpose is to make a failure path reachable: Shahkar:SharedSimPhone 09120000000 · Shahkar:MismatchNationalId 1111111111 · IdentityKyc:FailNationalId 0000000000 · BankOwnership:MismatchIban IR0000…0000 · Bnpl:NotEligibleMobile 09120000099 · BankTransfer:FailIban · BankTransfer:ForceFailure · PaymentCapture:ForceFailure · Moadian:ForceRegistered · ReviewModeration:AutoApproveClean · LicenseVerification:AutoApprove · Payments:InvalidSignatureMarker INVALID_SIGNATURE.


Client — client/.env.*

Every NEXT_PUBLIC_* value is inlined into the browser bundle at build time. It is public by definition, and changing one requires rebuilding the image, not restarting the container. This is why docker-compose.yml deliberately sets no environment: for the web service — anything there would be silently ignored.

Key .env.development .env.production Read by Required
NEXT_PUBLIC_API_URL http://localhost:5002 https://api.balinyaar.ir config.tsAPI_URL yesenvRequired, boot fails without it
NEXT_PUBLIC_ENV development production getCurrentEnvironment()IS_PRODUCTION
NEXT_PUBLIC_DEBUG true false IS_DEBUGtrue prints the resolved config, including the API URL, to the browser console
NEXT_PUBLIC_PUBLIC_URL http://localhost:3000 https://balinyaar.ir PUBLIC_URL (optional)
NEXT_PUBLIC_SITE_URL (unset) https://balinyaar.ir SITE_URLmetadata only (OG tags, metadataBase, robots.ts, sitemap.ts), never API calls — · falls back to http://localhost:3000
NEXT_PUBLIC_NESHAN_KEY (unset) (commented out) NESHAN_WEB_KEY — the Neshan web key — · unset ⇒ AddressMapPicker uses its bounded-canvas grid, so dev/CI/jsdom work without it
NEXT_PUBLIC_EVV_MOCK_GPS not in any file not in any file bookings/constants.ts — · in_range when the bookings mock is on, else off. Values: off in_range out_of_range denied
NEXT_PUBLIC_VERSION not in any file not in any file getCurrentVersion(), after npm_package_version — · falls back to 'unknown'

Two Neshan keys exist and they are different products. NEXT_PUBLIC_NESHAN_KEY is a client-embeddable web key; Seams:Geocoding:ApiKey is the server geocoding key. Never share one value between them.

client/.env.sample is the copy-me template for a fresh clone. It is not loaded by Next.js.


Docker — docker-compose.yml

Three containers, no published ports — everything is reached through the existing Caddy on the external caddy_net. Full graph in topology.md.

Service Variable Value Why it is here and not in a file
api ASPNETCORE_ENVIRONMENT Development Deliberate, so the demo + lifecycle seeders populate the shared DB. Consequences in DEPLOY.md
api Seams__Sms__Telegram__BaseUrl http://balinyaar-otp-relay:5010 Container DNS instead of loopback
api Seams__ObjectStorage__RootPath /app/data/object-storage Must land on the named volume
web (none) Every NEXT_PUBLIC_* is baked at build time; a variable here would be ignored
otp-relay TELEGRAM_BOT_TOKEN 8968527151:AAF… Live credential in git
otp-relay TELEGRAM_CHAT_IDS 1277103616,110209855 Every id receives every login code, for every phone number
otp-relay API_KEY 6a8dfaee… Must equal Seams:Sms:Telegram:ApiKey
otp-relay TELEGRAM_PROXY_URL http://hysteria-client:8081 api.telegram.org is filtered in Iran; a wrong value fails at boot, not per-OTP
otp-relay REDACT_CODE_IN_LOGS "true" Keeps codes out of docker logs so a host-log reader cannot harvest them

Volumes: api-object-storage (uploaded verification documents — losing it breaks the admin queue) and api-logs (Serilog file sink).

Caddy — deploy/Caddyfile

Not loaded by anything in this repo; it is a copy of the block DEPLOY.md tells you to paste into the Caddy container that owns caddy_net.

Hostname Upstream Notes
balinyaar.ir, www.balinyaar.ir balinyaar-web:3000
api.balinyaar.ir balinyaar-api:8080 8080 in-container, not 5002 — 5002 is the local launchSettings.json port

Caddy is the only TLS terminator and renews both certificates itself. It sets X-Forwarded-For and X-Forwarded-Proto by default, which is why no header directives are needed — but the API must trust the hop via ForwardedHeaders:KnownNetworks, or the rate limiter partitions every request onto Caddy's IP.

CORS

CorsServiceExtension, policy BalinyaarWebClient.

Origins Cors:AllowedOrigins; falls back to http://localhost:3000 when unset or empty
Headers Authorization · Content-Type · Accept-Language · Idempotency-Key — explicit, not AllowAnyHeader, so the surface is auditable
Methods any
Credentials not allowed. The client authenticates with a bearer header, not a cookie, so AllowCredentials() is unnecessary
Pipeline position after UseRouting(), before the rate limiter and authentication, so a pre-flight OPTIONS is answered rather than rejected as 429/401

Adding a browser origin means editing Cors:AllowedOrigins and rebuilding the client if its NEXT_PUBLIC_API_URL changes.

Telegram OTP relay — telegram-otp-bot

A standalone zero-dependency Node service. It is not an SMS gateway: there is no per-user routing — it broadcasts every code to a fixed list of chat ids. Workable for a trusted demo group, disqualifying the moment anyone outside it can request a code. Switching Seams:Sms:Provider to kavenegar at that point changes nothing else.

Variable Required Default Notes
TELEGRAM_BOT_TOKEN yes From @BotFather
TELEGRAM_CHAT_IDS yes Comma-separated. Each recipient must have messaged the bot first — Telegram forbids a bot opening a conversation
API_KEY yes Min 16 chars; the process refuses to start without it. Must equal Seams:Sms:Telegram:ApiKey. The value in .env.example is published, and TelegramSmsSender deliberately refuses to authenticate with it
PORT 5010
HOST 127.0.0.1
REDACT_CODE_IN_LOGS false true in the deployment
TELEGRAM_PROXY_URL unset ⇒ direct HTTP CONNECT or SOCKS5. HTTPS_PROXY/ALL_PROXY are honoured as a fallback

telegram-otp-bot/.env is for local npm start only — nothing in it is read inside the container.

The database is not containerised

It is a remote SQL Server at 87.107.152.16:1433, already provisioned and already seeded. Nothing in docker-compose.yml creates it; the API only needs network reach. Two databases: Baya (app) and Baya_Logs (Serilog sink).

Health, metrics, and the secrets guard

Endpoint Checks
/healthz/live process only — deliberately dependency-free, so a dependency outage never restarts a healthy instance
/healthz/ready app DB · log DB (deployed only — its connection string is a placeholder in Development) · an object-storage write round-trip
/HealthCheck the aggregate, retained for existing probes
/metrics Prometheus scrape. OpenTelemetry is the only metrics source; the duplicate prometheus-net stack was removed

StartupSecretsGuard runs before any service reads configuration and refuses to boot on a missing or placeholder value. It requires both connection strings in every environment, and the four crypto keys only when not Development. It is skipped entirely in the Testing environment. Placeholder markers: SET_VIA_USER_SECRETS_OR_ENV, not-for-production, change-me, ShouldBe-LongerThan-16Char-SecretKey, 16CharEncryptKey.


What the diff found

Enumerating both sets and subtracting them surfaced five things. None is a broken deployment; all five are places where the config is not discoverable from the config files.

  1. NEXT_PUBLIC_EVV_MOCK_GPS and NEXT_PUBLIC_VERSION are read by client code and declared in no .env file. Both have working defaults. Adding them commented-out to .env.sample would make them findable.
  2. OpenTelemetry:Otlp:Endpoint, Search:Backend and Seed:Admin* are read by server code and set nowhere. All three are intentionally opt-in, but a reader of appsettings.json cannot learn they exist. Search:Backend is the sharpest: a wrong value throws at startup.
  3. client/.env.sample still says NEXT_PUBLIC_API_URL = https://localhost:5002 — the https half of contradiction C-3, in the one file a fresh clone is meant to copy. .env.development has the correct http://.
  4. Seams:Sms:Telegram:ApiKey and the relay's API_KEY are the same secret in two files with no mechanism keeping them equal. They currently match. A mismatch fails every OTP send at runtime, not at boot.
  5. There is no appsettings.Production.json, and DEPLOY.md step 2 of "Going to Production" is therefore a create, not an edit.

The placeholder's name

SET_VIA_USER_SECRETS_OR_ENV names a store that no longer exists (contradiction C-2). The behaviour is correct — it is a sentinel that StartupSecretsGuard and the pre-commit hook both reject — but the name instructs a reader to use a removed mechanism.

It was not renamed in this phase, because the string is load-bearing in seven live files: appsettings.json (×6), StartupSecretsGuard.cs, .githooks/pre-commit, .githooks/README.md, Baya.Test.Api/StartupSecretsGuardTests.cs (×2), docs/rules/shared/git-and-gates.md and docs/rules/server/structure.md. Renaming it is a server-code + git-hook + test change requiring dotnet build and dotnet test to prove the gate still fires — out of scope for a documentation phase. This section is the authoritative statement of the mechanism; the rename is filed for Phase 4 with that exact seven-file worklist.