← home · /hire

Case study

Dryad — Focus Session Tracker

Personal project · Apr 2026 · React Native · FastAPI · PostgreSQL · Docker · Expo

[01] batu@batu0:~/case-studies/dryad

A personal focus-session tracker built entirely solo — product thinking, backend, mobile, animation, DevOps. You plant a virtual tree at the start of a work session; it grows as time passes. Abandon the session and it dies. The visual centrepiece is a De Morgan-aesthetic nymph (a Greek hamadryad — a wood nymph whose lifespan is bound to a single tree) from whom the tree grows. Named accordingly.

No AI, no monetisation, no tracking.

The idea

Forest is a well-known productivity app on this pattern. I had years of personal session history in it and wanted to understand the mechanics from the inside — and own the data. The engineering brief wrote itself: build the same core loop, add forensic logging the original doesn’t have, and do it in a stack I’d pitch to a client.

What I built

Offline-first sync

Sessions are created in local SQLite immediately so the timer starts without a network round-trip. A background sync service finds unsynced rows and pushes them to PostgreSQL on reconnect. Forensic log events queue locally and replay in order — so the server history is always reconstructable even after extended offline use.

Timer state machine

Deterministic FSM: IDLE → RUNNING → PAUSED → FINISHED (with ABANDONED exits). When the app is backgrounded, a timestamp is recorded; on foreground, elapsed time is corrected (elapsed += floor((now − backgroundedAt) / 1000)) so the timer stays honest across lock-screen and multitasking. The FSM was the first thing designed and tested — nothing else touches time state directly.

Forensic session logging

Every timer event (START, TICK, PAUSE, RESUME, ABANDON, FINISHED, APP_BACKGROUND, APP_FOREGROUND) is written to session_log_events with millisecond-precision timestamps. This gives you the exact interruption points, device-suspension detection, and a fully re-constructable session history — something Forest’s export doesn’t expose at all.

SVG tree animation

The nymph figure and tree are drawn with react-native-svg. Branch growth uses strokeDashoffset cubic-bezier draw animation; leaves use Animated.Value scale/fade. Six visual stages tied to elapsed-time percentage. Tag colour tints branches (full HSL saturation) and leaves (HSL-lightened). At completion the nymph opacity fades while the tree silhouette intensifies — a “merging” effect that plays on the hamadryad mythology.

JWT auto-refresh with request queueing

Axios interceptor detects 401 responses, fires one refresh request, queues all other in-flight requests, and replays them after the new token arrives. Tokens stored in Expo SecureStore (OS-level secure storage). Prevents the double-refresh race that breaks most naive implementations.

Forest data import

A CLI script (scripts/import_forest.py) auto-detects CSV or JSON format, parses Forest’s date format, and bulk-imports 110+ personal tags and historical sessions via the API. Seeded the stats view with years of existing data on day one.

Adaptive push notifications

21 local notifications (3 daily × 7 days) rescheduled on every app foreground. Copy shifts tone by streak count, days since last session, last tag used, and tree stage reached. No server required — Expo handles scheduling. Sardonic by design.

Microservice discipline

Each service owns its tables; stats_service uses reflected ORM models (read-only). Shared db.py lazy-initialises the async SQLAlchemy engine so services don’t race on startup. Routes are thin; business logic lives in service classes. Every route has happy path, 401, 422, and 404 test coverage (TDD throughout).

Stack

LayerTechnology
MobileReact Native, Expo Managed, Expo Router (file-based)
Mobile stateContext API + useReducer (5 contexts)
Mobile animationreact-native-svg, react-native-reanimated
Local storageExpo SQLite (sessions), AsyncStorage (tags/auth cache), SecureStore (tokens)
BackendFastAPI + Uvicorn, Python 3.10+
ORM / migrationsSQLAlchemy (async + asyncpg), Alembic
DatabasePostgreSQL 16
AuthJWT (python-jose + passlib/bcrypt)
GatewayNginx
InfraDocker Compose (6 containers)
Testingpytest + pytest-asyncio (backend), jest-expo + Testing Library (mobile)

Scale & outcomes

  • Personal project — the only client is me and my session history.
  • ~87 commits, 13 implementation plans (all FINISHED), Apr 2026.
  • MVP shipped with full stats view: 52-week heatmap, bar charts, streak counter, forest gallery.
  • Seeded with 110+ Forest tags and years of historical session data via the import script.
  • Post-MVP deferred: Firebase multi-device sync, BREAK/LONG_BREAK phases (FSM ready, UI deferred), tag merge tooling, CSV export.

← back to work · see /hire for similar engagements