Skip to content

Repository files navigation

TransBank

TransBank

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.

Quickstart

# 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-dev

Full test sweep:

dotnet test tests/TransBank.PropertyTests
# 121 tests — real Postgres via Testcontainers + WebApplicationFactory

The eight-commit story

The 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

The seven invariants

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

Architecture at a glance

┌────────────────────┐   ┌────────────────────┐   ┌────────────────────┐
│  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.

Auth surface

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 a DelegatingHandler that pulls from HttpContext.GetTokenAsync.
  • Api — JWT Bearer validates tokens for both audiences (transbank.api + transbank.ops) via ValidAudiences plural. RequireCustomerAttribute reads the customer_id claim; the legacy header path is gone.

Full walkthrough with all the landmines we hit: docs/developer-guide/authentication.md.

Requirements

  • .NET 10 SDK
  • Docker (for the Postgres testcontainer + docker compose up -d)
  • Windows for MAUI Consumer + Mobile preview (Windows dev preview TFM)

License

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)

About the author

Azim Litanga

Azim Litanga

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

About

A fictional retail bank in .NET 10 to prove real engineering discipline

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages