Feb 2026 – present
Noboru
In progressRole Co-founder. Backend and core mobile features.
A hiking companion app for iOS and Android: browse trails and parks, collect passport stamps, and plan trips that keep working with no signal.
Stack: React Native · Expo · TypeScript · .NET · AWS Lambda · Aurora PostgreSQL · SQLite
- trails in the catalog
- 38,005
- National Parks and forests covered
- 193
The problem
Hikers plan at home and then lose signal on the trail. Noboru is a React Native app with five tabs: Home, Explore, Community, Journal, and Profile. National parks, forests, and trails are real data, and a trip's itinerary, checklists, expenses, and notes have to stay usable when the network goes away.
My role is covered in Expeditions. This page is about the product and how it works.
The hardest stretch
Offline-first trip planning on top of a backend that was online-only. I built it in five phases, each one a prerequisite for the next.
- A local SQLite cache, read before the network, so a trip opens instantly and stays readable with no connection.
- Client-generated UUIDv7 ids, with every create idempotent on its id, so a retried request never duplicates a row.
- A durable write outbox. An action applies locally at once, appends a mutation to an ordered queue, and never waits on the network. Creating then deleting the same item cancels both, so nothing is sent.
- An incremental pull against a per-trip change cursor, with tombstones for deletes. The cursor only advances after the changes are applied, so a failed pull retries the same range.
- A conflict policy for notes.
Decisions
Chose A .NET API on AWS Lambda behind API Gateway, with Cognito tokens
Rejected The app talking to Aurora directly through the RDS Data API
In the direct setup any signed-in user could obtain credentials that ran arbitrary SQL as the cluster's master user. Now only the backend touches the database, through a least-privilege role, and it decides who owns a row.
Chose Client-generated ids
Rejected Server-assigned ids with local-to-server remapping
A row gets its real, final id when it is created, so it never needs remapping after it syncs, and a retry can be replayed safely.
Chose One changes feed for first load and refresh
Rejected A separate cold-load code path
A trip with no stored cursor pulls since null, which the backend treats as everything. There is one path to keep correct instead of two.
Chose Last-write-wins per field, except notes, which get a conflict copy
Rejected A merge UI or a text CRDT
A note is long-form writing, and silently overwriting it is the one clearly wrong outcome. A conflict copy never loses writing and needs no new data structure. Resolving it is just editing or deleting a note.