# Pre-launch — the hard gate before real users touch this

> Last verified: 2026-08-02 against commit `cd8144e`. Populated by phase 5 of the
> [documentation clean-up chain](../../archive/clarify-chain/README.md).

Everything below must be true before a real user with real money uses the platform. This is a gate, not a
backlog — items here are not ranked by convenience, they're ranked by what "real user, real money" requires.
Four things put you here: **committed credentials**, **mocked money/identity rails**, **Development running in
production**, and **legal/tax code that isn't finished**. A fifth — **every Phase 4 blocker** — is included
because "blocker" means *the product is wrong or unusable*, and that bar applies with or without real money.

---

## 1. Rotate the committed credentials

The repo contains live credentials **by deliberate pre-launch decision**
([root CLAUDE.md §6](../../CLAUDE.md), [BL-003](../status/backlog.md#blockers-18)): DB `sa`, both
JWE/field-encryption key halves, Kavenegar, Neshan, Finnotech, and the Telegram bot token. All of them must be
rotated and the secret half moved out of git before onboarding real users.

| Credential | Rotation is | What it takes |
| --- | --- | --- |
| DB `sa` password | a straight rotation | Change on the SQL Server, update `appsettings.Production.json`. No data migration. |
| Kavenegar / Neshan / Finnotech API keys | a straight rotation | Issue new keys with each vendor, update config. No data migration. |
| Telegram bot token | a straight rotation | Regenerate via BotFather, update `docker-compose.yml` (`otp-relay.environment`) and the appsettings copy — the two must match ([DEPLOY.md](../../DEPLOY.md)). |
| `IdentitySettings:SecretKey` / `Encryptkey` (JWE) | a straight rotation | Safe to rotate any time — the only side effect is signing everyone out. |
| **`Seams:FieldEncryption:Key` / `:HashKey`** | **not** a rotation — see below | **Never** change without the migration in the next section. |

### The one item that is a project, not a config edit

`Seams:FieldEncryption:Key` decrypts every encrypted column (phone numbers, addresses, IBANs, clinical
notes); `:HashKey` derives `users.PhoneHash`, which every login looks up. Changing either value in place makes
all existing encrypted data unreadable and locks every account out — this is stated as a permanent invariant
in [DEPLOY.md](../../DEPLOY.md) and [decisions.md](../status/decisions.md), and it is correct **as long as the
key never needs to change**. It will need to change eventually (security incident, key-management policy,
routine hygiene), and at that point it is its own migration project:

1. Decrypt every affected column with the old key, in place, inside a maintenance window or behind a
   dual-key read path.
2. Re-encrypt with the new key.
3. Recompute `PhoneHash` for every user with the new `HashKey`, since it's a one-way derivation — there is no
   way to "re-key" a hash without the plaintext.
4. Decide the cutover strategy: a maintenance-window rewrite (simplest, requires downtime sized to the data
   volume) versus a dual-read migration (no downtime, more code to write and then delete).

**Rough size: large.** Nothing about this is hard, but nothing about it is a config edit either — track it as
its own piece of work with its own testing pass, not a line item inside a generic "rotate credentials" task.

---

## 2. Still-mocked production seams

Exactly one `Seams:*:Provider` is set anywhere in the repo — `Seams:Sms:Provider = telegram`, and the Telegram
relay is [documented as the pre-launch demo rail](../../CLAUDE.md), not a production SMS gateway. Every other
rail runs on its mock, in both development and the deployed stack
([docs/flows/index.md](../flows/index.md#mock-vs-real-map--server-seams)):

| Rail | Mock today | Real adapter | What flipping takes |
| --- | --- | --- | --- |
| SMS / OTP | `LoggingSmsSender` (real path is `telegram`, a demo relay) | `KavenegarSmsSender` — already coded | Set `Seams:Sms:Provider = kavenegar` + real API key/sender. Attempting `smsir`/`ghasedak` throws at startup by design — those adapters don't exist ([BL-235](../status/backlog.md#deferred-43)). |
| Card PSP | `MockPaymentProvider` (redirects to a non-existent host — [BL-005](../status/backlog.md#blockers-18)) | `ZarinPalPaymentProvider` — already coded | Real merchant credentials from a licensed PSP — which requires e-namad (§5) — plus a reachable webhook endpoint. **The selector is a plain string check, not an enum** — any non-`mock` value silently selects ZarinPal, so a typo in this setting picks a real gateway by accident. |
| Settlement split (تسهیم) | `MockSettlementSplitProvider` | `ProviderSettlementSplitProvider` — already coded | Real settlement-provider credentials. |
| BNPL | `MockBnplProvider` (no gateway row even seeded — [BL-007](../status/backlog.md#blockers-18)) | `SnappPayBnplProvider` / `DigipayBnplProvider` — already coded | Real provider credentials, a seeded `Bnpl` gateway row, and a webhook receiver reachable from the provider (nothing fires it in dev today — [BL-131](../status/backlog.md#minor-115)). |
| Object storage | `LocalDiskObjectStorage` (`GetUrl` returns a `file://` URI no browser can fetch — [BL-024](../status/backlog.md#major-86)) | `S3ObjectStorage` — already coded | A real bucket + credentials. Flipping this also happens to fix BL-024, since S3's `GetUrl` returns an actual HTTP(S) URL. |
| Geocoder | `MockGeocoder` (server) / keyless grid stand-in (client, `NEXT_PUBLIC_NESHAN_KEY` unset — [BL-025](../status/backlog.md#major-86)) | `NeshanGeocoder` — already coded | A real Neshan API key on **both** sides — the client key is separate from any server-side one. |
| **Bank transfer (payouts)** | `MockBankTransferProvider` | **none — moves no money** | Nothing to flip. This is the one rail with no real adapter written at all; even after every other rail above goes real, **payouts still cannot move real money** until a real bank-transfer integration is built from scratch. Size this as new work, not a config change. |
| e-invoicing (مودیان) | `MockMoadianClient` | `MoadianClient` — already coded | Token-exchange refresh and the Moadian signing certificate are not wired ([BL-236](../status/backlog.md#deferred-43)) — both are deploy-time actions once real credentials exist, not code changes. |
| Shahkar · e-KYC · IBAN ownership | mocks | Finnotech adapters — already coded | Real Finnotech credentials. |
| Credential (MoH/INO) · eNamad · review moderation | mock, always | **none — deliberate** | These stay manual/mocked by product decision, not a launch gap — see [deferred.md](deferred.md). |
| Search | *(no mock)* | `SqlNurseSearch` | Already real. Not a launch item. |

**Net: of 10 rails with a real adapter, 9 are a credentials-and-config flip; 1 (bank transfer) doesn't have a
real adapter yet.** Two rails throw at startup rather than silently falling back to the mock if misconfigured
(SMS providers other than Kavenegar, and any `Search:Backend` other than `sql`) — that fail-fast behavior is
intentional and should stay.

---

## 3. Running as Development in production

[DEPLOY.md](../../DEPLOY.md) documents this as a **deliberate trade** for a demo deployment among trusted
people. Every consequence below stops being acceptable the moment strangers can reach the site:

| Consequence | Why it matters | Fix |
| --- | --- | --- |
| The developer exception page is public | Any unhandled 500 on `api.balinyaar.ir` returns a stack trace and configuration detail to the caller | Switch to `Production` environment |
| `GET /api/v1/dev/last_otp/{phone}` is live and anonymous ([BL-004](../status/backlog.md#blockers-18)) | Anyone who knows a registered phone number can read its login code and sign in as that user — **the single biggest exposure today** | Same — the endpoint is Development-gated in code; it disappears once the environment flips |
| Swagger served at `/swagger` | Full API surface exposed | Same |
| Seeders re-run on every boot; migrations auto-apply on boot | Safe today (idempotent), but not how a production release process should work | Run migrations as a one-shot command instead (`docker compose run --rm api dotnet Baya.Web.Api.dll migrate`) |
| gRPC reflection enabled; the demo `bookings/convert` payment-capture simulator is wired | Extra attack surface + a fake-payment code path reachable in a real deployment | Same — disappears with the environment flip |

**What must change**, per [DEPLOY.md "Going to Production"](../../DEPLOY.md#going-to-production-later):

1. Set `ASPNETCORE_ENVIRONMENT: Production` in `docker-compose.yml`.
2. Create `appsettings.Production.json` (it does not exist yet) with real `IdentitySettings:SecretKey` /
   `Encryptkey` — `StartupSecretsGuard` rejects any value containing `not-for-production` outside Development,
   so today's dev keys refuse to boot in Production by design. Keep `Seams:FieldEncryption` byte-identical to
   the Development file (§1).
3. Run migrations as a one-shot instead of on boot.
4. Swap the OTP rail to `kavenegar` (§2) — the Telegram relay broadcasting every code to a fixed recipient
   list stops being acceptable once someone outside that list can request a code.

---

## 4. Every Phase 4 blocker

"Blocker" means *the product is wrong or unusable* — that bar holds regardless of whether money is involved.
All 18 are pre-launch gate items; full detail and code traces are in
[backlog.md](../status/backlog.md#blockers-18).

| ID | One line | Rough effort |
| --- | --- | --- |
| BL-001 | Admin RBAC grants only the literal role `admin`; every seeded admin 403s everywhere | M — see [next-up.md](next-up.md) unit 3 |
| BL-002 | No seeded account holds the literal `admin` role, so even a fixed BL-001 is untestable out of the box | S — bundled with BL-001 |
| BL-003 | Committed live credentials, unrotated | See §1 |
| BL-004 | Dev-only OTP-read endpoint live on the production domain | See §3 |
| BL-005 | Card payment is a dead end everywhere — mock redirects to a non-existent host, no local webhook | M — see [next-up.md](next-up.md) unit 2 |
| BL-006 | Booking deadlines ship without a timezone; a 30-min window can render as ~4h and expire silently | S — see [next-up.md](next-up.md) unit 2 |
| BL-007 | No `bnpl` gateway row ever seeded; every BNPL call 400s | S — see [next-up.md](next-up.md) unit 4 |
| BL-008 | BNPL wizard's mock cross-imports the bookings-mock store; dead end for every real booking id | M — see [next-up.md](next-up.md) unit 4 |
| BL-009 | Refunds mock reads a retired store and would show a 10000%-scale refund on flip | M — see [next-up.md](next-up.md) unit 4 |
| BL-010 | Verification is 100% client-mocked; a server-verified nurse renders unverified everywhere | L — see [next-up.md](next-up.md) unit 1 |
| BL-011 | Patient/care records are 100% mocked; real DTO shapes would break on a naive flip | M |
| BL-012 | Nurse payouts: mock hides 4 working endpoints; no UI for the irreversible "process batch" step; "paid" status computed wrong | M — see [next-up.md](next-up.md) unit 5 |
| BL-013 | Partner center 100% mocked, zero real tenancy; 5 core routes don't exist server-side | L |
| BL-014 | Search results are index rows, not de-duplicated nurses; trust dossier mocked; unverified nurse profile page asserts "verified" | M |
| BL-015 | Editing an address silently nulls recipient name/phone/postal code on every save | S |
| BL-016 | A booking swept to `missed` never reaches a payable state | M |
| BL-017 | Reviews can never leave moderation on the live stack (sits behind BL-001; `AutoApproveClean` unset) | S once BL-001 lands |
| BL-018 | Zero option groups outside Development — every builder collapses to two steps, deployed | M |

---

## 5. Legal / tax items that are code-side

From [product/business/13-tax-invoicing-and-legal.md](../../product/business/13-tax-invoicing-and-legal.md),
confirmed the platform's weakest business area in [implemented.md](../status/implemented.md):

- **Terms & Privacy still ship placeholder legal copy behind a draft banner** — flagged for human/legal review
  since ui-phase-3 and still unreviewed ([BL-097](../status/backlog.md#major-86)). Swapping in
  counsel-reviewed copy (and removing the banner) is a pre-launch item, not a nice-to-have.
- **مودیان (e-invoice) integration is mocked past the point the business doc calls MVP.** The `invoices` model,
  VAT split, and reference fields exist correctly (§ commission/VAT model below), but the Moadian
  token-exchange refresh and signing certificate are unwired ([BL-236](../status/backlog.md#deferred-43)), and
  seeded partner-center invoices carry null Moadian reference/PDF fields
  ([BL-200](../status/backlog.md#major-86)). These are deploy-time actions once real Moadian credentials
  exist — but they don't exist yet, and مودیان readiness is explicitly named as MVP scope in the business doc.
- **The commission/VAT split itself is already correct and does not need code work**: platform commission is
  15% of gross, VAT is 10% of the *commission only* — this became the single source of truth in
  refinement-phase-3 ([decisions.md](../status/decisions.md)) and matches the business doc's nurse-as-
  taxable-seller / platform-as-commission-seller model. The one open defect is a **display** bug, not a model
  bug: the invoice screen computes a client-side row that's wrong by exactly the VAT amount instead of
  rendering the server's real total ([BL-060](../status/backlog.md#major-86) — folded into
  [next-up.md](next-up.md) unit 2).
- **`partner_centers` as merchant-of-record is the business doc's launch-critical legal vehicle** ("the fast,
  legal go-to-market is to partner with already-licensed centers") — and it is 100% mocked with zero real
  tenancy today ([BL-013](../status/backlog.md#blockers-18)). Until this is real, there is no functioning
  legal invoice-issuer path for a booking routed through a partner center.
- **e-namad is a business/licensing prerequisite, not a code task, but it gates one**: per
  [legal-landscape.md](../../product/research/legal-landscape.md), a monetized Iranian site needs e-namad to
  obtain an online payment gateway at all — meaning the real ZarinPal flip in §2 cannot complete until the
  launch entity holds e-namad, independent of anything in this codebase.
- **VAT-exempt-or-0% is a live legal question, not yet a code gap**: the business doc notes medical services'
  own VAT treatment is unconfirmed in Iran and asks for the rate to stay config-driven so it can land either
  way. Confirm with an Iranian tax advisor before launch, then confirm the rate is read from a
  `platform_configs` row (the repo's config-is-rows convention) rather than a hardcoded constant — this
  wasn't independently re-verified this phase and is worth a direct check before relying on it.

---

## Not in this file

Everything here is a **gate**, not a roadmap. What comes after the gate — the highest-leverage next units of
work, what's deliberately deferred and why, and what technical debt is accruing — is
[next-up.md](next-up.md), [deferred.md](deferred.md), and [tech-debt.md](tech-debt.md) respectively. Several
pre-launch items and next-up items overlap on purpose (e.g. BL-006, BL-060): the same fix both closes a
backlog item worth doing regardless of launch timing, and clears a launch gate.
