15 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 a database
dotnet user-secrets is no longer used — the <UserSecretsId> was removed from
Baya.Web.Api.csproj, so that store isn't read at all. A leftover secrets.json on your machine is inert
and can be deleted. All configuration lives in
appsettings.Development.json, which
already points at the shared remote database the deployed demo also uses — so a fresh clone boots with
no configuration step at all.
To work against the throwaway local container from step 2 instead, edit ConnectionStrings:SqlServer in
that file (the Password must match MSSQL_SA_PASSWORD in server/docker-compose.yml):
"SqlServer": "Server=localhost,1433;Database=Baya;User Id=sa;Password=Balinyaar_Dev1433;TrustServerCertificate=True;Encrypt=False;"
Env-var alternative (CI/containers, or to avoid a local edit showing up in
git status): setConnectionStrings__SqlServer(double underscore = the:config separator) — it overrides the file. 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;"
Deploying rather than developing? See DEPLOY.md.
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 against the (empty) local DB, then listens on
https://localhost:5002 — Swagger at https://localhost:5002/swagger. In Development it also seeds a
sandbox payment gateway and the demo world (below). It does not seed the old admin/qw123321 account
anymore (refinement-phase-5); a break-glass admin is created only if you set Seed:AdminUsername /
Seed:AdminPassword (see below), and the day-to-day admin path is the phone-OTP demo admins.
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.
On top of that, the lifecycle seeder (DemoLifecycleSeeder) populates every flow's mid-state so manual
testing never starts from an empty world: booking requests in every status, 8 bookings across every reachable
state (incl. a 5-session package mid-engagement and a cancelled-and-refunded one), the balanced ledger, three
refunds (card-succeeded / BNPL-processing / post-payout clawback), a paid and a draft payout batch,
reviews in all moderation states, tickets, notifications, patient care records, a merchant-of-record partner
center, and a mid-pipeline verification case. The console logs Demo lifecycle seeded: … (or already seeded — no-op). The full scenario map + per-flow walkthroughs live in
dev/post-phase/manual-testing-plan.md.
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) |
09120000030 |
partner-center owner | بهنام رستگار (male) | owns مرکز پرستاری آرامش (merchant-of-record, sponsors علی کریمی). No admin/nurse role — log in, then navigate to /partner manually (REQ-038: no /me partner signal yet) |
The old username+password
admin/qw123321account is no longer auto-seeded (refinement-phase-5 — no committed credential). To bootstrap a break-glass username+password admin, add both keys toappsettings.Development.jsonbefore boot, then log in via the API (not the web UI, which is phone-OTP only):"Seed": { "AdminUsername": "admin", "AdminPassword": "<a-strong-password>" }
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.
OTP over Telegram (optional — instead of reading the log)
For manual testing you can have the code arrive on your phone in Telegram rather than in the server
console. A standalone dev-only relay (telegram-otp-bot/) forwards it;
the API talks to it through the normal ISmsSender seam. Development only — the relay broadcasts every
code to every configured chat id, so it is a test-group convenience, not an SMS gateway.
- Start the relay (see its README for creating the bot with @BotFather and discovering chat ids —
each recipient must press Start in Telegram first, then
GET /chat_ids):cd telegram-otp-bot && npm start # no npm install — zero dependenciesapi.telegram.orgis filtered in Iran, so setTELEGRAM_PROXY_URLin its.envto your VPN/proxy client (http://127.0.0.1:10809,socks5://…, or the proxy container on a VPS). The boot banner prints the bot's@username— that line appearing means the token and the proxy work. - Share the secret with the API — the same value on both sides: the relay's
.envAPI_KEYandSeams:Sms:Telegram:ApiKeyinappsettings.Development.json. The appsettings side is already filled in; copy that value into your localtelegram-otp-bot/.env. Do not use the one in.env.example— it is published in git, soTelegramSmsSenderrejects it withSeams:Sms:Telegram:ApiKey is not configured (unset, or still the published example key). - Flip the provider in
server/src/API/Baya.Web.Api/appsettings.Development.json:"Seams": { "Sms": { "Provider": "telegram" } } // committed default is "mock" - Log in as usual — the 6-digit code arrives in Telegram. The
dev/last_otphelper keeps working alongside it (telegramis the one non-mock provider that leaves the capture bridge on), so scripts and e2e tests are unaffected. Set the provider back tomockto return to reading the console.
If the relay is down or reaches nobody it answers 502 and login fails loudly (request_otp returns an
error) rather than pretending an undelivered code was sent.
Good to know
- The API speaks HTTP/1.1 and HTTP/2 (Kestrel
Protocols: Http1AndHttp2, refinement-phase-5 — the previous HTTP/2-only default broke non-TLS HTTP/1.1 hops). Over TLS the client negotiates h2 via ALPN, so gRPC andfetchboth work; plain-HTTP hops fall back to HTTP/1.1. - Secrets fail fast. If
ConnectionStringsis blank or still the base file'sSET_VIA_USER_SECRETS_OR_ENVplaceholder, the API refuses to start withRefusing to start: required secret configuration is missing…. Production/Staging must additionally supply realIdentitySettingsJWE keys andSeams:FieldEncryptionkeys; Development uses the dev-only ones inappsettings.Development.json. - Never change
Seams:FieldEncryption:Key/:HashKey. They decrypt every PII column in the shared database and deriveusers.PhoneHash, which every login looks up. Changing either locks everyone out. - Enable the secret-scan pre-commit hook once per clone so a stray credential can't be committed:
git config core.hooksPath .githooks(see.githooks/README.md). - Behind a reverse proxy, list its address in
ForwardedHeaders:KnownProxies(or a CIDR in:KnownNetworks) so the rate limiter partitions on the real client IP, not the proxy's. - 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: Refusing to start: required secret configuration is missing… |
ConnectionStrings:SqlServer is blank or still a placeholder. Do setup step 3. |
API startup: Login failed for user 'sa' / connect timeout |
DB not up or wrong password — check docker compose ps and that the password in appsettings.Development.json 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" |
Expected — user-secrets was removed. Edit appsettings.Development.json instead. |