Files
baya-monorepo/archive/docs/flows/public-front-door.md
T
2026-08-02 20:01:31 +03:30

12 KiB
Raw Blame History

Flow — public front door

Last verified: 2026-08-02 against commit c841bde

Actor(s): guest (unauthenticated) · Status: partial — what is built works; tier (c) guest browse is not built Client: real (rewrite, gating and ?next= all verified against a production build) · Server: real Business source: no dedicated business area — the depth decision lives in product/notes/open-questions.md § "Decided — public guest-browse depth"; the nearest requirement is "a customer can register and browse with only a verified phone" in product/business/01-actors-and-onboarding.md Integration: docs/integration/api-contract.md § The anonymous surface · domains/search.md

What it does

A visitor with no session lands on / and is meant to see a marketing landing — what Balinyaar is, the service categories, how escrow works, why nurses are trusted — plus a nurse-recruitment CTA and the legal pages. Everything else redirects to login, carrying the attempted URL so a deep link survives the round trip. Guest browse (searching nurses, opening a public profile without logging in) was deliberately not built: tier (c) is still deferred.

Screens

Step Route Component / notes
1 /fa (guest) Middleware rewrites — never redirects — to /{locale}/welcome, so the URL and SEO canonical stay / (client/middleware.ts:35-37)
1 /fa (signed in) Falls through to the customer Home; a nurse/admin hitting / is a RoleGuard mismatch and is bounced to their own app
2 /fa/welcome (public-routes)/welcome/WelcomeScreen.tsx — RSC with zero query hooks; hero, 5 static category tiles (linking to login, not to search), 3-step "how it works" with the verbatim EscrowNotice, 4-row trust explainer, nurse CTA (/login?role=nurse), footer. A signed-in hit here 307s to / (middleware.ts:40-42)
3 /fa/terms, /fa/privacy Static RSC legal copy, 8 / 7 sections, both fronted by a «پیش‌نویس» draft banner — not legally reviewed. Linked from the welcome footer and the login consent line
4 /fa/login Phone-OTP entry — see the auth flow, not this file
5 any unmatched /fa/* [...rest]/page.tsx calls notFound() → branded [locale]/not-found.tsx («صفحه پیدا نشد»)
render throw [locale]/error.tsx («مشکلی پیش آمد») inside the locale providers; app/global-error.tsx above it
/robots.txt, /sitemap.xml app/robots.ts disallows 13 private roots per locale; app/sitemap.ts emits only '', /login, /terms, /privacy × 2 locales

PUBLIC_PATHS = ['/login','/terms','/privacy','/welcome'] (src/constants/routes.ts:210), matched with startsWith. ROUTES.HOME ('/') must never be addedstartsWith('/') would un-gate every route. That trap is why the guest root is an exact-match rewrite instead of a PUBLIC_PATHS entry.

API

The front door itself calls nothing — welcome, terms and privacy are static Server Components. The 20 anonymous server operations exist for other flows (and for the unbuilt tier c). Shapes: api-contract.md.

Call Endpoint Notes
OTP pair POST /auth/request_otp, POST /auth/verify_otp the only anonymous ops the front door actually reaches, via /login
Reference reads GET /catalog/categories, /catalog/option_groups, /geo/{provinces,cities,districts,tree} probed without a token → 200. The welcome grid does not use them (static i18n labels)
Public nurse reads GET /nurses/{id}/profile, /trust_badge, /reviews, /review_tags, /nurse_variants/get/{id} anonymous today, but no guest UI consumes them
Search GET /search/nurses anonymous; requires cityId and serviceCategoryId > 0 — bare call returns 400, so there is no open browse
Ping GET /ping/get_status, /get_status_rate_limited anonymous liveness
Webhooks POST /webhooks/payments/{p}, /webhooks_bnpl/{p}, /webhooks/payouts/{p} provider callbacks, not a UI surface
Dev GET /dev/last_otp/{phone} Development-only, anonymous — see testing-setup.md

Rules that must hold

Rule Source
/ forks by auth with a rewrite, never a redirect — the guest URL and canonical stay / middleware.ts:31-37, welcome generateMetadata sets alternates.canonical = '/{locale}'
'/' is never in PUBLIC_PATHS (the startsWith trap) routes.ts:205-210, client/CLAUDE.md hard rule 16
The middleware auth check is UX onlyisTokenAlive cannot read a JWE claim, so any well-formed 5-part token counts as alive. Never a security boundary client/CLAUDE.md hard rule 17
?next= is validated same-origin and role-permitting on the way out (resolvePostLoginDestination); /partner* is never a valid target services/auth/routing.ts
The escrow line on the landing is product-mandated verbatim fa copy, never paraphrased EscrowNotice.tsx:8-9
Guest browse depth = tiers (a)+(b) only; tier (c) is deferred pending REQ-066/067 + a privacy sign-off open-questions.md §3.1
Welcome must never wait on an API call — RSC, zero query hooks in the whole tree WelcomeScreen.tsx:22-30

Tier (c) is still deferred — confirmed. REQ-066/067 remain open, but narrower than filed: both endpoints are already anonymous on the server (probed below). What is missing is (a) the rate limit — SearchController/NursesController carry no [EnableRateLimiting], so guest traffic falls to the 100/min global per-IP limiter — and (b) the privacy review of the profile payload. See domains/search.md:80-81. No guest search/profile route exists in the client.

How to test

  1. Log in as nobody — this flow is the logged-out surface. Boot per testing-setup.md; use a private window (or clear access_token/refresh_token).
  2. Prove the anonymous surface, no token. All seven returned 200:
    for u in nurses/1/profile nurses/1/trust_badge "nurses/1/reviews?page=1&pageSize=2" \
             catalog/categories geo/provinces ping/get_status nurses/3/profile; do
      curl -s --noproxy '*' -o /dev/null -w "$u %{http_code}\n" "http://localhost:5002/api/v1/$u"; done
    
    Expect: seven 200s. nurses/1/trust_badgeisVerified: true, credentialTypes: ["criminal_record","moh_competency_license"]; nurses/1/reviewsaverageRating: 5.00, publishedCount: 1. A bare GET /search/nurses400 («City Id must be greater than 0») — that is correct, not a defect.
  3. Open http://localhost:3000/fa/welcome. Expect: the marketing landing — brand lockup + tagline, «شروع کنید» CTA, 5 category tiles, «چطور کار می‌کند» with the escrow callout, the trust card, the nurse CTA, and terms/privacy footer links. Tab title is plain «بالین‌یار» (welcome sets no title).
  4. Open /fa. Expect: the same body as step 3 with the URL still /fa — the rewrite, not a redirect.
  5. Open /fa/terms and /fa/privacy. Expect: 8 and 7 numbered sections, each under the blue «این متن پیش‌نویس است…» draft banner.
  6. Open /fa/zzz-nope logged out. Expect: 307 → /fa/login?next=%2Fzzz-nope — the auth gate runs before routing, so a guest never reaches the branded «صفحه پیدا نشد» card. Log in first to see it.
  7. Open /fa/bookings logged out. Expect: 307 → /fa/login?next=%2Fbookings.
  8. curl /robots.txt and /sitemap.xml. Expect: 200; robots disallows /*/bookings, /*/nurse, /*/admin, … ; the sitemap lists exactly 4 URLs × 2 hreflang alternates.

Run these against a production build, not next dev. cd client && npm run build && PORT=3001 npm run start. Under next dev bare / returns 404 (a Turbopack root-path quirk) and stale .next/ prerenders can be served ahead of the middleware, which makes every gate above look broken when it is not.

Verified 2026-08-02 against npm run build (exit 0) on :3001: /307 → /fa · guest /fa200 and byte-identical to /fa/welcome bar the URL (213 894 vs 213 906 chars) · /fa/{bookings,profile,wallet, admin,nurse,select-role,zzz-nope}307 → /fa/login?next=… each · /bookings307 → /fa/bookings · /robots.txt and /sitemap.xml200.

Known gaps

  • next dev cannot be used to check any of this — and it looks broken when it is not. On the dev server every gate above appeared to fail: /fa served the customer Home's metadata (<title>اپلیکیشن خانواده | بالین‌یار</title>) instead of the welcome page, /bookings returned 200 rather than 307, /fa/{profile,wallet,admin,nurse,select-role} all returned 200, and no Link: …hreflang header was emitted even though middleware.ts:62-65 always copies one on. Every response carried x-nextjs-cache: HIT / x-nextjs-prerender: 1 — stale prerenders being served ahead of the middleware. A fresh npm run build + npm run start on :3001 reproduced none of it; all gates behave exactly as coded. The gap is in the dev-server experience, not the product: anyone auditing guest routing against next dev will file bugs that do not exist.
  • GET /api/v1/nurses/{id}/profile returns avatarUrl as a file:///C:/Users/<user>/AppData/Local/Temp/ balinyaar-object-storage/avatars/nurse/1/…png — a local filesystem path, to an anonymous caller. A browser cannot load it, and it discloses the host path. Deployed it becomes file:///app/data/…, equally unusable. Blocks REQ-067 and breaks any avatar on a public profile.
  • GET /api/v1/nurses/3/profile returns 200 anonymously for the unverified nurse (مریم احمدی, in_review, isBookable: false) — the persona that "must never appear in search". Search correctly hides her; the by-id profile read does not. Enumerable by incrementing nurseId.
  • No anonymous rate limit on the public reads: SearchController and NursesController carry no [EnableRateLimiting], so guest traffic falls to the shared 100/min global per-IP limiter (REQ-066).
  • /terms and /privacy ship placeholder legal copy behind a draft banner — flagged for human/legal review since ui-phase-3 and still unreviewed.
  • Tier (c) unbuilt: there is no guest search-results screen, no public nurse profile route, and no guest→login handoff at the «درخواست رزرو» tap. The welcome category tiles link to /login, not /search.
  • welcome/opengraph-image.tsx is Latin-onlyImageResponse's fallback font has no Persian glyphs, so the fa OG card cannot show the Persian tagline until a Mikhak font buffer is embedded.
  • /fa/welcome has no in-app linkROUTES.WELCOME appears only in middleware.ts. It is reachable only as the rewrite body of / or by typing the URL, so the rewrite failing hides the page entirely.
  • NEXT_PUBLIC_SITE_URL is unset in .env.development, so dev robots.txt/sitemap.xml/OG tags fall back to http://localhost:3000. Correct in .env.production; only a dev-preview caveat.
  • GET /api/v1/dev/last_otp/{phone} is anonymous and Development-only, and the deployment runs as Development — so it is live on api.balinyaar.ir, returning any registered phone's login code.