12 KiB
Backend Phase 0 — Foundation, cross-cutting seams & starter cleanup
Mission: turn the inherited starter skeleton into a clean Balinyaar foundation. Remove the demo scaffolding, stand up the REST API surface the marketplace needs (the baseline is gRPC-only today), wire the missing cross-cutting plumbing (rate limiting, request logging, current-user + audit-field stamping, PII encryption), and define every mock-able external dependency as a DI seam with a faithful in-memory implementation so later phases just plug real providers in. No domain tables yet — this phase makes the next fifteen phases possible and consistent.
Track: backend · Depends on: nothing (first phase) · Unlocks: every backend phase Before you start, read
../_shared/agent-operating-rules.md. It is not optional.
1. Context — where this sits
This is the very first build phase. The server (server/, .NET 10, Clean Architecture, CQRS via
martinothamar/Mediator — not MediatR) already ships a working spine you must keep and build
on, and some demo scaffolding you must remove.
What already exists (do not rebuild) — confirmed in the codebase:
- ASP.NET Core Identity + JWE/JWT + phone-OTP (passwordless TOTP) auth, the dynamic-permission
RBAC system, the CQRS pipeline (
ValidateCommandBehavior,MetricsBehaviour),OperationResult<T>, Mapster, FluentValidation, Serilog, OpenTelemetry/prometheus, health checks, theBaseController+ the full MVC filter/versioning/Swagger stack, andBaya.Tests.Setup(in-memory SQLite). - The 12-project Clean-Arch solution
Baya.sln. Identity tables map to theusrschema.
What is starter scaffolding you will remove in this phase:
- The
Orderdemo end-to-end:Domain/Entities/Order/Order.cs,Application/Features/Order/**,Contracts/Persistence/IOrderRepository.cs,Persistence/Repositories/OrderRepository.cs,Persistence/Configuration/OrderConfig/, theUser.Ordersnav,IUnitOfWork.OrderRepository,OrderGrpcServices+OrderGrpcServiceModels.protoand its wiring. - The three old migrations + snapshot (
Migrations/2021…,2022…,2023…, namespacePersistence.Migrations) — they predate the marketplace and will be regenerated as the new baseline in backend-phase-1.
Known gaps you will close here: no HTTP controllers exist (REST surface must be created);
LoggingBehavior<,> is defined but never registered; no rate limiting is wired despite
CONVENTIONS.md §11; OTP delivery is stubbed (handled in b2).
2. Required reading (do this first)
../_shared/agent-operating-rules.mdand../_shared/backend-conventions-checklist.md.server/CLAUDE.md— Startup wiring, Project map, Identity & auth, Persistence; andserver/CONVENTIONS.md— §1 routing, §4 controllers, §6 persistence (audit fields, soft delete), §9 logging, §11 security (rate limiting), §12 service registration.product/overview/platform-summary.md— the four ground truths (no cash custody, 10% VAT, full-upfront BNPL, weekly payout) and the IRR-Rials-always rule that shapes the money types you scaffold here.../../contracts/conventions/api-conventions.mdandmoney-and-types.md— the envelope/format your REST surface must honour.- Read the actual code you'll touch:
Baya.Web.Api/Program.cs,Baya.Application/Common/*Behavior*.cs,Baya.WebFramework/BaseController/*,Baya.Infrastructure.Persistence/ApplicationDbContext.cs, and each project'sServiceConfiguration/extension.
3. Scope — build this
3.1 Remove the starter scaffolding
Delete the Order feature/entity/repository/config/gRPC/proto and the three old migrations listed in §1.
Remove every reference (nav property, IUnitOfWork member, DI wiring, proto compile). The solution must
build clean afterwards. Do not remove Identity/auth/observability — those stay.
3.2 Stand up the REST surface
The marketplace is REST/JSON. Create the first versioned controller(s) under Baya.Web.Api/Controllers/V1/
following CONVENTIONS.md §4 exactly (sealed, BaseController, inject ISender, [controller]/[action]
tokens, base.OperationResult(...), [Display(Description=...)]). A minimal health/ping or
reference controller is enough to prove the pipeline end-to-end through Swagger; later phases add the
real controllers. Confirm MapControllers() now serves real routes and Swagger renders them.
3.3 Close the wiring gaps
- Register
LoggingBehavior<,>in the Application pipeline (alongside the existing behaviors), in the correct order, so every request is structurally logged (no PII —CONVENTIONS.md§9). - Rate limiting (
CONVENTIONS.md§11): addAddRateLimitervia aServiceConfiguration/extension andUseRateLimiter()beforeUseAuthentication()inProgram.cs. Define named policies (a per-IP fixed-window/token-bucket baseline) ready for auth/OTP/refund/payout endpoints to apply later.
3.4 Current-user + audit-field stamping
- Add
ICurrentUser(Application contract) wrapping the HTTP context (user id, roles), registered Scoped, with a null-object for non-HTTP contexts (jobs/tests). - Add a SaveChanges interceptor (or extend the existing
SavingChangeshook) that stampsCreatedAt/ModifiedAtandCreatedById/ModifiedByIdfromICurrentUseron a shared base/interface, perCONVENTIONS.md§6. Define the audit-capable base type (extend the existingBaseEntity/ITimeModificationrather than inventing a parallel one). Theaudit_logstable and the append-only change log come in b1 — here you build the field-stamping plumbing the interceptor needs and leave a clean extension point for b1 to add log-row writing.
3.5 Define the cross-cutting seams (interfaces + mocks + DI)
Create these Application-layer interfaces with real-shaped signatures, an Infrastructure mock
implementation each, and DI registration via a ServiceConfiguration/ extension (selected by
config so a real impl swaps in later). Keep amounts as IRR long. This phase introduces these seams;
later phases reuse them.
IDateTimeProvider—DateTimeOffset UtcNow(so time is testable; noDateTime.Nowin handlers).IFieldEncryptor—string Encrypt(string)/string Decrypt(string)(+ a deterministicHash(string)for lookups likeiban_hash). Mock = local symmetric key from config; never log plaintext PII. This is what every encrypted PII column will use.ICacheService— typed get/set/remove with TTL and aGetOrCreateAsync. Mock = in-memory (IMemoryCache-backed). The config accessor (b1) and read-heavy queries cache through this; Redis later.IObjectStorage— presigned/streamed put/get/delete keyed by an opaque storage key, returning a retrievable URL. Mock = local-disk/in-memory store under a scratch path. MinIO/S3 later.INotificationDispatcher—DispatchAsync(notification)with a channel concept (in-app now; SMS/push later). Mock = no-op/log; the real in-appnotificationswrite lands in b15. Define the seam now so emitting domains (booking, payments) can depend on it.
The OTP/SMS seam (
ISmsSender) is introduced in b2 (with the auth REST surface), and the search/payment/bnpl/bank/vendor seams in their phases — do not pre-build those here; just leave the registry rows. (Listed in mocks-registry.md.)
3.6 Money & convention guardrails
Add (or confirm) the small shared helpers the money path will rely on: IRR is long/BIGINT
everywhere; if you add a money value object keep it integer-only with no float path. Document the rule in
server/CONVENTIONS.md if not already explicit.
4. Mocks & seams in this phase
| Seam | Mock behaviour | Registry |
|---|---|---|
IDateTimeProvider |
returns real UTC now (deterministic override in tests) | n/a (not external) |
IFieldEncryptor |
local symmetric key from config; passthrough-but-reversible; deterministic hash | update row |
ICacheService |
in-memory IMemoryCache |
update row |
IObjectStorage |
local-disk/in-memory blob store | update row |
INotificationDispatcher |
log/no-op (in-app write arrives in b15) | update row |
Record each in mocks-registry.md (seam, file,
what's faked, config keys, how to make real, status 🟡).
5. Critical rules you must not get wrong
- Don't break the working spine. Identity, JWE auth, dynamic permissions, the CQRS behaviors, and observability must still work after cleanup. Run the existing tests.
- Seams live in Application, implementations in Infrastructure. Never reference Infrastructure from
Application/Domain. Register via
ServiceConfiguration/extensions called fromProgram.cs— no inline DI inProgram.cs(CONVENTIONS.md§12). - Mock = real interface, fake body. No
if (mock)branches in handlers; selection is by registration. IFieldEncryptornever leaks plaintext into logs, exceptions, or non-AsNoTrackingquery projections of PII.- Audit-field stamping is infrastructure, not handler code — handlers never set
CreatedByIdetc.
6. Definition of Done
The shared definition-of-done.md, plus:
Order+ the three old migrations are gone;dotnet build Baya.slnis clean (zero new warnings);dotnet test Baya.slnpasses (existing identity tests still green).- At least one real REST controller is reachable through Swagger and returns the
OperationResultenvelope. LoggingBehaviorregistered; rate limiter wired (policies defined,UseRateLimiterplaced before auth).ICurrentUser+ audit-field interceptor in place; the five seams (§3.5) registered with mocks.- The Project map in
server/CLAUDE.mdis updated (Order removed; seams/cross-cutting noted);CONVENTIONS.mdnotes the IRR-BIGINTmoney rule if it wasn't explicit.
7. How to test (what a human can verify after this phase)
dotnet build Baya.slnanddotnet test Baya.sln— both succeed.dotnet runthe API → open/swagger→ the new REST controller appears and its endpoint returns a 200 in the standard envelope; the Order endpoints are gone.- Hit the rate-limited test endpoint past its limit →
429. - A unit test proves
IFieldEncryptor.Decrypt(Encrypt(x)) == xand the audit interceptor stampsCreatedAt/CreatedByIdon a save (use the SQLite test context).
8. Hand off & document (close the phase)
- Docs: update
server/CLAUDE.mdProject map (+ a one-line note on the new seams and where they're registered). UpdateCONVENTIONS.mdonly if you established a new rule. - Contracts: no domain API yet — but publish the first
swagger.jsonsnapshot (openapi/README.md) so the frontend's f0 can wire its type pipeline against the envelope shape. - Handoff & report: write
shared-working-context/backend/handoff/after-backend-phase-0.md(the spine is clean, REST works, seams exist, what f0/b1 can rely on), append tobackend/STATUS.md, writereports/backend-phase-0-report.md, and updatereports/mocks-registry.md(the five rows → 🟡). - Memory: save a
projectmemory note for any non-obvious decision (e.g. how the seams are selected by config, the audit interceptor design) with aMEMORY.mdpointer.