17 KiB
Phase 2 — Integration & dependency
Depends on: Phase 0 · Can run in parallel with: Phase 1 · Blocks: Phase 3 · Size: one session
Goal
Make the client↔server dependency a thing you can read in one place, and re-sync the contract layer with the code it stopped tracking on 2026-07-13.
Right now the seam is real but scattered: envelope shape and casing in one contract doc, cookies and
refresh in client/CLAUDE.md, CORS in a refinement report, NEXT_PUBLIC_API_URL in .env files,
the container topology in DEPLOY.md and docker-compose.yml, and the OTP relay in its own README.
Nobody can answer "what does the client actually need from the server?" without reading six files.
Inputs
docs/integration/openapi/swagger.v1.json— the fresh snapshot from Phase 0, plus the added/removed endpoint diff Phase 0 wrote intodocs/_plan/open-contradictions.md.- dev/contracts/ — 18 domain files + 2 convention files + the stale snapshot.
- dev/shared-working-context/frontend/requests/for-backend.md — 67 REQs; many are contract amendments that were delivered but never folded back into the domain docs.
- dev/shared-working-context/reports/mocks-registry.md — which seams are mocked.
- DEPLOY.md · docker-compose.yml · deploy/Caddyfile
- telegram-otp-bot/README.md + INTEGRATION-PROMPT.md
client/.env.development,client/.env.production,server/src/API/Baya.Web.Api/appsettings*.json- Code, for the seam itself:
client/src/lib/api/,client/src/services/*/, the server'sApiResultenvelope, CORS setup, and auth middleware.
Outputs
docs/integration/
index.md THE seam, one page: what crosses the wire and what each side owes the other
api-contract.md envelope, casing, pagination, errors, idempotency, auth headers/cookies
domains/ 22 files, one per service domain — refreshed against the live swagger
openapi/
swagger.v1.json (from Phase 0)
README.md how to regenerate, when it was last taken, endpoint count
config-matrix.md every env var / appsettings key, both projects + docker + the bot
topology.md the runtime dependency graph
Also updated: DEPLOY.md — fix the stale user-secrets reference and link to
docs/integration/topology.md. It stays at root and stays the deploy procedure.
Steps
1. Write index.md first — the seam on one page
Before touching the per-domain detail, answer these in one page:
- Transport: HTTP/JSON over
NEXT_PUBLIC_API_URL; where gRPC exists and whether it is used. - The envelope:
ApiResult—{ isSuccess, statusCode, message, requestId, data }, payload always underdata,requestIdis a W3C trace id (refinement-phase-9). Client unwraps inclientFetch/unwrap(). - Casing: JSON bodies camelCase; URL segments snake_case. (REQ-001 settled this — confirm against the live swagger, don't trust the doc.)
- Pagination:
{ items, total, page, pageSize }; the server bindspageSizecase-insensitively (REQ-010). - Errors: status codes, machine-readable error codes (REQ-003), what the client does with 401 / 403 / 5xx / network.
- Auth: the cookie set, JWE opacity to the client, silent refresh, session rotation and
reuse-detection,
/meandme/select_role, role hydration. - Idempotency: which endpoints require
Idempotency-Keyand what the client generates. - Money: IRR on the wire, Toman at the UI boundary — link to
docs/rules/shared/. - What the server owes the client and what the client owes the server, as two short lists.
2. Refresh the 22 domain contracts against the live swagger
For each domain, compare the contract doc against swagger.v1.json and the handler code. Mark each
endpoint: matches · drifted (describe) · undocumented (in code, not in contract) ·
phantom (in contract, not in code).
Fold in the delivered REQs — 17+ were delivered in refinement-phase-3 and amended shapes that the domain docs still describe the old way. The domain doc is the thing that should be true; the REQ ledger is a change log. After this phase, a reader should never need to read the REQ file to know the current shape.
Note the domain-file cleanup: there is both a messaging.md (851 B stub) and a
messaging-notifications-admin.md (10.6 KB) — merge. Align the file set with the client's 22
services/ domains so the mapping is one-to-one where it can be.
Anything you cannot verify from swagger or code: mark UNVERIFIED: and add a row to Phase 4's input
list. Do not guess a shape.
3. Write config-matrix.md
One table: key · where it's set (appsettings / .env / docker-compose / Caddyfile) · consumed by · required? · default · notes. Cover at minimum:
NEXT_PUBLIC_API_URL,NEXT_PUBLIC_NESHAN_KEY, and the rest of the client'sNEXT_PUBLIC_*IdentitySettings:SecretKey/:EncryptkeySeams:FieldEncryption:Key/:HashKey— flag as load-bearing and immutable- the connection strings (app DB + log DB), and that the DB is remote and not containerised
OpenTelemetry:Otlp:Endpoint, the health endpoints (/healthz/live,/healthz/ready)- the seam selectors that choose real vendor adapters vs mocks (refinement-phase-8)
- the Telegram OTP bot's token/chat config
- CORS origins, and the Caddy hostnames
balinyaar.ir/api.balinyaar.ir
State plainly, once, that config lives in files and dotnet user-secrets is not used — and that this
is a deliberate pre-launch trade with live credentials in git (link to DEPLOY.md "Going to
Production" and to Phase 5's pre-launch.md).
4. Write topology.md
The runtime dependency graph — a mermaid diagram plus a short table:
browser -> Caddy (caddy_net) -> client container (Next.js) -> server container (ASP.NET)
-> server container -> remote SQL Server (not containerised)
-> telegram-otp-bot (OTP relay)
-> object storage / external rails (per seam config)
For each edge: what flows over it, what breaks if it is down, and where it is configured. This is the "dependency between projects, documented in a proper place" deliverable.
5. Point the old locations at the new one
dev/contracts/README.md gets a one-line "moved to docs/integration/" banner (Phase 6 archives the
folder; until then, don't leave two live copies competing).
Verification
- Every endpoint in
swagger.v1.jsonappears in exactly onedocs/integration/domains/*.md, or is listed in that file's "undocumented, intentionally" section with a reason. - No
phantomendpoints remain undeclared — each is either removed from the doc or listed as "planned, not built" with a backlog reference for Phase 4. config-matrix.mdaccounts for every key inappsettings.Production.json,docker-compose.yml, and both.envfiles — diff mechanically, don't eyeball.DEPLOY.mdno longer instructs anyone to useuser-secrets.index.mdfits on one screen-and-a-half and answers the seam question without a single click.
Definition of done
A frontend agent can build against the API by reading docs/integration/index.md plus one domain
file, and a deploy question is answered by topology.md + config-matrix.md without opening
docker-compose.yml.
Handoff
Run 2026-07-30 against commit
d3ec723. All verification items pass, checked mechanically (script in the session scratchpad, results reproduced below).
What shipped
docs/integration/
index.md 153 lines — the whole seam, one screen and a half
api-contract.md envelope · casing · status codes · auth · pagination · idempotency · money · enums · rate limits
config-matrix.md every key in both appsettings, docker-compose, 3 .env files, the bot, Caddy, CORS
topology.md mermaid graph + a 10-edge table + ports + startup order + the local variant
domains/
index.md the census, the route-shape exceptions, the enum map, what-replaced-what
<22 files> one per client services/ domain, 1:1
openapi/README.md updated: C-9 resolved, the servers-block provenance wrinkle recorded
Also updated: DEPLOY.md (links topology + config-matrix; states there is no appsettings.Production.json
so step 2 of "Going to Production" is a create), docs/README.md (integration → written),
dev/contracts/README.md + openapi/README.md + domains/messaging.md (moved/merged banners).
Verification
| Item | Result |
|---|---|
Every swagger path declared in exactly one domains/*.md |
178/178, 0 unreferenced, 0 duplicated |
| Operation census adds up | 186 = 184 domain + 2 ping in api-contract.md |
| Every phantom declared with a reason + REQ | 24/24 |
config-matrix.md accounts for every key |
Diffed mechanically; 5 gaps found and written up |
DEPLOY.md no longer instructs user-secrets |
Pass — it already documented the removal; now also links the new files |
index.md fits a screen and a half |
153 lines |
Every doc carries a Last verified stamp |
27/27 |
| No reference file over 400 lines | Pass |
| Relative links resolve | 216 checked, 0 broken |
Scope note
api-contract.md was merged into index.md's siblings as planned, but the plan's domains/ count of
22 was met by re-cutting the file set along the client's domain boundaries rather than the backend
phases the old 17 files used. That is what makes the mapping 1:1 and is why the file names changed.
swagger.v1.json was not re-fetched — Phase 0's 2026-07-29 snapshot is 1 day older than this run and
the server was not booted. Everything here is derived from that snapshot plus current source.
Drift list — Phase 4's input
A. Phantom endpoints (24). The client's real API clients call 24 routes the server does not expose. Each is declared in its domain file with its REQ. Grouped by REQ:
| REQ | Status | Phantoms | Domain |
|---|---|---|---|
| REQ-031 | deferred | 3 (admin_roles/*) |
admin |
| REQ-061 | never filed | 2 (admin_users/search, /lookup) |
admin |
| REQ-022 | partial | 3 (checkout_bnpl/options, /schedule, /wallet_installments) |
bnpl |
| REQ-032 | partial | 5 (centers/me/*, partner-centers/{id}/nurses) |
partner-center |
| REQ-033 | partial | 1 (centers/me/settlement) |
partner-center |
| REQ-064 | open | 1 (centers/me/bookings/{id}) |
partner-center |
| REQ-034 | deferred | 3 (admin_verifications documents-url / approve / reject) |
verification |
| REQ-035 | deferred | 3 (admin_refunds preview / approve / reject) |
refunds |
| REQ-036 | deferred | 1 (admin_payouts/{id}/transfer_reference) |
payouts |
| REQ-047 | open | 1 (bookings/payment_history) |
payment |
| REQ-048 | open | 1 (refunds/my) |
refunds |
| REQ-063 | open | 1 (tickets/{id}/assign) |
tickets |
Two are live 404s because their domain's mock is off: bookings/payment_history (payment) and
tickets/{id}/assign (tickets). Both are guarded in the client, so neither is user-visible — but they are
reachable, not hypothetical.
B. Five REQs whose filed status is wrong or narrower than reality. Each was checked against the swagger, not against another document. Phase 4 should re-triage these rather than copy the ledger.
| REQ | Filed as | Actually |
|---|---|---|
| REQ-050 | open — "the list DTO carries neither field" | BookingRequestListItemDto has variantLabel (and patientAge). It lacks variantPrice/variantPriceUnit and the status=answered group filter. The REQ was verified against the client type, not the wire |
| REQ-063 | open — "no close/reopen/assign routes yet" | tickets/{id}/close and /reopen exist and are wired. Only assign is missing. tickets/constants.ts carries the stale reason |
| REQ-066 | open — needs an anonymous search read | GET /search/nurses is already anonymous. What is missing is the rate limit — SearchController has no [EnableRateLimiting] |
| REQ-067 | open — needs an anonymous profile read | GET /nurses/{id}/profile is already anonymous. What is missing is the privacy review |
| REQ-029 / REQ-030 | delivered | Correct — but admin/constants.ts still names them as reasons the admin mock is primary. The only live reasons are REQ-031 and REQ-061 |
C. Client-side drifts (client work, not contract work).
| # | Where | What |
|---|---|---|
| 1 | client/src/lib/api/types.ts |
ApiEnvelope<T> omits code, which the server sends and clientFetch already reads at runtime. Incomplete type, no defect |
| 2 | client/src/services/admin/apis/clientApi.ts |
pageQuery() sends page_size; these endpoints declare PageSize. Model binding is case-insensitive, not separator-insensitive, so it binds nothing and every admin list silently gets the default page size. Latent until USE_ADMIN_MOCK flips |
| 3 | client/src/services/bookingRequests/types.ts |
Marks variantLabel as client-augmented/absent; the server serves it. Widening the type would light up the redesigned inbox card today |
| 4 | client/src/services/payouts/apis/clientApi.ts |
Comment says NursePayoutHistoryDto has no failureReason; the wire has it on all three payout read models |
| 5 | client/src/services/tickets/constants.ts |
Says the backend has no close/reopen/assign; close and reopen exist (B, above) |
| 6 | client/src/services/payouts/apis/clientApi.ts |
Sends Idempotency-Key on admin_payouts process/retry, where the server never reads it. Decorative, not harmful |
| 7 | client/.env.sample |
Still NEXT_PUBLIC_API_URL = https://localhost:5002 — the https half of C-3, in the file a fresh clone copies. .env.development is correct |
| 8 | client/src/services/profiles/ |
POST customer_profiles/avatar exists and is live; the client only wires the nurse route, so a customer cannot set a photo |
D. Cross-side vocabulary asymmetry (1). The client's TicketAuthorRole includes system;
Entities/Messaging/TicketCodes.cs defines only customer/nurse/admin. Safe on the reading side, but a
reader of the client types would wrongly conclude the server emits it. All other 17 shared vocabularies
match exactly, both directions.
E. Route-shape exceptions (4). admin/partner-centers (+ /set-active, /sponsor-nurse),
admin/tickets, admin/reviews/… and internal/bookings/… hardcode route strings instead of using the
[controller]/[action] tokens; three introduce hyphens, against the snake_case rule. Because the route
also derives the dynamic-permission key, normalising them breaks permissions as well as URLs — recorded,
not fixed.
F. Config gaps (5). Written up in
config-matrix.md § What the diff found:
NEXT_PUBLIC_EVV_MOCK_GPS and NEXT_PUBLIC_VERSION read but declared in no .env;
OpenTelemetry:Otlp:Endpoint, Search:Backend and Seed:Admin* read but set nowhere (and a bad
Search:Backend throws at startup); .env.sample's stale https; the Telegram shared secret
duplicated across two files with nothing keeping them equal; no appsettings.Production.json.
G. Unfiled REQ (1). ui-phase-11-report.md records "REQ-061…064 appended" to the ledger. Only
062/063/064 were. Ten live client files cite REQ-061 for the admin user directory
(admin_users/search, admin_users/lookup). Phase 4 must file it, not assume it is tracked.
Contradictions resolved here
C-6 · C-7 · C-8 · C-9 closed; C-2 closed as a documentation decision; C-3's contract-layer half closed. Decisions recorded in open-contradictions.md § Resolved.
What Phase 3 should know
- The seam facts it needs are settled and mechanically verified: camelCase bodies, snake_case URLs, bearer
header (not cookie auth), money as a digit string outbound and an int64 inbound,
Idempotency-Keyon exactly two endpoints, 20 anonymous operations. GET /api/v1/dev/last_otp/{phone}is live onapi.balinyaar.ir— the deployment runs as Development with a capture-safe SMS provider, so both gate conditions hold. Any flow doc that describes logging in should say this is how you do it today, and that it is the deployment's largest exposure.- C-4 (the
dev-certsstep) is confirmed dead: the API binds plain HTTP locally, so there is no certificate to trust. C-5's two bring-up worlds are real and give different data — the committed dev config points at the same remote, already-seeded database the deployment uses.