Hexis · Sports nutrition
From several minutes to under 700ms
Hexis plans what athletes eat around their training, and it's used by 40% of the Tour de France peloton and 50% of the Premier League. When I joined, loading a few days of one athlete's data could take minutes. I rebuilt the data model and the backend on top of it, and those queries now return in under 700ms. That turnaround was a key factor in closing the company's $2.1M seed round.
- multi-day queries, down from several minutes
- <700ms
- seed round closed, with the speed-up a key factor
- $2.1M
- regions taking writes: London, Iowa, Sydney
- 3
- Role
- Senior engineer, then CTO
- Team
- 5 engineers, me included
- When
- 2024 to present
The problem
Every athlete on Hexis has years of history: plans, logged meals and training data synced from wearables. That's millions of rows per table. The app reads several days at a time, and those multi-day queries took several minutes.
Then Hexis grew outside Europe. The database lived in London, and a single query's round trip took about 80ms from New York and 280ms from Sydney. A page that runs 5 to 10 queries paid that round trip on every one.
My role
I joined Hexis in June 2024 as a senior full stack engineer and became CTO. I grew engineering to five people, me included, and I still write production code most days. I rebuilt the data model and the backend myself, and later designed and built the multi-region setup.
Constraints
- Years of history per athlete, with millions of rows per table.
- Athletes write all day. They log meals, and training data syncs in from wearables.
- A five-person engineering team that also ships to production every week.
Decisions
Rebuild the schema around how the app reads
- Context
- Building one day for one athlete meant pulling from about ten related tables, and each day's nutrition sat in separate rows for protein, carbs, fat and energy. A multi-day view repeated all of that one day at a time.
- Decision
- I moved each athlete's day into a single row, keyed by athlete and date, with that day's meals, workouts and notes stored next to each other as JSON. Then I rewrote the range queries to fetch those rows by key, in parallel.
- Consequence
- Multi-day queries went from several minutes to under 700ms.
Give every region its own writable database
- Context
- The first plan was read replicas in the US and Australia. But replicas only help reads, and with them every meal logged in Sydney would still travel to London and back. They also bring read-after-write bugs, where a meal someone just logged is missing on the next screen.
- Decision
- London runs on Cloud SQL, and Iowa and Sydney run PostgreSQL on Compute Engine that we manage ourselves. All three take reads and writes, and pglogical copies each node's row changes to the other two. The app runs in all three regions behind one global load balancer, and HAProxy points each region at its local database first and fails over to the others.
- Consequence
- Athletes in Europe, the US and Australia read and write to a database in their own region. Running the US and Australia nodes ourselves costs about $120 to $140 a month each, against the $400 to $500 a month we'd estimated for each managed replica. The catch is that we own their upgrades and backups.
Send every schema change through one script
- Context
- pglogical copies rows, not schema changes. A migration has to land on all three nodes, and while their schemas differ, replicated rows can fail to apply.
- Decision
- Every Prisma migration goes through one script, which GitHub Actions runs whenever the schema changes. It stops if the three nodes would get different changes or if replication is already broken. Then it freezes writes, waits for queued changes to drain, applies the change on every node, and lets the app write again only once all six subscriptions are replicating.
- Consequence
- If a step fails halfway, writes stay frozen. Pausing writes is better than three databases quietly drifting apart.
The result
Multi-day queries return in under 700ms instead of several minutes, and the speed-up was a key factor in closing the $2.1M seed round. Athletes in Europe, the US and Australia now get an API and a database in their own region.
What I'd tell anyone going multi-region
- Decide how you'll handle writes first. Replicas only help reads.
- Build the schema-change process before you go live. It's most of the work.
Stack
- PostgreSQL
- Prisma
- TypeScript
- pglogical
- HAProxy
- Google Cloud
- Pulumi
- GitHub Actions