12 KiB
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 (b0–b15) and the 16-phase frontend chain (f0–f15) 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 freshnpm run devcalls 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) ↔ serverAuthController/MeController(/api/v1/auth/*,/api/v1/me). - The client fetch layer (
client/src/lib/api/client.ts) already readsNEXT_PUBLIC_API_URL, attaches the bearer, unwraps theApiResultenvelope, 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: noUseCors).server/src/API/Baya.Web.Api/appsettings.json/appsettings.Development.json— the connection strings (identical; both point at the remote87.107.152.16).server/src/API/Baya.Web.Api/Properties/launchSettings.json— the API bindshttps://localhost:5002only.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.cswithAddCorsPolicies(configuration). - Read the allowed origins from configuration (
Cors:AllowedOriginsstring array), defaulting tohttp://localhost:3000in Development. Do not useAllowAnyOrigin()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 theAuthorization,Content-Type,Accept-Language, andIdempotency-Keyheaders (the client sets all four) and the methods the API uses. - Register
app.UseCors(...)in the pipeline afterUseRouting()and beforeUseRateLimiter()/UseAuthentication()so a pre-flightOPTIONSis 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.ymlwithmcr.microsoft.com/mssql/server:2022-latest(Developer edition, a dev-onlySA_PASSWORD) exposing1433. - Point the Development connection string at that local instance via a non-committed mechanism —
dotnet user-secrets(the server project already supports it) or environment variables — and replace the committed remote string inappsettings*.jsonwith 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 (andfetch) rejectshttps://localhost:5002and 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_otpresponse or expose a tinyGET /api/v1/dev/last_otp/{phone}behind anIsDevelopment()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 theotprate-limit or the per-phone resend window.
3.4 Client env & run docs
- Confirm
client/.env.development(auto-loaded bynpm run dev) hasNEXT_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 aclient/.env.localoverride. - Add a top-level "Run the whole app locally" section (root
README.mdor a newdev/post-phase/refinement/RUNBOOK.md) that ties the twonpm/dotnetcommands, 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.
UseCorsmust sit afterUseRoutingand before the rate limiter / auth, or pre-flightOPTIONSrequests 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.slnclean,dotnet test Baya.slngreen.- 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 --trustand the OTP-from-logs step are documented in the runbook.client/:npm run checkgreen; 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)
docker compose up(or start local SQL Server) → set the Dev connection string via user-secrets.cd server && dotnet run --project src/API/Baya.Web.Api/Baya.Web.Api.csproj→ API boots, applies migrations, seeds, listens onhttps://localhost:5002/swagger.cd client && npm run dev→http://localhost:3000.- Open
http://localhost:3000/fa/login, enter a phone number, request the OTP. - Read the 6-digit code from the server console (the
LoggingSmsSenderline), enter it, submit. - 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/meall returning200with theApiResultenvelope — and no CORS error in the console. This is the first real authenticated round-trip between the two projects. - 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.