Overview
Subscriptions accumulate quietly. Streaming, software, utilities, the gym you stopped going to in February — by the end of the year it adds up to a number that's surprising in the worst way. This Next.js app catalogs them, generates expected payments on schedule, and surfaces the trends in a dashboard. The interesting bit is the storage layer: the same code runs on Docker with SQLite or on Vercel with localStorage, no environment-specific UI branching.
Stack
- Framework: Next.js 14.2, React 18, TypeScript (strict)
- Styling: Tailwind CSS 3.4 with emerald theme + dark mode
- Database: SQLite (better-sqlite3) for Docker,
localStoragefallback for Vercel/demo - Cloud Sync: Supabase (optional)
- Charts: Recharts 2.12
- Dates: date-fns 3.6
- Testing: Jest 29 + React Testing Library
- Deployment: Docker (primary) + Vercel (demo mode)
Key Features
- Subscription and utility-bill CRUD with a tab bar
- Auto-payment generation with multi-cycle catchup for missed billing periods
- Dashboard: stats, due-soon list, recent payments, cost donut chart
- Analytics: cost trends, category breakdown, price changes, lifetime spend
- Category management (12 default categories, custom categories supported)
- Theme system: light/dark + accent color picker
- Demo data on first visit with a merge strategy (user items first, then filtered demo data)
- PWA with offline support
2-Tier Storage
DATABASE_TYPE=sqliteenables SQLite mode (Docker deployment).- Without it, the app falls back to
localStorage(Vercel/demo mode). - An
/api/configendpoint exposes thesqliteEnabledflag to the client. - Hooks call
isSQLiteMode()and branch accordingly. - Row mappers handle SQLite ↔ TypeScript conversions:
INTEGER↔ boolean,TEXT↔ JSON arrays.
Data Model (4 tables)
| Table | Notes |
|---|---|
categories | Seeded with 12 defaults via INSERT OR IGNORE on init |
subscriptions | FK to categories, tags stored as JSON text |
utility_bills | FK to categories |
payments | FK to subscriptions or utility_bills; status = paid/pending/missed/skipped |
WAL journal mode, foreign keys enabled.
localStorage Keys (fallback mode)
All prefixed with sub-tracker-: subscriptions, utilities, payments, categories, theme, accent-color, plus deleted-*-ids tracking.
State Management
Pure Context API — no Redux, no Zustand. A ThemeProvider for light/dark + accent colors, plus custom hooks for every data operation.
Environment Variables
| Variable | Purpose |
|---|---|
DATABASE_TYPE | Set to sqlite to enable SQLite backend |
DATABASE_PATH | Directory for subscription-tracker.db |
NEXT_PUBLIC_SUPABASE_URL | Optional cloud sync |
NEXT_PUBLIC_SUPABASE_ANON_KEY | Optional cloud sync |
Lessons Learned
- All
npmcommands run from theweb-app/subdirectory, not the project root — bind that to muscle memory or you'll wonder why nothing builds. - Next.js 14 needs
experimental.serverComponentsExternalPackages: ['better-sqlite3']for native modules to load. - Track deleted demo IDs separately or you'll re-create them every page reload.
- API routes return 501 when SQLite is disabled. That's intentional — the client uses it as a feature-detection signal.
Testing
112 tests (Jest + React Testing Library). CI runs lint → test → build on every PR.