Habit-tracker is a daily habit tracker with one founding constraint: no backend. Habits and per-day completions live in one IndexedDB database, and every derived view is computed at read time rather than stored as a counter. React 19, React Router 7 and Tailwind 4, styled by ash lumen tokens compiled from JSON at build time.
architecture
two tables and two pure functions. every derived view is computed at read time from completions, so there is no counter to drift after an edit.
a missing day breaks the streak, except today: calcStreak skips an unmarked today and counts from yesterday, so streaks do not read zero every morning. skipped completions are a gap, never a break and never an extension.
Two tables. A habit has an id, a name, a creation time, an optional archive time and a sort order. A completion has a habit id, a YYYY-MM-DD date, a status of done or skipped, and a creation time, with a compound [habitId+date] index. Schema version 2 adds three sync tables.
Derivation is two pure functions. calcStreak takes the set of done dates and today. buildHeatmapGrid takes completions and a number of weeks back, 20 on the overview and 52 on a habit’s own page. Reads are live queries, mutations write to Dexie, and the components re-render. There is no state layer. Theme is the one localStorage value, applied by a blocking inline script so the first paint has the right colors.
Dates are the user’s calendar days through date-fns, compared with differenceInCalendarDays, never UTC. Sync is optional: mutations append to an outbox, drained on load, on tab focus, and five seconds after a write burst.
technical decisions
- No backend. One local database, everything derived at read time, no sync conflicts to resolve in the default configuration.
- Derived over stored counters. Streaks recompute from completions, so there is no counter to drift or repair after an edit, a delete, or a backfill.
- What a missed day means. A date absent from the done set breaks the streak, except today.
calcStreakskips an unmarked today and counts from yesterday, so streaks do not read zero every morning before check-in. A skipped completion is neither a break nor an extension. - The compound index enforces one completion per habit per day at the schema level and makes the per-day lookup an index hit.
- Tailwind 4, CSS-first. No config file. An
@theme inlineblock maps the generated token variables to utilities, so the token file stays the single source. Opacity modifiers do not work on variable colors, hencecolor-mix()in the heatmap. - Archive, not delete. Archiving keeps history.
hard problems
The streak arithmetic. The current streak walks back up to 365 days with the today-is-optional exception. The longest streak is a separate pass over lexicographically sorted date strings, using calendar-day differences for adjacency rather than string math, which keeps daylight-saving changes and month boundaries correct. Two honest caveats: the 365-day walk silently truncates a longer current streak, and there is no test on calcStreak yet. CI gates on lint and build.
numbers
| measure | value |
|---|---|
| source | 25 files, 5 routes |
| tables | 2, plus 3 for sync |
| heatmaps | 20 weeks on the overview, 52 on a habit |
| streak walk | 365-day cap |