# Phase 15 — Turn off developer/debug mode on the live site **Blocker:** blockers.md §B.2. Do this deliberately, on its own, right before any real stranger is let near the site — it touches deploy config and secrets handling, not app logic, and has real prerequisites. **Depends on:** nothing code-wise, but bundle with credential rotation (blockers.md §B.1) when you do it — currently deferred per your own call, revisit before real users arrive. --- **Root cause.** The exposure (`GET api/v1/dev/last_otp/{phone}`, unauthenticated by design, guarded only by `environment.IsDevelopment()`) is correctly implemented in code — the entire gap is that the deployed `docker-compose.yml` sets `ASPNETCORE_ENVIRONMENT: Development`, **deliberately and already documented** (a comment in the compose file and a full section in `DEPLOY.md` spell out why and what depends on it). This is **not** a one-line flip — five real prerequisites, already documented in `DEPLOY.md`, need to land together: 1. Create `server/src/API/Baya.Web.Api/appsettings.Production.json` with real (non-`not-for-production` sentinel) `IdentitySettings:SecretKey`/`Encryptkey` — `StartupSecretsGuard` rejects the sentinel outside Development, so a bare env-var flip with the current file would refuse to boot. **`Seams:FieldEncryption:Key`/`:HashKey` must be copied byte-identical** from the current Development value — never regenerated (root `CLAUDE.md` rule 6 — this key decrypts all existing PII). 2. Set `ASPNETCORE_ENVIRONMENT: Production` in `docker-compose.yml`. 3. Switch migrations from boot-time auto-apply (Development-only branch) to the documented one-shot (`... migrate`) deploy step — and confirm the demo/lifecycle seed data the shared DB currently relies on survives the transition, since those seeders stop running automatically post-flip. 4. Swap `Seams:Sms:Provider` from `telegram` (a broadcast-to-a-fixed-chat-list relay, fine for trusted internal testers, not for strangers) to `kavenegar` with a real API key — flipping only the environment variable closes the `/dev/last_otp` leak but leaves OTPs going out over the Telegram relay instead of to the real phone. 5. Bundle with credential rotation (blockers.md §B.1) when you do it — the same config file carries the DB password alongside the encryption keys. **Flag:** whether to keep the Telegram relay live in parallel with `kavenegar` for internal/test accounts post-launch, or retire it entirely, is a product/ops call, not answered by the code. --- ## Not phases — noted here so they aren't lost, but no code to write - **§B.1 (rotate committed credentials)** — per your call, skipped for now (private repo, single developer). Revisit before any real user touches the deployed site; bundle with step 5 above when you do. - **§B.3 (legal review of Terms/Privacy)** — not a code task; needs an actual legal review. - **§B.4 (مودیان e-invoicing)** — per your call, not built now. When you pick it up: the invoice data and tax math are already correct; the only missing piece is the actual government-facing registration call, and it needs a chosen provider/gateway before any adapter code makes sense. Add a note to forgotten-features.md saying exactly that, rather than building a speculative seam with nothing real to point it at. - **§B.5 (e-namad certification)** — a business/paperwork step, not a code change; also gates the real ZarinPal card-payment path ([07-card-payment-redirect.md](07-card-payment-redirect.md)) from ever going fully live, independent of any code fix. - **§B.6 (real bank-transfer payout rail)** — per your call, stays mocked. Resolved by [11-nurse-payouts.md](11-nurse-payouts.md) — the seam already exists server-side; only the client UI in front of it was missing.