Files
baya-monorepo/docs/roadmap/pre-launch.md
T
2026-08-02 18:33:43 +03:30

15 KiB

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.

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, BL-003): 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).
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 and 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, not a production SMS gateway. Every other rail runs on its mock, in both development and the deployed stack (docs/flows/index.md):

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).
Card PSP MockPaymentProvider (redirects to a non-existent host — BL-005) 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) 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).
Object storage LocalDiskObjectStorage (GetUrl returns a file:// URI no browser can fetch — BL-024) 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) 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) — 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.
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 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) 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":

  1. Set ASPNETCORE_ENVIRONMENT: Production in docker-compose.yml.
  2. Create appsettings.Production.json (it does not exist yet) with real IdentitySettings:SecretKey / EncryptkeyStartupSecretsGuard 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.

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 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 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 unit 2
BL-007 No bnpl gateway row ever seeded; every BNPL call 400s S — see 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 unit 4
BL-009 Refunds mock reads a retired store and would show a 10000%-scale refund on flip M — see next-up.md unit 4
BL-010 Verification is 100% client-mocked; a server-verified nurse renders unverified everywhere L — see 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 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

From product/business/13-tax-invoicing-and-legal.md, confirmed the platform's weakest business area in 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). 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), and seeded partner-center invoices carry null Moadian reference/PDF fields (BL-200). 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) 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 — folded into 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). 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, 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, deferred.md, and 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.