Splits
💡 The Inspiration
In video game speedrunning, a “split” is a timed segment of a run. Speedrunners break their games into segments, time each one, and obsessively compare against their personal bests. The tools they use - LiveSplit, WSplit - show real-time deltas, track records, and visualize where time is gained or lost.
Splits takes that concept and applies it to real life. Any routine you do repeatedly - a morning routine, a cleaning checklist, a practice session, a cooking workflow - can be broken into segments, timed, and tracked. The question isn’t just “how long did it take?” but “how does this run compare to my best?”
⚡ What It Does
Splits is a cross-platform app built with Flutter and Firebase. The core loop mirrors speedrunning tools:
- Create tasks - the individual segments of your routine (like stages in a speedrun)
- Organize them into routines - ordered sequences of tasks you run through
- Start a run and time each split with a precision timer that supports pause/resume
- Save records when you finish, capturing every interval down to the millisecond
- Track your stats - min, average, and max times per task and per routine, across all your runs
Just like a speedrunner studying their splits to find where they lost time on Bowser’s Castle, you can look at your records and see exactly which segment of your morning routine is the bottleneck.



🗄️ The Two-Database Architecture
One of the more interesting technical decisions is the use of two databases for different purposes.
Firestore handles ephemeral, in-progress run data. When you’re mid-run, your active splits and timer intervals live in Firestore. This gives you real-time syncing and low-latency writes - exactly what you want when a timer is ticking.
PostgreSQL (via Firebase DataConnect) stores everything permanent: users, tasks, routines, completed records, logs, and intervals. When you finish a run, a Cloud Function transforms the Firestore data into structured PostgreSQL records and cleans up the ephemeral state.
This separation is pragmatic. Firestore excels at real-time document sync. PostgreSQL excels at relational queries like “what’s my average time on Task X across all runs of Routine Y?” Each database does what it’s good at.
⏱️ Intervals: Not Just a Single Time
Splits doesn’t store a single duration per segment. It tracks intervals - individual start/stop pairs representing each timing session. Pause a split, take a break, resume - each segment is a discrete interval. Total duration is the sum.
This mirrors how speedrun timers work. Real-time (RTA) vs. segmented timing matters. And if you need to adjust a split after the fact, you can edit individual interval timestamps down to the millisecond - like correcting a bad split in LiveSplit.
🔤 Lexicographic Ranking for Segment Ordering
Reordering segments in a list sounds trivial until you need to do it efficiently in a database. If you store order as integers (1, 2, 3…), moving item 3 to position 1 means updating every item below it.
Splits uses lexicographic ranking instead. Each item gets a string rank like "a", "b", "c". To insert between "a" and "b", assign "ab". Only the moved item needs an update - no cascading writes. This is used for both routine task ordering and log ordering within records.
📸 Snapshotting Stats at Record Time
When you finish a run, Splits doesn’t just save your times. It snapshots your current personal best statistics (min/avg/max) at the moment the record is created. This means a record from three months ago reflects what your stats were then, not what they are today.
This is directly inspired by how speedrunners think about progression. Your PB from January means more when you can see what your average was at the time. It turns records into historical markers, not just raw data.
☁️ Cloud Functions as the Trust Boundary
All persistent writes go through Firebase Cloud Functions. The client never writes directly to PostgreSQL. Each function validates input with Zod schemas, verifies ownership, and performs operations atomically.
The createRecord function is the most complex - it reads the active run and all its splits from Firestore, validates every interval for chronological consistency and no overlaps, snapshots current statistics, creates the record with ordered logs and intervals in PostgreSQL, then cleans up Firestore.
📱 Frontend Architecture
The Flutter app uses a feature-based structure with Riverpod for state management. Navigation uses GoRouter with auth guards and a StatefulShellRoute for tab-based navigation between Home (your run history) and Library (your tasks and routines).
The UI is Material Design with a dark theme, constrained to 480px max width. Firebase DataConnect auto-generates both a Dart client SDK and a Node.js admin SDK from the GraphQL schema, keeping types in sync across frontend and backend without manual maintenance.
🔒 Security Across Every Layer
- Firestore rules enforce users can only access their own runs and splits
- Storage rules isolate uploads to
media/{userId}/with a 5MB limit - DataConnect queries filter by
auth.uid - Sensitive mutations are server-only (
NO_ACCESS), preventing direct client writes - Cloud Functions validate ownership before every operation
- Firebase App Check provides device attestation
🧩 Why This Stack
The combination of Flutter + Firebase DataConnect (PostgreSQL) + Firestore is uncommon. Most Firebase apps go all-in on Firestore. Splits demonstrates that you can use Firestore for real-time ephemeral state while keeping relational data in PostgreSQL with strongly-typed GraphQL queries - getting the best of both worlds.
🎯 Takeaways
A few patterns worth stealing:
-
Match the database to the access pattern. Real-time mutable state and long-term queryable records have different needs. Two databases isn’t over-engineering if each one does a job the other can’t.
-
Lexicographic ranking is underrated. User-reorderable lists in a database without O(n) updates on every reorder.
-
Snapshot statistics at write time. Historical records should reflect what the world looked like when they were created, not today.
-
Speedrunning is a design pattern. The split-based model of breaking a process into timed segments, tracking personal bests, and comparing runs isn’t just for games. Any repeatable process benefits from the same feedback loop that makes speedrunners faster.
🚀 What’s Next
Splits is functional but there’s more to build.
Android and iOS releases. The app is built with Flutter, so cross-platform support is already in the codebase. The next step is getting Splits published on the Google Play Store and Apple App Store so anyone can download it.
Social features. Speedrunning is inherently competitive and communal. Splits will add the ability to share routines with other users, compare records on shared routines, and follow friends to see their activity. The goal is to bring the same leaderboard-driven motivation that pushes speedrunners to shave seconds off their times into everyday routines.
# Splits
<img src="/images/projects/splits/logo-256x110.png" alt="splits logo" class="" />
## 💡 The Inspiration
In video game speedrunning, a "split" is a timed segment of a run. Speedrunners break their games into segments, time each one, and obsessively compare against their personal bests. The tools they use - LiveSplit, WSplit - show real-time deltas, track records, and visualize where time is gained or lost.
**Splits** takes that concept and applies it to real life. Any routine you do repeatedly - a morning routine, a cleaning checklist, a practice session, a cooking workflow - can be broken into segments, timed, and tracked. The question isn't just "how long did it take?" but "how does this run compare to my best?"
## ⚡ What It Does
Splits is a cross-platform app built with Flutter and Firebase. The core loop mirrors speedrunning tools:
- **Create tasks** - the individual segments of your routine (like stages in a speedrun)- **Organize them into routines** - ordered sequences of tasks you run through- **Start a run** and time each split with a precision timer that supports pause/resume- **Save records** when you finish, capturing every interval down to the millisecond- **Track your stats** - min, average, and max times per task and per routine, across all your runs
Just like a speedrunner studying their splits to find where they lost time on Bowser's Castle, you can look at your records and see exactly which segment of your morning routine is the bottleneck.
<div class="flex gap-2 not-prose overflow-x-auto"> <img src="/images/projects/splits/task.png" alt="task screenshot" class="w-64" /> <img src="/images/projects/splits/split.png" alt="split screenshot" class="w-64" /> <img src="/images/projects/splits/records.png" alt="records screenshot" class="w-64" /></div>
## 🗄️ The Two-Database Architecture
One of the more interesting technical decisions is the use of *two* databases for different purposes.
**Firestore** handles ephemeral, in-progress run data. When you're mid-run, your active splits and timer intervals live in Firestore. This gives you real-time syncing and low-latency writes - exactly what you want when a timer is ticking.
**PostgreSQL** (via Firebase DataConnect) stores everything permanent: users, tasks, routines, completed records, logs, and intervals. When you finish a run, a Cloud Function transforms the Firestore data into structured PostgreSQL records and cleans up the ephemeral state.
This separation is pragmatic. Firestore excels at real-time document sync. PostgreSQL excels at relational queries like "what's my average time on Task X across all runs of Routine Y?" Each database does what it's good at.
## ⏱️ Intervals: Not Just a Single Time
Splits doesn't store a single duration per segment. It tracks **intervals** - individual start/stop pairs representing each timing session. Pause a split, take a break, resume - each segment is a discrete interval. Total duration is the sum.
This mirrors how speedrun timers work. Real-time (RTA) vs. segmented timing matters. And if you need to adjust a split after the fact, you can edit individual interval timestamps down to the millisecond - like correcting a bad split in LiveSplit.
## 🔤 Lexicographic Ranking for Segment Ordering
Reordering segments in a list sounds trivial until you need to do it efficiently in a database. If you store order as integers (1, 2, 3...), moving item 3 to position 1 means updating every item below it.
Splits uses **lexicographic ranking** instead. Each item gets a string rank like `"a"`, `"b"`, `"c"`. To insert between `"a"` and `"b"`, assign `"ab"`. Only the moved item needs an update - no cascading writes. This is used for both routine task ordering and log ordering within records.
## 📸 Snapshotting Stats at Record Time
When you finish a run, Splits doesn't just save your times. It snapshots your current personal best statistics (min/avg/max) at the moment the record is created. This means a record from three months ago reflects what your stats were *then*, not what they are today.
This is directly inspired by how speedrunners think about progression. Your PB from January means more when you can see what your average was at the time. It turns records into historical markers, not just raw data.
## ☁️ Cloud Functions as the Trust Boundary
All persistent writes go through Firebase Cloud Functions. The client never writes directly to PostgreSQL. Each function validates input with **Zod schemas**, verifies ownership, and performs operations atomically.
The `createRecord` function is the most complex - it reads the active run and all its splits from Firestore, validates every interval for chronological consistency and no overlaps, snapshots current statistics, creates the record with ordered logs and intervals in PostgreSQL, then cleans up Firestore.
## 📱 Frontend Architecture
The Flutter app uses a **feature-based** structure with Riverpod for state management. Navigation uses GoRouter with auth guards and a `StatefulShellRoute` for tab-based navigation between Home (your run history) and Library (your tasks and routines).
The UI is Material Design with a dark theme, constrained to 480px max width. Firebase DataConnect auto-generates both a Dart client SDK and a Node.js admin SDK from the GraphQL schema, keeping types in sync across frontend and backend without manual maintenance.
## 🔒 Security Across Every Layer
- **Firestore rules** enforce users can only access their own runs and splits- **Storage rules** isolate uploads to `media/{userId}/` with a 5MB limit- **DataConnect queries** filter by `auth.uid`- **Sensitive mutations** are server-only (`NO_ACCESS`), preventing direct client writes- **Cloud Functions** validate ownership before every operation- **Firebase App Check** provides device attestation
## 🧩 Why This Stack
The combination of Flutter + Firebase DataConnect (PostgreSQL) + Firestore is uncommon. Most Firebase apps go all-in on Firestore. Splits demonstrates that you can use Firestore for real-time ephemeral state while keeping relational data in PostgreSQL with strongly-typed GraphQL queries - getting the best of both worlds.
## 🎯 Takeaways
A few patterns worth stealing:
1. **Match the database to the access pattern.** Real-time mutable state and long-term queryable records have different needs. Two databases isn't over-engineering if each one does a job the other can't.
2. **Lexicographic ranking is underrated.** User-reorderable lists in a database without O(n) updates on every reorder.
3. **Snapshot statistics at write time.** Historical records should reflect what the world looked like when they were created, not today.
4. **Speedrunning is a design pattern.** The split-based model of breaking a process into timed segments, tracking personal bests, and comparing runs isn't just for games. Any repeatable process benefits from the same feedback loop that makes speedrunners faster.
## 🚀 What's Next
Splits is functional but there's more to build.
**Android and iOS releases.** The app is built with Flutter, so cross-platform support is already in the codebase. The next step is getting Splits published on the Google Play Store and Apple App Store so anyone can download it.
**Social features.** Speedrunning is inherently competitive and communal. Splits will add the ability to share routines with other users, compare records on shared routines, and follow friends to see their activity. The goal is to bring the same leaderboard-driven motivation that pushes speedrunners to shave seconds off their times into everyday routines.
Sebario