WorkThe .NET year
SkillLedger
Let professionals trade work for work instead of money, settling in an internal credit that was encrypted, signed on every transaction, and held in escrow until a milestone cleared.
- Ran on
- Railway (API), Cloudflare Workers (web, via OpenNext)
- Ran
- August 2025 to July 2026
- Domain
- Professional skill-trading marketplace
That platform choice is part of a longer migration: Azure, then Railway, then Cloudflare, across the whole estate. Read how and why.
Retired
Built with
- .NET 9
- EF Core
- ASP.NET Core Identity
- PostgreSQL
- SignalR
- Next.js
- Stripe
- xUnit
Hosted services
- Neon (PostgreSQL)
- Redis
- Stripe
The decision
SkillLedger encrypted its wallet balances at the column level (AES-256-GCM ciphertext, not a
plaintext integer), so the database itself could never read a balance. That choice moved overdraft
protection out of SQL and into the application: balances could not be summed, indexed or constrained
at the database layer, so every balance check ran as C# logic inside a Serializable transaction
instead.
In exchange, a leaked database export would have exposed no balances, and every transaction row
carried an HMAC-SHA256 signature, with a KeyIdentifier column reserved for key rotation.
A self-commissioned audit of that wiring later found gaps between the design and the running code:
Program.cs never bound the key-vault configuration, so the flag gating a real Azure key stayed
false in every environment and both signing keys resolved to hashes of hardcoded string
constants; the branch that does reach Key Vault fetches a real key object and then discards it,
deriving the AES key as a hash of the key’s name and the current calendar month instead, so wiring
the flag correctly would not have fixed it; and the key-rotation method behind the KeyIdentifier
column logged “not yet implemented” and returned true, reporting success on a rotation that never
ran. None of it touched the column-level encryption itself: the database still never held a
plaintext balance.
Architecture
Three .NET projects (Api, Core, and Infrastructure) sat in front of a Next.js frontend, over
PostgreSQL. Program.cs was the composition root, and every one of its 93 service registrations was
inline.
The project moved from SQL Server to Postgres in February 2026 and collapsed everything before
that into one InitialPostgresCreate, so three EF Core migrations covered the 70-table schema.
Escrow held credit between agreement and delivery, and its six-state lifecycle was modelled as
methods on the entity itself rather than scattered conditionals. Active and PartiallyReleased
are the hub of the machine, and the repository draws it as two graphs rather than one, because all
seven of their outbound transitions on a single graph put the labels close enough to merge:
money moving forward
(start) -> Active client funds the project
Active -> PartiallyReleased a milestone is released
PartiallyReleased -> PartiallyReleased the next milestone
Active -> Completed one release covers the total
PartiallyReleased -> Completed releases reach the total
Active, PartiallyReleased -> Cancelled
the account paused or contested
Active, PartiallyReleased -> Disputed
Active, PartiallyReleased -> Frozen
Frozen -> Disputed
Disputed, Frozen -> Active nothing released yet
Disputed, Frozen -> PartiallyReleased something already released
Every arrow is a real call site rather than an inferred state. The four release edges are gated on
CanBeReleased, and the two resume edges carry a guard on the amount already released, which is why
Frozen and Disputed return to PartiallyReleased instead of Active once anything has gone
out. The four transitions that move credit (funding, milestone release, full release and cancel) are
each written from inside a Serializable transaction, and each release or refund writes a row
through the same signed transaction log: the state machine and the ledger are two views of one
write. The four that only change state (dispute, resolve, freeze, unfreeze) save without one.
By the numbers
Each figure carries where it came from and when it was measured. Open one to see the command behind it, where the repository recorded a command.
- Application source
- 115,872 lines across 380 C# files
Provenance
Source portfolio/METRICS.md § Source size
git ls-files 'src/*.cs' | wc -l ; lines 'src/*.cs'Measured
- Tests
- 122,933 lines across 217 C# files
Provenance
Source portfolio/METRICS.md § Source size
git ls-files 'tests/*.cs' | wc -l ; lines 'tests/*.cs'Measured
- Declared backend test cases
- at least 4,136 declared [Fact] test cases, plus 88 [Theory] and 358 [InlineData] cases
Provenance
Source portfolio/METRICS.md § Tests
Measured
- Data model
- 70 tables, 64 DbSet<>, 62 entity files, 3 EF Core migrations
Provenance
Source portfolio/METRICS.md § Data model
git grep -c 'DbSet<' -- src/SkillLedger.Infrastructure/Data/SkillLedgerDbContext.csMeasured
- API surface
- 36 controllers, 70 Infrastructure services, 52 Core interfaces, 93 DI registrations in one 1,037-line Program.cs
Provenance
Source portfolio/METRICS.md § Components
Measured
- Commit history
- 944 commits, 2025-08-29 to 2026-07-08
Provenance
Source portfolio/METRICS.md § History, exported from the private source repository; this snapshot is one commit
cat docs/source-history.jsonMeasured
- Self-commissioned code audit
- 78 defects logged across severity tiers from a full-codebase security and bug audit he ran against his own code
Provenance
Source docs/BUG_REPORT.md § Full codebase security and bug audit
Measured
- Live-URL Playwright sweep
- A Playwright sweep he built and ran against the deployed site found 13 bugs, including a 404 on every dynamic route
Provenance
Source web/FOUND_BUGS.md
Measured
Its portfolio/ directory holds the architecture, metrics and testing documents these figures were taken from.