# 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](../../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 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](refinement-phase-4-frontend-de-mock.md)) or build any missing endpoints (that's [Refinement Phase 3](refinement-phase-3-contract-batch.md)) — 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](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](../../../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 mechanism** — `dotnet 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](refinement-phase-5-security-hygiene.md); 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](refinement-phase-1-database-and-seed.md).) ### 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](refinement-phase-8-external-rails.md). 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](../../phases/_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 dev` → `http://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.