Files
baya-monorepo/dev/phases/backend/backend-phase-0.md
T
2026-06-28 21:59:59 +03:30

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/Mediatornot 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, the BaseController + the full MVC filter/versioning/Swagger stack, and Baya.Tests.Setup (in-memory SQLite).
  • The 12-project Clean-Arch solution Baya.sln. Identity tables map to the usr schema.

What is starter scaffolding you will remove in this phase:

  • The Order demo end-to-end: Domain/Entities/Order/Order.cs, Application/Features/Order/**, Contracts/Persistence/IOrderRepository.cs, Persistence/Repositories/OrderRepository.cs, Persistence/Configuration/OrderConfig/, the User.Orders nav, IUnitOfWork.OrderRepository, OrderGrpcServices + OrderGrpcServiceModels.proto and its wiring.
  • The three old migrations + snapshot (Migrations/2021…, 2022…, 2023…, namespace Persistence.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)

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): add AddRateLimiter via a ServiceConfiguration/ extension and UseRateLimiter() before UseAuthentication() in Program.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 SavingChanges hook) that stamps CreatedAt/ModifiedAt and CreatedById/ModifiedById from ICurrentUser on a shared base/interface, per CONVENTIONS.md §6. Define the audit-capable base type (extend the existing BaseEntity/ ITimeModification rather than inventing a parallel one). The audit_logs table 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.

  • IDateTimeProviderDateTimeOffset UtcNow (so time is testable; no DateTime.Now in handlers).
  • IFieldEncryptorstring Encrypt(string) / string Decrypt(string) (+ a deterministic Hash(string) for lookups like iban_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 a GetOrCreateAsync. 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.
  • INotificationDispatcherDispatchAsync(notification) with a channel concept (in-app now; SMS/push later). Mock = no-op/log; the real in-app notifications write 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 from Program.cs — no inline DI in Program.cs (CONVENTIONS.md §12).
  • Mock = real interface, fake body. No if (mock) branches in handlers; selection is by registration.
  • IFieldEncryptor never leaks plaintext into logs, exceptions, or non-AsNoTracking query projections of PII.
  • Audit-field stamping is infrastructure, not handler code — handlers never set CreatedById etc.

6. Definition of Done

The shared definition-of-done.md, plus:

  • Order + the three old migrations are gone; dotnet build Baya.sln is clean (zero new warnings); dotnet test Baya.sln passes (existing identity tests still green).
  • At least one real REST controller is reachable through Swagger and returns the OperationResult envelope.
  • LoggingBehavior registered; rate limiter wired (policies defined, UseRateLimiter placed before auth).
  • ICurrentUser + audit-field interceptor in place; the five seams (§3.5) registered with mocks.
  • The Project map in server/CLAUDE.md is updated (Order removed; seams/cross-cutting noted); CONVENTIONS.md notes the IRR-BIGINT money rule if it wasn't explicit.

7. How to test (what a human can verify after this phase)

  • dotnet build Baya.sln and dotnet test Baya.sln — both succeed.
  • dotnet run the 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)) == x and the audit interceptor stamps CreatedAt/CreatedById on a save (use the SQLite test context).

8. Hand off & document (close the phase)

  • Docs: update server/CLAUDE.md Project map (+ a one-line note on the new seams and where they're registered). Update CONVENTIONS.md only if you established a new rule.
  • Contracts: no domain API yet — but publish the first swagger.json snapshot (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 to backend/STATUS.md, write reports/backend-phase-0-report.md, and update reports/mocks-registry.md (the five rows → 🟡).
  • Memory: save a project memory note for any non-obvious decision (e.g. how the seams are selected by config, the audit interceptor design) with a MEMORY.md pointer.