Skip to content

Repository files navigation

Habitude

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.

Stack

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 go builds 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.

Run it

npm install
npm start        # then press `i`, or scan with Expo Go
npm test         # jest
npm run typecheck

Build status

# 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.

Layout

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 generate

Design language

The 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.

Two things that are easy to get wrong

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.

About

Personal offline habit and goal tracker for iOS. Expo + React Native + SQLite. Single user, no backend.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages