TrophyScan
A 3D antler scoring app with a social feed, competitions and subscriptions. My contribution ran across five surfaces — iOS, Flutter, the Node backend, the React admin, and a Python/FastAPI platform service.
Over a year here I worked on iOS, the Node backend, the React admin dashboard, the Flutter rewrite, and lately a Python/FastAPI platform service — which changes what you can see. Instrumentation, API shape and client behaviour stop being three teams’ problems and start being one.
Employment work at Scopic Software. TrophyScan is the client’s product, built by a ten-person team — everything described below is my own contribution within it, and the interface shown is an illustration of the concepts, not a screenshot.
Five surfaces, one product
TrophyScan scores whitetail and mule deer antlers from a 3D scan, then wraps that in the things that make people come back — a social feed, certification, competitions with leaderboards and a subscription.
I joined on mobile and did not stay there.
02 — Surfaces
Five surfaces, four languages.
Instrumentation, API shape and client behaviour stop being three teams’ problems when one person has written all three. The backend rule I kept to is the clearest example: if a row must not be visible, it should not come out of the database — filtering it out afterwards means it has already crossed a boundary, and every new caller has to remember the same rule.
- Exclusion enforced in the query, so no caller can forget it
- One schema for an event, whichever of three codebases fires it
- The same product shipped on two mobile toolchains
- Swift 6 strict-concurrency migration
one definition · fired from Swift, TypeScript and Node One schema
three spellings of the same action, and a funnel nobody trusts Drift
- Concurrency
- Swift 6 strict
- Subscription
- Pro — StoreKit 2
- Deep links
- Branch.io
03 — Mobile
Shipped it in Swift. Then shipped it again.
I was a key contributor to the first public release on iOS — the social layer (publish, like, share), trophy certification, competitions with leaderboards, ownership transfer over Branch.io deep links, and the StoreKit 2 subscription paywall with its tiers, trials and receipt validation.
Later the team moved the app to Flutter and I moved with it, so I have built the same product twice on two toolchains — which is the fastest way to learn which of your decisions were really about the platform and which were about the product.
- First public release — social, certification, competitions
- Ownership transfer via Branch.io deep links, survives install
- StoreKit 2 paywall: tiers, trials, receipt validation
- Contributed to the Swift 6 strict-concurrency migration
- Then rebuilt on Flutter with the team
Swift · via the tracking wrapper Same schema
TypeScript · via the tracking wrapper Same schema
Node · via the tracking wrapper Same schema
three possible origins, one well-formed event ~55 events total
- Tracked events
- ~55
- Surfaces
- iOS · Web · API
- Naming
- wrapper-enforced
04 — Instrumentation
The SDK was easy. Agreement was not.
Around 55 tracked events across iOS, web and the backend. The difficulty was never the Amplitude SDK — it was making the same user action produce one well-formed event regardless of which surface it originated on.
So the fix was a tracking wrapper that standardises names and properties on every platform. Without it you get three spellings of the same event and a funnel nobody trusts, which is worse than no analytics at all because people act on it.
- ~55 events instrumented across three codebases
- One wrapper per platform, enforcing names and properties
- Same action → same event, whichever surface fires it
- Schema agreed before instrumenting, not after
two lock modes on a hot table — read
pg_locks before writing it
Shipped
documented errors checked against raised ones, both directions Guarded
caught a test that asserted the bug was correct behaviour Verified
- Migrations
- lock-aware
- Tests
- real PostgreSQL
- Storage & queue
- S3 · SQS
05 — Platform
Hired for mobile. Shipped into the API.
The last stretch has been backend: a production Python / FastAPI service over PostgreSQL — access-control hardening, schema constraints and migrations, background-job reconciliation, and API contract guarantees.
The one worth telling: a status enum had to be mirrored onto the database as a CHECK constraint, and doing it in one transaction would have held two different lock modes on a hot table at once. I read pg_locks to see which, then split the migration in two. Every fix was then mutation-tested — revert it, and a named test has to fail.
- Migrations written against measured lock behaviour, not assumption
- A bidirectional guard so the API spec and the code cannot drift apart
- Regression tests run against real PostgreSQL, not a mock
- Each branch measured against a pristine base on a fresh database
Stack
Mobile
- Swift · SwiftUI
- Flutter · Dart
- Core Data
- Combine
- StoreKit 2
- Branch.io
Backend
- Python · FastAPI
- Node.js · Express
- Drizzle ORM
- PostgreSQL
- AWS S3 · SQS
- REST APIs
Web
- React
- TypeScript
- Data grids
- Modal systems
Practice
- Amplitude
- Cross-language schema design
- Mutation testing
- Swift 6 concurrency
Outcome
Key contributor to the first public release, then to the layers behind it, then to the rebuild — and most recently into the platform service itself. Working across every surface means the instrumentation, the API shape and the client can be reasoned about together rather than negotiated across teams.
I go where the problem is, not where my title is.