9.9 KiB
Run the whole app locally — Balinyaar bring-up runbook
The one copy-pasteable procedure for standing up both projects on one machine and watching a real, authenticated request cross the wire. Established by Refinement Phase 0 (refinement-phase-0-bring-up.md).
After this you have: the API on https://localhost:5002 (against a local SQL Server), the web client on
http://localhost:3000, and a working phone-OTP login backed by the real backend. Auth is the only real
domain until Refinement Phase 4 — everything else in the UI is
still an in-browser mock.
Prerequisites
- .NET 10 SDK (
dotnet --version) — preview is fine. - Node.js 20+ and npm (
node --version). - Docker (Desktop or engine) for the local database — or your own SQL Server on
localhost:1433.
One-time setup
1. Trust the ASP.NET Core HTTPS dev certificate
Without this the browser (and fetch) rejects https://localhost:5002 and every call fails with an opaque
network error.
dotnet dev-certs https --trust
Accept the OS prompt. (macOS/Windows trust the cert; on Linux see the .NET docs for the per-distro step.)
2. Start a local SQL Server
From server/:
cd server
docker compose up -d
This runs SQL Server 2022 (Developer edition) on localhost:1433 with a dev-only SA password
(Balinyaar_Dev1433, defined in server/docker-compose.yml — not a secret, never used in production).
Give it ~20–30s on first start (docker compose ps shows healthy).
Already have a SQL Server? Skip this and point the connection string in step 3 at it instead.
3. Point the API at the local database (via user-secrets — never a committed file)
The committed appsettings*.json carry a placeholder connection string on purpose. Supply the real
local one through dotnet user-secrets so no working credential ever lands in git. From the API project:
cd server/src/API/Baya.Web.Api
dotnet user-secrets set "ConnectionStrings:SqlServer" "Server=localhost,1433;Database=Baya;User Id=sa;Password=Balinyaar_Dev1433;TrustServerCertificate=True;Encrypt=False;"
The Password must match MSSQL_SA_PASSWORD in docker-compose.yml. User-secrets auto-load only in the
Development environment, so this never affects a deployed build.
Env-var alternative (e.g. for CI/containers): set
ConnectionStrings__SqlServer(double underscore = the:config separator) instead of using user-secrets. PowerShell:$env:ConnectionStrings__SqlServer = "Server=localhost,1433;Database=Baya;User Id=sa;Password=Balinyaar_Dev1433;TrustServerCertificate=True;Encrypt=False;"bash:export ConnectionStrings__SqlServer="Server=localhost,1433;Database=Baya;User Id=sa;Password=Balinyaar_Dev1433;TrustServerCertificate=True;Encrypt=False;"
Run it (two terminals)
Terminal 1 — backend (from server/)
dotnet run --project src/API/Baya.Web.Api/Baya.Web.Api.csproj
On boot the API applies all EF migrations and seeds roles + an admin user + a sandbox payment gateway
against the (empty) local DB, then listens on https://localhost:5002 — Swagger at
https://localhost:5002/swagger.
In Development it additionally runs the demo-world seeder (Refinement Phase 1): verified/unverified
demo nurses with priced variants + Tehran coverage (and therefore real nurse_search_index rows), plus demo
customers with patients and addresses. The console logs Demo world seeded: 3 nurse(s), 2 customer(s)… (or
already seeded — no-op on a subsequent run). It is idempotent and never runs outside Development.
Terminal 2 — frontend (from client/)
cd client
npm install # first time only
npm run dev
Serves http://localhost:3000. It reads the API base URL from client/.env.development
(NEXT_PUBLIC_API_URL = https://localhost:5002). If you run the API on a different port/scheme, create
client/.env.local with your NEXT_PUBLIC_API_URL=… (it overrides .env.development, is git-ignored).
Demo accounts (Development seed)
The demo seeder creates these loginable accounts (phone-OTP; any 6-digit code you read from the console/dev endpoint works). Use them to see the real path populated:
| Phone | Role | Who | State |
|---|---|---|---|
09120000001 |
nurse | زهرا عزیزی (female) | verified, 3 variants, whole-city + 2 districts |
09120000002 |
nurse | علی کریمی (male) | verified, 2 variants, 3 districts |
09120000003 |
nurse | مریم احمدی (female) | unverified — not discoverable in search |
09120000010 |
customer | سارا محمدی (female) | 2 patients, 1 Tehran address |
09120000011 |
customer | رضا حسینی (male) | 1 patient, 1 Tehran address |
09120000020 |
admin (super_admin) |
نگار مدیری (female) | full backoffice — lands on /admin, sees every console incl. RBAC |
09120000021 |
admin (finance) |
کامران مالی (male) | scoped backoffice — lands on /admin, sidebar shows only the money consoles (useAdminCapabilities gating) |
admin / qw123321 |
admin | reference super-admin (username+password) | not a phone-OTP login — the frontend uses the phone admins above |
The phone-OTP admins (09120000020 / 09120000021, refinement-phase-2) are how you reach the /admin
console through the same web login flow as everyone else — admin sub-roles are server-granted, never
self-selectable via me/select_role. Log in with either phone exactly like a nurse/customer; role
hydration routes you to /admin. To reach the nurse app, log in as a verified nurse phone
(09120000001); a fresh customer can also become a nurse in-app (SelectRole → me/select_role) and is
then routed to /nurse after the next /me.
Prove search works without the frontend: open Swagger →
GET /api/v1/search/nurses?service_category_id=1&city_id=101 returns the two verified nurses' variants;
service_category_id=3 (only the unverified nurse) returns an empty page.
Log in (the real round-trip)
- Open
http://localhost:3000/fa/login. - Enter an Iranian mobile number (e.g. the verified nurse
09120000001, or any demo phone above) and request the code. - Get the 6-digit OTP one of two ways:
- Read the server console (Terminal 1) — SMS is mocked, so the code is logged:
MOCK SMS — OTP code 123456 for phone ending in 0001. - Or hit the Development-only helper (handy for scripts/e2e):
GET https://localhost:5002/api/v1/dev/last_otp/09120000001→{ "data": { "phone": "09120000001", "code": "123456" }, ... }. This endpoint returns 404 outside Development and is superseded by real SMS in Refinement Phase 8.
- Read the server console (Terminal 1) — SMS is mocked, so the code is logged:
- Enter the code and submit → role hydration routes you to the app for your role: a customer to the family
home (
/), a nurse to/nurse, an admin to/admin(refinement-phase-2). - Verify in DevTools → Network:
POST /api/v1/auth/request_otp,POST /api/v1/auth/verify_otp, andGET /api/v1/meall return 200 with theApiResultenvelope, and there is no CORS error in the console. That is the first real authenticated request between the two projects.
Good to know
- The API speaks HTTP/2 (Kestrel
Protocols: Http2, for gRPC). Browsers negotiate h2-over-TLS automatically, sofetchjust works; forcurladd--http2. - Only
authis real by default. 21 of 22 client service domains default to an in-browser mock (USE_*_MOCK = true); the home, search, bookings, etc. are fake in-memory data until Refinement Phase 4. - The DB self-migrates + self-seeds, so pointing at an empty local instance is enough — including the Development demo world (nurses, variants, search rows, customers) from Refinement Phase 1.
- Explicit migration path (deploy / migrations-off-boot). Boot-time
MigrateAsyncis the dev convenience; to apply migrations without booting the app (the direction Refinement Phase 7 will formalise), run fromserver/:(dotnet ef database update --project src/Infrastructure/Baya.Infrastructure.Persistence --startup-project src/API/Baya.Web.Apidotnet tool install --global dotnet-efif theefcommand is missing. It reads the same connection string, so set the user-secret / env var first.) This applies schema only — the reference + demo seeds run on the next app boot. - Allowed browser origins are configuration-driven (
Cors:AllowedOrigins), defaulting tohttp://localhost:3000in Development. A deployed environment lists its real web origin(s).
Stopping / resetting
docker compose down # stop the DB, keep its data
docker compose down -v # stop the DB and wipe the volume (fresh migrate + reference + demo seed next run)
Re-running dotnet run against an existing seeded DB does not duplicate anything — both the reference and
the demo seeders are idempotent (the demo seeder guards each persona on its phone number). To get a clean demo
world, wipe the volume (docker compose down -v) and boot again.
Troubleshooting
| Symptom | Fix |
|---|---|
Browser: net::ERR_CERT_AUTHORITY_INVALID on :5002 |
Run dotnet dev-certs https --trust (setup step 1). |
API startup: Login failed for user 'sa' / connect timeout |
DB not up or wrong password — check docker compose ps and that the user-secrets password matches docker-compose.yml. |
Console: ...has been blocked by CORS policy |
UseCors missing/mis-ordered, or the browser origin isn't in Cors:AllowedOrigins. It must sit after UseRouting and before the rate limiter. |
dotnet user-secrets errors with "could not find UserSecretsId" |
Run it from server/src/API/Baya.Web.Api (the project with <UserSecretsId>). |