WorkThe .NET year
GeoLeap
Told you which countries and streaming services carried a show or movie, and suggested a VPN destination when it wasn't available where you were.
- Ran on
- Railway (API), Cloudflare Workers (web, via OpenNext)
- Ran
- August 2025 to July 2026
- Domain
- Streaming availability and VPN discovery
That platform choice is part of a longer migration: Azure, then Railway, then Cloudflare, across the whole estate. Read how and why.
Built with
- .NET 9
- PostgreSQL
- Redis
- Hangfire
- SignalR
- Next.js 16
- React 19
- React Native / Expo
- Cloudflare Workers
- Stripe
Hosted services
- Neon (PostgreSQL)
- Redis
- Stripe
- Resend
- Sentry
- MaxMind GeoIP2
- RapidAPI (streaming-availability)

The decision
Every availability answer GeoLeap gave cost money: the data came from one commercial API billed per
call. ApiCostManager sat on the request path and stopped a call before it went out once spend hit
a ceiling of $200 a month and $10 a day: real budget enforcement on a dependency billed by the
call.
A second implementation, BudgetManager, checked a per-provider limit alongside the daily and
monthly totals, and served an admin reporting endpoint. Both checks failed open on a database or
cache error, returning true from a catch block under a comment that recorded the choice.
Architecture
Three clients (a Next.js 16 web app, a React Native mobile app, and a Playwright end-to-end suite) sat in front of one .NET 9 API of 68 controllers and 905 HTTP endpoints, backed by PostgreSQL, Redis, Stripe, and a single paid streaming-availability API.
Like GatherGrove, the schema itself changed mid-project: 202 entities were squashed into a single PostgreSQL baseline migration when the project cut over from SQL Server in February 2026.
The API’s types were declared twice, once under frontend/src/types/ and once under
mobile/src/types/, with no shared package between the two clients to hold them in step. The web app also carried 19 route handlers of its own instead of
forwarding straight through to the API, because a straight pass-through does not forward
Set-Cookie back to the browser and the session cookie was HTTP-only.
Two places refused to trust their own configuration. Stripe verifies a webhook signature against the
raw, unmodified bytes, so EnableBuffering() runs on /api/webhooks ahead of the correlation-id
middleware that would otherwise read the body first. And the API would not start in Production
without three unique indexes on MobileSubscriptions: AssertRequiredProductionIndexesAsync looks
for them and stops the process, because without them nothing errors and purchases silently
double-count.
Eleven background services ran in process, gated on ENABLE_BACKGROUND_SERVICES, alongside Hangfire
on a switch of its own.
SensitiveDataFilter, a redaction utility carrying 54 sensitive field names and six regex patterns,
already sat in the codebase when a December 2025 audit found two places that logged raw bodies ahead
of it: the request and response logging middleware, and the Stripe webhook controller, which was
writing the webhook secret itself in plaintext. Routing both through the filter, shipped
2025-12-17, closed it, backed by 75 new sanitization test cases. One
limit is written down rather than left quiet: the card regex expects 16 digits and does not match a
15-digit Amex number, so those redact by field name only.
Closed the same day: a double click, a network retry or a second browser tab could each have produced a duplicate charge, so the fix added an idempotency key checked in the application and backed by a unique index in the database. A test fires 10 concurrent requests under one key and asserts that at most one transaction row exists afterward.
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.
- Total source
- 1,063,755 lines across 2,675 files (C#, TSX, TS, JS, SQL)
Provenance
Source portfolio/METRICS.md § Lines and files, counted with wc -l
find . -type f -name "$1" -not -path "*/node_modules/*" -not -path "*/bin/*" -not -path "*/obj/*" -not -path "*/.next/*" -not -path "*/coverage/*" -print0 | xargs -0 cat | wc -lMeasured
- API surface
- 68 controllers, 905 HTTP endpoints
Provenance
Source portfolio/METRICS.md § API surface
grep -rho '\[Http\(Get\|Post\|Put\|Delete\|Patch\)' backend/GeoLeap.Api --include='*.cs' | wc -lMeasured
- Database
- 202 DbSet<> entities, 215 tables
Provenance
Source portfolio/METRICS.md § API surface (215 confirmed by migrating an empty PostgreSQL 16 instance)
Measured
- Backend tests
- 6,527 tests passing
Provenance
Source portfolio/METRICS.md § Test results: Backend
cd backend && ENABLE_COVERAGE=true dotnet test --settings coverlet.runsettings --collect:"XPlat Code Coverage"Measured
- Frontend tests
- 7,786 tests passing
Provenance
Source portfolio/METRICS.md § Test results: Frontend
cd frontend && npm run test:coverageMeasured
- Commits
- 2,322 across 140 active days
Provenance
Source portfolio/METRICS.md § Commit history
git rev-list --count HEADMeasured
- Pre-launch audit sweep (20 working days, two-thirds through the build)
- 174 bugs found, 108 closed within the sweep
Provenance
Source portfolio/DEVELOPMENT-HISTORY.md § A 20-day audit, mid-build
Measured
Screens




Its portfolio/ directory holds the architecture, metrics and testing documents these figures were taken from.