13 KiB
Balinyaar Server
The backend API of Balinyaar, a trust-first home-nursing marketplace in Iran. It owns the booking lifecycle, an escrow-style double-entry ledger, weekly nurse payouts, the nurse verification pipeline, and every piece of encrypted PII and clinical data on the platform.
Last verified: 2026-07-30 against commit
d3ec723.
- Repo-wide context and the frontend → root CLAUDE.md
- Current product truth (what to test, what's blocking, what's missing) →
mvp/ - Business rules in depth (archived reference, not actively maintained) →
archive/product/. Read the relevant doc before designing an entity, feature, or endpoint — don't infer a business rule from code.
Stack
- ASP.NET Core / .NET 10 (
net10.0), Web API - Clean Architecture (Domain → Application → Infrastructure → API)
- CQRS with Mediator (
martinothamar/Mediator— source-generator based, not MediatR). UseISender/ICommand/IQuery; any prose that says "MediatR" is wrong. - EF Core 10 + SQL Server (Repository + Unit of Work)
- ASP.NET Core Identity with JWE (signed + AES-128-encrypted JWT), phone-OTP, and dynamic permission authorization
- Mapster for mapping, FluentValidation for validation, Serilog for structured logging
- OpenTelemetry (metrics at
/metrics, tracing, opt-in OTLP), NSwag for OpenAPI, Asp.Versioning - xUnit + NSubstitute for tests
- All NuGet versions are centrally pinned in
Directory.Packages.props
Commands (run from server/)
| Task | Command |
|---|---|
| Restore / build | dotnet restore Baya.sln · dotnet build Baya.sln |
| Run API | dotnet run --project src/API/Baya.Web.Api/Baya.Web.Api.csproj |
| Test | dotnet test Baya.sln |
| Apply migrations (deploy-time one-shot) | dotnet run --project src/API/Baya.Web.Api/Baya.Web.Api.csproj -- migrate |
| Add migration | dotnet ef migrations add <Name> --project src/Infrastructure/Baya.Infrastructure.Persistence --startup-project src/API/Baya.Web.Api |
| Update DB | dotnet ef database update --project src/Infrastructure/Baya.Infrastructure.Persistence --startup-project src/API/Baya.Web.Api |
Default URL: http://localhost:5002 (per launchSettings.json), Swagger at /swagger. A reachable SQL
Server is required to start.
Quality gates
dotnet build Baya.sln— zero new warnings. Unused usings, locals, parameters, private fields or members count as failures. Delete them; don't suppress them.dotnet test Baya.sln— all tests pass, including the ones your change adds.- Read your own diff as if reviewing a PR: would a senior engineer approve it without comment?
- If the change alters the architecture, update the Project map below in the same change.
Two pre-existing warnings are expected and must not be "fixed" unless a task says so: NU1510 on
Microsoft.Extensions.Logging.Debug (Baya.Web.Api), and NETSDK1057 (the .NET 10 SDK is preview here).
Hard rules
- Dependencies point inward. Domain references nothing; Application references only Domain. Never reference Infrastructure or the API from Domain or Application.
- Never throw for an expected failure. Return
OperationResult.SuccessResult/FailureResult/NotFoundResult/ConflictResult. Let genuinely unexpected exceptions reach the globalExceptionHandler; never swallow one. - Controllers are
sealed, inheritBaseController, injectISender, and returnbase.OperationResult(result). Never callOk()/BadRequest()/NotFound()directly. OneSend, one result, no business logic. - Route segments come from
[controller]/[action]tokens (the snake_case transformer). Never hardcode a route string — it also breaks the dynamic-permission key. If a method name doesn't read as a URL, rename it. - Handlers are
internal sealed; requests arerecords; one handler per request. Entities areclasswith no public setters. - Reads use
AsNoTracking()and project with.Select()to a DTO. Never hydrate entities to map them, never return an entity from a handler. Every unbounded list is paginated. - Access the DB through
IUnitOfWork; commit once per command.ApplicationDbContextis referenced directly only inside Infrastructure. - Every soft-deletable entity declares a global query filter in its
IEntityTypeConfiguration<T>. A missing filter is a silent data leak. NeverWhere(x => !x.IsDeleted)per query. - Money is IRR
BIGINT, integer-only — no float path anywhere.gross = commission + payoutalways. Toman converts only inside a provider adapter at its boundary.ledger_entriesis append-only and every posting group balances. - Config is rows, read at compute time via
IPlatformConfig— never hardcoded. And a rate change is never retroactive: snapshot the rate onto the row at compute time. - Money-path writes are idempotent: upsert the webhook event first and no-op on a duplicate, claim before
executing, and treat a unique-violation on confirm as an idempotent success. The DB constraint is the
authoritative backstop, not the handler's
if. - Money movement stays human-approved. A scheduled job may generate a draft payout batch; the
irreversible
processstep is always an explicit admin action. - Route every status write through the forward-only transition table.
statushas a private setter and only cohesive domain methods mutate it; the handler pre-checks and returns a clean 409. - A guarded cross-aggregate flip is one transaction: load both tracked, mutate through one pure domain
helper,
CommitAsynconce. Never flip a derived flag from a controller or out of band. - Self-committing facades run after
CommitAsync()—RaiseAsync,DispatchAsync,WriteAsyncandSetConfigeach callSaveChangeson the shared scoped context and will flush your partial changes. Seams:FieldEncryption:Keyand:HashKeyare load-bearing — never change them. They decrypt all existing PII and derive the phone-lookup hash.- PII goes through
IFieldEncryptor; equality lookups go through the deterministic hash column. Never queryPhoneNumber == x. The encryptor must stay a process-wide singleton. - Two-stage clinical disclosure. A booking request exposes only limited unencrypted
customer_notesand masks the address to a coarse city/district; encrypted care instructions are readable only post-confirmation, only by the assigned nurse and admin, and never projected into a list or logged. is_internalis a hard visibility boundary enforced at the QUERY layer, never in the UI. A non-staff caller can never set or read one.- Tenancy is resolved from
ICurrentUser, never from the request body, and a mismatch is a clean 404 — never a 403, which confirms the row exists. - Auth, OTP and money endpoints are rate-limited (
otp/auth/sensitive/webhookpolicies). - Never hardcode a secret in C#, and never put a real value in the base
appsettings.json— it stays at itsStartupSecretsGuard-rejected placeholder.dotnet user-secretsis not used and is not read (the<UserSecretsId>was removed), so any instruction to use it is stale. - Never concatenate raw SQL. EF parameterizes; if you must,
FromSqlInterpolated, neverFromSqlRawwith user data. async/awaitall the way,CancellationTokenthreaded through every call. Never.Result,.Wait(), orasync void. Don't add.ConfigureAwait(false)in this app.- Never log PII or secrets. Structured templates only; use
userId, not an email. - Package versions live only in
Directory.Packages.props— neverVersion=in a.csproj. - Register infrastructure through a
ServiceConfiguration/extension method called fromProgram.cs. No inline registration;Program.csstays an orchestrator. - A mock lives behind a DI-registered seam, selected by config, defaulting to the mock. Never an
if (mock)in a handler. Record every mock inmvp/blockers.md. - No dead code (the gate is zero new warnings) and comment the why, never the what.
- When you change the architecture, update the Project map below in the same change.
Project map
The canonical list of projects, layers, and cross-layer dependencies — 14 .csproj projects, 55 V1
controllers. Expanded, with the seam catalogue and startup wiring, in
docs/rules/server/structure.md.
src/
├── Core/
│ ├── Baya.Domain Entities per area (User, Identity, Geography, Catalog, Verification,
│ │ Search, Booking, Payments, Refunds, Invoices, Bnpl, Payouts, Reviews,
│ │ Messaging, PartnerCenters, + Configuration/Audit/Analytics/Holidays/
│ │ Notifications/SupportAlerts) · Common/ (BaseEntity, IAuditable,
│ │ [AuditRedacted]) · status-code sets + transition tables
│ └── Baya.Application Features/<Area>/{Commands|Queries}/ · Contracts/ (the seams:
│ Common, Payments, Search, Reviews, Persistence) · Models/ ·
│ pipeline behaviors (Logging → Metrics → Validate)
├── Infrastructure/
│ ├── Baya.Infrastructure.Persistence ApplicationDbContext (+ encrypted-PII converters & phone-hash sync) ·
│ │ ValueConversion/ · Configuration/<Area>Config/ · Repositories/ ·
│ │ Migrations/ · Interceptors/ (AuditFieldInterceptor) ·
│ │ Services/ (DB-backed platform facades, Scheduling/, Search/, Seeding/)
│ ├── Baya.Infrastructure.Identity Jwt/ · Identity/ (Managers, Stores, PermissionManager, Seed, CurrentUser/)
│ ├── Baya.Infrastructure.CrossCutting Serilog wiring · Seams/ (mocks) · Seams/Real/ (vendor adapters) ·
│ │ AddCrossCuttingSeams
│ └── Baya.Infrastructure.Monitoring HealthChecks (live/ready split) · OpenTelemetry
├── API/
│ ├── Baya.Web.Api Program.cs · Controllers/V1/ (55) · appsettings*.json
│ ├── Baya.WebFramework BaseController · Filters/ · Middlewares/ · Swagger/ · Routing/ ·
│ │ ServiceConfiguration/ (rate limiting)
│ └── Plugins/Baya.Web.Plugins.Grpc gRPC services + .proto (User only)
├── Shared/Baya.SharedKernel Extensions + validation base
└── Tests/
├── Baya.Tests.Setup Shared test infrastructure (SQLite, NSubstitute, TestFieldEncryptor)
├── Baya.Test.Infrastructure.Identity xUnit identity tests
├── Baya.Test.Foundation Cross-cutting plumbing + identity handler unit tests
└── Baya.Test.Api WebApplicationFactory integration tests (in-memory SQLite, env "Testing")
DB schemas, one per area: usr, ops, geo, catalog, verif, search, booking, payments,
payouts, reviews, messaging, partner.
Features/Booking(singular — the money-free pre-payment request) andFeatures/Bookings(plural — the post-payment engine) are different areas, not a rename. The entity typeBookingis aliased where the namespaces collide.
Where to read more
Open one of these for the area you are touching.
| Working on… | Read |
|---|---|
| Projects, layers, startup wiring, the seam catalogue, observability | docs/rules/server/structure.md |
| Adding a feature — command, query, handler, validator, controller | docs/rules/server/cqrs.md |
| EF Core, audit, state machines, uniqueness, snapshots, search, jobs, seeders | docs/rules/server/persistence.md |
| Anything on the money path — ledger, refunds, BNPL, payouts, invoices | docs/rules/server/money.md |
| Auth, JWE, sessions, field encryption, tenancy, disclosure, logging | docs/rules/server/identity.md |
| C# style, naming, async, error handling, tests, DI | docs/rules/server/conventions.md |
| The wire contract — envelope, status codes, enums, pagination | docs/integration/ |
| What is built, what is mocked, what is next | docs/status/ |
| Cross-project rules — naming, gates, code quality, config | docs/rules/shared/ |