A habit tracker built from scratch to actually learn Redis and cron jobs — not just read about them. Every architectural decision in this repo exists to make that learning concrete: Redis handles the one interaction that happens constantly (marking a habit done), and a nightly cron job is what turns that ephemeral state into permanent streak history.
🔗 Repo: github.com/TacticalReader/Routineforge
Most habit trackers write directly to a relational database every time a user checks off a habit. While this works for small apps, it doesn't scale well when thousands of users are tapping buttons simultaneously.
RoutineForge was created to solve this specific problem by introducing a caching layer. By using Redis for ephemeral state (daily completions) and PostgreSQL for permanent state (historical data), the app minimizes database round-trips and provides a lightning-fast user experience. It's a practical playground for mastering caching strategies, background jobs, and modern web architecture.
- ✅ Create, complete, and track daily habits: Easily manage your daily routines with a clean, intuitive interface.
- 🔥 Automatic streak tracking: Keep track of your current and longest streaks to stay motivated.
- ⚡ Instant habit-completion toggling: Backed by Redis — not a database write on every tap. Enjoy sub-millisecond response times.
- ⏰ Nightly cron jobs: A background process that finalizes the day, updates streaks, and flushes data to Postgres.
- 📬 Morning reminder emails: Automated emails via Resend for habits with a streak worth protecting.
- 🔐 Full auth flow: Secure, protected routes powered by Supabase Authentication.
- 🎨 Flat, high-contrast UI: A deliberate, single-theme design with no light/dark toggle for maximum focus.
- 📊 Analytics Dashboard: Visualize your progress over time (Coming soon).
| Layer | Technology |
|---|---|
| 🖥️ Frontend | React.js (Vite) + React Router |
| 🗄️ Database & Auth | Supabase (Postgres + Auth) |
| ⚡ Caching / Queue | Upstash Redis (REST API) |
| ⏱️ Scheduled Jobs | Supabase Edge Functions + pg_cron |
| Resend | |
| 🎨 Icons | lucide-react |
| ☁️ Hosting | Vercel |
This is the part of the project that actually matters, so it's worth spelling out:
- Today's completions live in Redis, not Postgres — stored as a hash
(
completions:<userId>:<date>) with a 48-hour TTL. Toggling a habit is a sub-millisecond Redis write instead of a database round-trip. - Streak counts are cached in Redis too (
streak:<habitId>), using a cache-aside pattern: read the cache first, fall back to Postgres on a miss, then repopulate the cache. - A nightly cron job (
nightly-streak-processor, running at 11:59 PM viapg_cron) reads each user's Redis completions, writes the permanent record intohabit_completions, recalculates streaks, and refreshes the Redis cache — closing the loop between "ephemeral" and "permanent" state. - A morning cron job (
morning-reminder-dispatch, running at 8 AM) emails users who have an active streak worth protecting.
routineforge/
├── public/
│ ├── favicon.svg
│ └── manifest.json
│
├── src/
│ ├── assets/
│ │ └── icons/
│ │
│ ├── components/
│ │ ├── common/
│ │ │ ├── AppButton.jsx
│ │ │ ├── AppModal.jsx
│ │ │ └── LoadingSpinner.jsx
│ │ ├── habits/
│ │ │ ├── HabitCard.jsx
│ │ │ ├── HabitForm.jsx
│ │ │ ├── HabitCompletionToggle.jsx
│ │ │ └── StreakBadge.jsx
│ │ └── layout/
│ │ ├── AppHeader.jsx
│ │ ├── AppSidebar.jsx
│ │ └── AppFooter.jsx
│ │
│ ├── pages/
│ │ ├── DashboardPage.jsx
│ │ ├── HabitDetailPage.jsx
│ │ ├── AuthPage.jsx
│ │ └── SettingsPage.jsx
│ │
│ ├── hooks/
│ │ ├── useHabits.js
│ │ ├── useStreakData.js
│ │ └── useAuthSession.js
│ │
│ ├── services/
│ │ ├── supabaseClient.js
│ │ ├── habitService.js
│ │ ├── redisCacheService.js
│ │ └── notificationService.js
│ │
│ ├── context/
│ │ └── SessionProvider.jsx
│ │
│ ├── routes/
│ │ └── AppRoutes.jsx
│ │
│ ├── utils/
│ │ ├── dateHelpers.js
│ │ └── streakCalculator.js
│ │
│ ├── styles/
│ │ └── global.css
│ │
│ ├── App.jsx
│ └── main.jsx
│
├── supabase/
│ ├── functions/
│ │ ├── nightly-streak-processor/
│ │ │ └── index.ts
│ │ └── morning-reminder-dispatch/
│ │ └── index.ts
│ ├── migrations/
│ │ └── 0001_init_schema.sql
│ └── config.toml
│
├── notes/
│ └── project-log.txt
│
├── .env.example
├── package.json
├── vite.config.js
├── vercel.json
└── README.md
Copy .env.example to .env and fill in:
VITE_SUPABASE_URL=
VITE_SUPABASE_ANON_KEY=
VITE_UPSTASH_REDIS_REST_URL=
VITE_UPSTASH_REDIS_REST_TOKEN=The Edge Functions in supabase/functions/ need their own secrets, set
separately via the Supabase CLI (never committed to .env):
supabase secrets set SUPABASE_URL=https://<project-ref>.supabase.co
supabase secrets set SUPABASE_SERVICE_ROLE_KEY=<service-role-key>
supabase secrets set UPSTASH_REDIS_REST_URL=<your-upstash-url>
supabase secrets set UPSTASH_REDIS_REST_TOKEN=<your-upstash-token>
supabase secrets set RESEND_API_KEY=<your-resend-key>git clone https://github.com/TacticalReader/Routineforge.git
cd Routineforge
npm install
cp .env.example .env # then fill in your credentials
npm run devThe app runs at http://localhost:5173.
| Command | Description |
|---|---|
npm run dev |
Start the local dev server |
npm run build |
Build for production |
npm run preview |
Preview the production build locally |
npm run lint |
Run ESLint |
Frontend (Vercel):
- Push to GitHub
- Import the repo in Vercel
- Add the four
VITE_*environment variables in the Vercel dashboard - Deploy —
vercel.jsonhandles client-side routing rewrites automatically
Backend (Supabase):
supabase link --project-ref <your-project-ref>
supabase db push
supabase functions deploy nightly-streak-processor
supabase functions deploy morning-reminder-dispatchCron schedules are registered via pg_cron + pg_net, with the
service role key stored in Supabase Vault rather than hardcoded into the
schedule SQL. See notes/project-log.txt for the exact statements used.
| Job | Schedule | Purpose |
|---|---|---|
nightly-streak-processor |
59 23 * * * (11:59 PM) |
Flushes Redis completions → Postgres, recalculates streaks |
morning-reminder-dispatch |
0 8 * * * (8:00 AM) |
Emails users with an active streak worth protecting |
Check job health anytime with:
select * from cron.job_run_details order by start_time desc limit 10;- PWA Support: Install RoutineForge on your mobile device for quick access.
- Detailed Analytics: Weekly and monthly charts to visualize habit consistency.
- Social Accountability: Share your streaks with friends or accountability partners.
- Customizable Themes: Add a few carefully curated color palettes.
Contributions, issues, and feature requests are welcome! Feel free to check the issues page.
- Fork the project
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
Distributed under the MIT License. See LICENSE for more information.