A layered, JWT-authenticated retail banking core with a Blazor Server operator dashboard,
a .NET MAUI consumer app, and a Postgres-backed ledger enforcing seven invariants at every write.
📄 One-page technical brief · 🎬 Demo storyboard · 📚 Full docs
TransBank is a fictional bank built to prove real engineering discipline. A Postgres ledger under three .NET clients — a Blazor Server ops dashboard, a MAUI consumer app, a MAUI mobile staff app — all speaking to a single ASP.NET Api. Every request is cryptographically attributable through Duende IdentityServer + JWT Bearer. Every write path is guarded by property tests against seven ledger invariants that would break real money if violated.
It's not a scaffold. Eight coherent feature commits ship: full card management with fraud detection, monthly PDF statements, a signature splash + brand identity across every surface, real login flows for both customer and operator with role-based endpoint gating. 121 property tests run against a real Postgres testcontainer — including HTTP-level integration tests that exercise the whole OIDC handshake. Zero binary noise in git; the eight commits tell the entire product story.
It's a demo you can hand to anyone. The technical brief is a one-page PDF rendered by TransBank's own QuestPDF pipeline — the same code path that ships customer statements. The storyboard walks through a 10-minute recording that would land on a portfolio page. The architecture doc shows the layered project graph. The authentication doc walks every landmine we hit in the OIDC + Blazor Server + MAUI dance so the next person doesn't have to.
# 1. Postgres
docker compose up -d
# 2. Api (Duende IdentityServer + JWT Bearer + ledger)
dotnet run --project src/TransBank.Api
# 3. Ops dashboard (Blazor Server + OIDC)
dotnet run --project src/TransBank.Ops
# → http://localhost:5269 — sign in as ops-admin / admin-dev
# 4. Consumer app (MAUI Windows preview)
dotnet run --project src/TransBank.Consumer -f net10.0-windows10.0.19041.0
# → boots into LoginPage → tap "Sign in with TransBank" → system browser
# → sign in as alice / alice-devFull test sweep:
dotnet test tests/TransBank.PropertyTests
# 121 tests — real Postgres via Testcontainers + WebApplicationFactoryThe whole product journey lives in git log --oneline. Each commit is self-contained, each proves something new about the system:
| Commit | What it shipped |
|---|---|
cards §1 |
Wallet — issue, freeze, unfreeze. XAML card art with FROZEN overlay |
cards §2 |
Card charging + per-card activity feed. New Merchant + CardId on Transaction |
cards §3 |
Ops surface for card management + fraud rules (card testing + frozen retries) |
statements |
Monthly PDF export from Consumer Profile via QuestPDF |
duende §1 |
Consumer switches from mock X-Customer-Id header to real JWT via ROPC |
duende §2 |
OIDC + PKCE via system browser. Branded login page. SecureStorage session |
brand |
TransBank logo + 7-step splash + hover pulse across every surface |
duende §3 |
Ops onto OIDC + role claims. Legacy X-Customer-Id header retired everywhere |
Real banking systems break in interesting ways when they violate these — TransBank tests each one after every mutation path.
| # | Invariant | Where it's enforced |
|---|---|---|
| INV-1 | Every transaction sums to zero across its ledger entries | TransferService, CorrectionService, CardChargeService — property tests |
| INV-2 | Ledger entry currency equals the account's currency | TransferService request validation + property test |
| INV-3 | The application role cannot UPDATE or DELETE ledger_entries |
Postgres migration granting restricted permissions to transbank_app |
| INV-4 | Materialized balance equals SUM(ledger entries) per account | Reconciliation dashboard + property test |
| INV-5 | Customer accounts cannot go negative | TransferService balance guard + FOR UPDATE lock |
| INV-6 | Amounts are positive integers in minor units — no floats | bigint amount_minor + DB CHECK constraint |
| INV-7 | One idempotency key maps to at most one transaction | UNIQUE index on idempotency_key |
┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐
│ TransBank.Ops │ │ TransBank.Consumer │ │ TransBank.Mobile │
│ Blazor Server │ │ MAUI │ │ MAUI │
│ Cookie + OIDC │ │ OIDC + PKCE │ │ (mock identity) │
└────────┬───────────┘ └────────┬───────────┘ └─────────┬──────────┘
│ │ │
│ JWT Bearer (ops) │ JWT Bearer (customer) │
└───────────────┬────────┴─────────────────────────┘
▼
┌───────────────────┐
│ TransBank.Api │
│ ASP.NET + Duende │
└────────┬──────────┘
│
┌────────▼──────────┐
│TransBank.Application ← TransferService, CardService, ...
└────────┬──────────┘
│
┌────────▼──────────┐
│TransBank.Infrastructure ← EF Core + Npgsql + migrations
└────────┬──────────┘
│
┌────────▼──────────┐
│ Postgres │
└───────────────────┘
Layered so strictly that TransBank.Domain doesn't reference EF Core, Contracts doesn't reference Domain, and every client speaks to the Api through the same DTOs. Full breakdown in docs/developer-guide/architecture.md.
Two schemes, three flows, one identity server.
- Consumer — OIDC + PKCE via the system browser. RFC 8252 loopback listener on port 52000. Tokens stashed in
SecureStorage. Boot-time ping catches stale signing keys before the app draws the home screen with a dead token. - Ops — Blazor Server with OpenID Connect + Cookie session. Role claims (
ops.viewer,ops.admin) gate every endpoint. Ops → Api requests carry the operator's Bearer token via aDelegatingHandlerthat pulls fromHttpContext.GetTokenAsync. - Api — JWT Bearer validates tokens for both audiences (
transbank.api+transbank.ops) viaValidAudiencesplural.RequireCustomerAttributereads thecustomer_idclaim; the legacy header path is gone.
Full walkthrough with all the landmines we hit: docs/developer-guide/authentication.md.
- .NET 10 SDK
- Docker (for the Postgres testcontainer +
docker compose up -d) - Windows for MAUI Consumer + Mobile preview (Windows dev preview TFM)
Fictional bank for learning. Not for real money movement.
- Duende IdentityServer — Community Edition (free for personal / non-commercial / small commercial use under $1M revenue)
- QuestPDF — Community license (same rationale)
|
Building .NET systems that prove themselves. Founder @ LogicDynamics TransBank is one of five featured applications at logicdynamics.net — every commit in this repo tells a self-contained story about how a real banking core defends its own invariants under real load. If any of it resonates, let's talk. 🌐 logicdynamics.net · 💼 LinkedIn · 📄 TransBank case study |
