Files
baya-monorepo/dev/post-phase/refinement/refinement-phase-0-bring-up.md
T
2026-07-10 20:59:47 +03:30

12 KiB
Raw Blame History

Refinement Phase 0 — Local end-to-end bring-up & the integration seam

Mission: make the client and the server actually talk to each other, once, on one machine, in a repeatable way — and prove it with one real authenticated round-trip. Today the two projects each run, but nothing wires them together: the browser can't call the API (no CORS), the API only starts against a remote database, and the frontend calls almost nothing real. This phase removes the three hard integration blockers and ends with a logged-in session backed by the real backend.

Track: integration (both projects) · Depends on: nothing · Unlocks: every other refinement phase Before you start, read ../../phases/_shared/agent-operating-rules.md.

1. Context — where this sits

The 16-phase backend chain (b0b15) and the 16-phase frontend chain (f0f15) are both complete against their own specs. But they were built in parallel, decoupled lanes that only ever met through the dev/contracts/ documents — they were never run against each other. As a result the app "runs" but is not integrated:

  • The frontend is mock-primary: 21 of its 22 service domains default to an in-browser mock (USE_*_MOCK = true), so a fresh npm run dev calls the backend for auth only. Everything you see — the customer home, search results, bookings — is fake in-memory data.
  • The backend has no CORS, so even the calls the frontend does make (auth) would be blocked by the browser the moment the two run on different origins.
  • The backend only starts against a committed remote SQL Server (see the security phase) — there is no local-database story, so a developer can't stand it up cleanly.

This phase is the foundation: it makes "run both and watch a real request succeed" a 5-minute, documented procedure. It does not flip the frontend mocks (that's Refinement Phase 4) or build any missing endpoints (that's Refinement Phase 3) — it only removes the plumbing blockers and proves the seam works with the one domain that's already real: auth.

What already exists (do not rebuild):

  • Real, wired phone-OTP auth on both sides — client services/auth (USE_AUTH_MOCK = false) ↔ server AuthController/MeController (/api/v1/auth/*, /api/v1/me).
  • The client fetch layer (client/src/lib/api/client.ts) already reads NEXT_PUBLIC_API_URL, attaches the bearer, unwraps the ApiResult envelope, and does silent 401 refresh. It is correct — it just has nothing to talk to.
  • On boot the server applies all 17 EF migrations and seeds roles + an admin user + a sandbox gateway (Program.cs:99-104). Migrations are current, not behind.

2. Required reading (do this first)

  • This directory's README.md — the full problem inventory and how the 10 refinement phases fit together. Read it before anything.
  • server/src/API/Baya.Web.Api/Program.cs — the startup pipeline (note: no UseCors).
  • server/src/API/Baya.Web.Api/appsettings.json / appsettings.Development.json — the connection strings (identical; both point at the remote 87.107.152.16).
  • server/src/API/Baya.Web.Api/Properties/launchSettings.json — the API binds https://localhost:5002 only.
  • client/src/lib/api/client.ts, client/src/config.ts, client/.env.development — the client's API base URL wiring (NEXT_PUBLIC_API_URL = https://localhost:5002).
  • server/src/Infrastructure/Baya.Infrastructure.CrossCutting/Seams/LoggingSmsSender.cs — OTP delivery is a log statement; the code you need to log in is printed to the server console, not sent by SMS.
  • Root CLAUDE.md working agreement #6 (never commit secrets) — the local-DB change here must not add a new committed secret.

3. Scope — build this

3.1 CORS on the server (the #1 blocker)

There is no CORS anywhere in server/ (verified: zero matches for AddCors/UseCors). A browser at http://localhost:3000 calling https://localhost:5002 is blocked by the same-origin policy before the request even reaches a controller.

  • Add a CORS policy via an extension method (per the server's "expose it as an extension method, call it from Program.cs" rule — never inline). Suggested home: Baya.WebFramework/ServiceConfiguration/CorsServiceExtension.cs with AddCorsPolicies(configuration).
  • Read the allowed origins from configuration (Cors:AllowedOrigins string array), defaulting to http://localhost:3000 in Development. Do not use AllowAnyOrigin() together with credentials.
  • The client sends the token as a bearer header (not a cookie) on cross-origin calls, so AllowCredentials() is not strictly required — but allow the Authorization, Content-Type, Accept-Language, and Idempotency-Key headers (the client sets all four) and the methods the API uses.
  • Register app.UseCors(...) in the pipeline after UseRouting() and before UseRateLimiter()/ UseAuthentication() so a pre-flight OPTIONS is answered before auth/limits run.
  • Keep it config-driven so a deployed environment lists its real web origin(s) and Development stays permissive to localhost only.

3.2 A local database story (so the API can start without the remote server)

Today both appsettings files hardcode the remote 87.107.152.16 SQL Server. A developer with no access to that host cannot boot the API at all.

  • Provide a local SQL Server for development. Recommended: a server/docker-compose.yml with mcr.microsoft.com/mssql/server:2022-latest (Developer edition, a dev-only SA_PASSWORD) exposing 1433.
  • Point the Development connection string at that local instance via a non-committed mechanismdotnet user-secrets (the server project already supports it) or environment variables — and replace the committed remote string in appsettings*.json with a placeholder. (The full credential rotation is Refinement Phase 5; here we only need a working, secret-free local default so the app boots.)
  • Document the one-time HTTPS dev-cert trust: dotnet dev-certs https --trust — without it the browser (and fetch) rejects https://localhost:5002 and every call fails with an opaque network error.
  • On boot the server already runs MigrateAsync() + seeders, so pointing at an empty local DB is enough — it self-migrates and self-seeds roles/admin/gateway on first run. (Rich demo data is Refinement Phase 1.)

3.3 A Development-only way to complete OTP login without an SMS gateway

OTP delivery is LoggingSmsSender — it logs the code (server/.../Seams/LoggingSmsSender.cs) instead of sending it. A tester can already read the 6-digit code from the server console, but make this explicit and frictionless:

  • Document the "read the OTP from the server console" procedure in the client and server run docs.
  • Optionally (Development/Testing-gated only) return the code in the request_otp response or expose a tiny GET /api/v1/dev/last_otp/{phone} behind an IsDevelopment() guard, so an automated/e2e flow can complete login. This must be impossible to enable outside Development and is superseded by the real SMS gateway in Refinement Phase 8. Do not weaken the otp rate-limit or the per-phone resend window.

3.4 Client env & run docs

  • Confirm client/.env.development (auto-loaded by npm run dev) has NEXT_PUBLIC_API_URL = https://localhost:5002. It already does — verify it matches the server's actual bound URL. If a developer runs the API on a different port/scheme, they set a client/.env.local override.
  • Add a top-level "Run the whole app locally" section (root README.md or a new dev/post-phase/refinement/RUNBOOK.md) that ties the two npm/dotnet commands, the cert trust, the local DB, and the OTP-from-logs step into one copy-pasteable procedure.

(DEFERRED here, done later): flipping any USE_*_MOCK flag (Phase 4), building any missing endpoint (Phase 3), rotating the leaked credentials (Phase 5), a reverse proxy / production CORS origins (Phase 5/9).

4. Mocks & seams in this phase

No new seams. This phase uses the existing LoggingSmsSender (OTP in logs) as the interim OTP channel and touches no business logic. The only "mock" decision is the optional Development-only OTP echo (3.3), which is a dev affordance, not a seam.

5. Critical rules you must not get wrong

  • CORS middleware order matters. UseCors must sit after UseRouting and before the rate limiter / auth, or pre-flight OPTIONS requests get rejected (429/401) before the browser ever sends the real request.
  • Do not introduce a new committed secret. The local DB password and connection string live in user-secrets/env/compose, never in a committed appsettings*.json. Replacing the remote string with a placeholder is in-scope; committing a new one is not.
  • The Development OTP shortcut is Development-only. Gate it on app.Environment.IsDevelopment() and never log/return the code in any other environment. It is a bridge until real SMS (Phase 8).
  • Change nothing about the money path, auth crypto, or the envelope shape. This is plumbing only.

6. Definition of Done

On top of the shared definition-of-done.md:

  • server/: CORS policy added (config-driven, localhost:3000 in Dev), registered in the correct pipeline position; dotnet build Baya.sln clean, dotnet test Baya.sln green.
  • A developer with no access to the remote server can boot the API against a local SQL Server using only documented steps (compose/user-secrets), and the committed appsettings no longer carry a working remote credential as the default.
  • dotnet dev-certs https --trust and the OTP-from-logs step are documented in the runbook.
  • client/: npm run check green; the client points at the running API; no mock flags changed.
  • The end-to-end proof in §7 passes.

7. How to test (what a human can verify after this phase)

  1. docker compose up (or start local SQL Server) → set the Dev connection string via user-secrets.
  2. cd server && dotnet run --project src/API/Baya.Web.Api/Baya.Web.Api.csproj → API boots, applies migrations, seeds, listens on https://localhost:5002/swagger.
  3. cd client && npm run devhttp://localhost:3000.
  4. Open http://localhost:3000/fa/login, enter a phone number, request the OTP.
  5. Read the 6-digit code from the server console (the LoggingSmsSender line), enter it, submit.
  6. Expected: you land on the customer home; the browser Network tab shows real calls to https://localhost:5002/api/v1/auth/request_otp, /auth/verify_otp, and /api/v1/me all returning 200 with the ApiResult envelope — and no CORS error in the console. This is the first real authenticated round-trip between the two projects.
  7. Negative check: temporarily remove the CORS registration and confirm the same flow now fails with a CORS error in the console — proving the fix is what unblocked it.

8. Hand off & document (close the phase)

  • Update server/CLAUDE.md "Startup wiring" to list the new CORS extension in the pipeline order.
  • Write/append the local-run runbook (§3.4).
  • Report per the operating rules: what was wired, the exact local-run procedure, and confirm auth is the only real domain until Phase 4. Save a memory note on the CORS + local-DB + OTP-in-logs bring-up so the next agent doesn't rediscover the blockers.