Project ·

JigSpot

Offline-first React Native app + a 100% serverless Cloudflare API for Filipino anglers.

  • React Native
  • Expo
  • Cloudflare Workers
  • D1
  • MapLibre
  • TypeScript

Case study snapshot

Role
Product builder and engineer across mobile app, API, offline data, mapping, and product scope.
Scope
Offline maps, GPS trip recording, catch logs, spot sharing, local storage, sync, and Cloudflare API.
Stack
React Native, Expo, Cloudflare Workers, D1, MapLibre, TypeScript
Status
Native iOS and Android apps in Google Play internal testing.
Proof
Network-optional core loop with maps, GPS trip tracking, and serverless APIs.
Outcome
Offline-first fishing app for weak-signal offshore conditions.

JigSpot turns a smartphone into a marine fish-finder companion: it shows seafloor depth contours and structure, records fishing trips by GPS, logs catches, and lets friends share productive spots — all working offline at sea, with no cell signal.

At a glance

  • Role: Product builder and engineer across mobile app, API, offline data, maps, and sync architecture.
  • Scope: Offline maps, GPS trip recording, catch logs, spot sharing, subscription gating, serverless API, and map data pipeline.
  • Stack: Expo, React Native, MapLibre, SQLite, MMKV, TanStack Query, Cloudflare Workers, D1, R2, TypeScript, Python.
  • Status: Native iOS and Android apps in Google Play internal testing.
  • Proof: Network-optional core loop, around 30 JSON API routes, shared Zod contracts, and custom bathymetry bundles served from R2.

The problem

Filipino anglers who jig and bottom-fish offshore hit a stack of problems that off-the-shelf apps don’t solve:

  • No signal where the fish are. Productive grounds are kilometres offshore, outside cellular coverage — so anything that needs the network is useless exactly when it matters.
  • Generic maps don’t show the seafloor. Anglers need bathymetry — depth contours, drop-offs, and structure — because fish hold on specific depth bands and features, not on the surface.
  • Chartplotters are expensive and stuck on the boat, and local knowledge of “good spots” stays trapped in people’s heads with no record of what worked.

The goal: an affordable, offline-first “smart fishing log + seafloor map” in every angler’s pocket, with hard-won local knowledge made shareable.

The solution

An offline-first mobile app backed by a lightweight serverless API. The phone is the source of truth during a trip; the cloud is for sync, sharing, and distributing map data.

  • Offline bathymetric maps — vector depth contours and structure edges rendered with MapLibre. Region packs download over Wi-Fi and run fully offline, with target-depth highlighting and an “examine area” tool that reports depth, slope, and a computed fishability score.
  • GPS trip tracking — one-tap recording that logs the route in the background with the screen off, plus in-trip catch logging and auto-generated shareable trip cards.
  • Spots & sharing — save spots with photos, revisit them offline, and share them into a trusted circle.
  • Friends & live location — opt-in live broadcast with battery-aware polling.
  • Catch log & conditions — filterable history with per-trip weather/marine conditions so results can be correlated with tide and weather over time.

Premium features (broadcast, offline_download, premium_map, spot_limit) are gated server-side as the source of truth, with the UI mirroring it.

Technical overview

The whole backend runs on Cloudflare’s edge — no servers to manage, global low latency, and a cost model that scales to zero.

Mobile — Expo SDK 56 / React Native 0.85 / React 19 in TypeScript, with Expo Router, @maplibre/maplibre-react-native for native vector maps, expo-sqlite as the local database, MMKV for settings, and Zustand + TanStack Query for state. Background GPS uses an expo-location + expo-task-manager foreground service for screen-off tracking.

Offline-first data layer — local SQLite is the source of truth during a trip. Typed repositories cover tracks, spots, trips, photos, region packs, and a sync queue; writes happen locally and instantly, then reconcile with the Worker when connectivity returns.

API — a single Cloudflare Worker (~30 JSON routes) with Better Auth, Cloudflare D1 (edge SQLite, strictly parameterized queries), and R2 for versioned map bundles (PMTiles + GeoJSON) proxied for efficient offline caching.

Shared monorepo@jigging-map/core holds Zod schemas and the fishing math (target depth, fishability scoring, geo, route simplification), and @jigging-map/api is a typed client used by both the app and a companion PWA — so the contract can’t drift between clients. Every Worker response is parsed through a core Zod schema before it’s trusted.

Map data pipeline — bathymetry is generated from raw GEBCO seafloor rasters: Python derives contours and structure into compact PMTiles + GeoJSON, a Node script publishes versioned bundles to R2 with a manifest, and the Worker serves them — with reduced-detail free variants for the free tier.

Key decisions

  • Truly offline, not “offline-tolerant” — the network is optional for the entire core loop; the device is the source of truth and syncs later.
  • Imperative map control — camera, sources, and layer styling are kept out of React/Zustand state and driven through a dedicated controller, avoiding a whole class of re-render bugs and keeping panning smooth on mid-range Android.
  • One set of rules, three surfaces — depth bands, fishability, and validation live once in shared code and mean the same thing in the app, the PWA, and the API.

Status

Native iOS and Android apps on a shared, type-safe monorepo with a companion PWA, a fully serverless Cloudflare backend, and a custom offline map pipeline from raw bathymetry. Currently in Google Play internal testing, ahead of a public launch.

Have a similar system to build?

Send the product goal, timeline, and current blockers. I’ll help you turn it into a practical build plan.

Start a conversation

All works