Personal habit and goal tracker for iOS. Single user, local only — no backend, no accounts, no network calls. Everything lives in SQLite on the phone.
Full brief: SPEC.md.
Expo SDK 54 · React Native 0.81 · TypeScript · expo-router · expo-sqlite +
drizzle-orm · expo-notifications · expo-file-system / expo-sharing
Pinned to SDK 54 on purpose — do not upgrade casually. Expo Go on the iOS App Store is stuck at v54.0.2 (Sept 2025); the SDK 55/56/57 builds are still in Apple review. Expo Go supports exactly one SDK at a time, so anything newer than 54 cannot be opened on the phone at all — it just says "download the latest version of Expo Go", which no update fixes.
eas gobuilds a matching Expo Go, but that needs a paid Apple Developer membership. Upgrade only once a newer Expo Go actually ships to the App Store.
npm install
npm start # then press `i`, or scan with Expo Go
npm test # jest
npm run typecheck| # | Step | Status |
|---|---|---|
| 0 | Scaffold: router, dark theme, timezone-safe date layer | ✅ |
| 1 | Schema and migrations | ✅ |
| 2 | Today screen with logging | ✅ |
| 3 | OVR computation + tests for the math | ⬜ |
| 4 | Progress screen | ⬜ |
| 5 | Goals | ⬜ |
| 6 | Notifications and export | ⬜ |
Each step lands as its own commit and stops for review.
app/ expo-router routes
_layout.tsx runs migrations + seed, then the dark stack
(tabs)/ Today · Progress · Goals · Manage
drizzle/ generated migrations — do not hand-edit
src/
theme.ts dark palette, spacing, type scale
db/schema.ts the 8 tables
db/client.ts expo-sqlite + drizzle instance
db/queries.ts reads as query builders, writes as functions
lib/date.ts timezone-safe local YYYY-MM-DD handling
lib/habits.ts pure scheduling + credit maths (tested)
components/
Changing src/db/schema.ts means regenerating migrations:
npx drizzle-kit generateThe interface is built as a player rating card, because that is what the app actually is — a 0–100 rating that moves two points a day and has to be earned. Everything follows from that:
- Type. Saira Condensed for ratings (jersey numerals), IBM Plex Sans for
body, IBM Plex Mono for anything tabular. Import weights from the deep paths
(
@expo-google-fonts/ibm-plex-sans/400Regular) — the package barrel pulls every weight of every family into the bundle. - Colour is data. Tier colours (BRONZE → ELITE) encode a rating; category colours encode identity. Nothing is coloured to look nice. If something needs emphasis and isn't a rating, it gets weight or space, not a new hue.
- Meters are segmented, one segment per scheduled instance, because completion is counted in whole instances. Over 14 it degrades to a plain bar.
- One moment of motion: the step handoff in the new-habit flow. It respects
AccessibilityInfo.isReduceMotionEnabled.
Adding a habit is a guided flow (app/habit/new.tsx), one question per screen,
with suggestions per category so it's mostly tapping. The flow length changes
with your answers — the amount question only exists for numeric habits.
Dates. Every date is a local calendar day stored as YYYY-MM-DD. Never
call new Date('2026-07-29') — that parses as UTC midnight and lands on the
previous local day west of Greenwich, which would shift logs and break streaks.
Go through fromISODate / toISODate in src/lib/date.ts. There are tests.
Ratings. The displayed rating is not the computed rating. It walks toward
the computed value at most 2 points per day, from the snapshot in
ratings_history. A category with no data shows --, not 0.
© 2026 Arunachalam Kasi. All rights reserved. Public for review purposes. No license granted for reuse or redistribution.