A full-stack habit tracker built with Flask, SQLAlchemy, and vanilla JS. This is an enhanced rebuild of the original Habit Tracker project: same core idea, with a real design system, streaks, reports, and a friendlier data model.
- Accounts. Threadline now supports multiple people on one deployment — each person registers their own login, and habits, completions, and notes are private to their account. Enforced server-side on every API route, not just hidden in the UI.
- Full front end. Four working pages (Dashboard, Calendar, Reports, Settings) with a custom "habit ledger" visual design — no template UI kit, no Bootstrap defaults.
- Streak math, done properly. Current streak, best streak, and 30-day completion rate are computed server-side per habit, and there's a combined "every due habit, every day" overall streak on the dashboard.
- Frequency-aware scheduling. Habits can be Daily, Weekdays, Weekends, or Custom (pick specific weekdays) — the calendar and streaks respect this, so a "Mon/Wed/Fri" habit isn't penalized for Tuesdays.
- Monthly ledger view. A spreadsheet-style grid (habits × days) to tap through a whole month at once, styled like the README always promised.
- Reports page. Weekly and monthly views — category breakdown, per-habit streak/rate bars, a trend vs. last week, and a month heatmap.
- Journal. A dedicated page for daily notes; once a day ends, that entry is sealed and can't be edited — same rule as habit completions.
- Days lock at midnight. Completions and journal entries can only be edited for today; once a day is over, it's a permanent record.
- Archive instead of just delete. Retire a habit without losing its history; permanent delete is still available and asks for confirmation.
- Daily reminders. Optional, local to your device — a nudge (in-app and as a browser notification) if you still have open habits at a time you set.
- Installable on mobile. A Progressive Web App — add it to your home screen from Chrome or Safari for an app-like, full-screen experience.
- Input validation + JSON error responses on every write endpoint.
- Dark / light mode, remembered across visits.
- Safe upgrades. If you already have a
database.dbfrom an earlier, single-user version of this app, startup adds the new columns and folds any pre-existing habits/notes into one fallback account automatically — nothing is deleted. See "Upgrading from the single-user version" below.
- Python 3.8+
- Install dependencies:
pip install -r requirements.txt
- Run it:
python app.py
- Open http://127.0.0.1:5000 and create an account — the first screen you'll see is the login page, with a link to register.
app.py Routes: auth, pages, JSON API
models.py User / Habit / Completion / Note models, scheduling logic
stats.py Streaks, completion rate, weekly/monthly report calculations
database.py SQLAlchemy instance
templates/ Jinja pages (auth, dashboard, calendar, reports, settings, journal)
static/css/ Design system (tokens, layout, components)
static/js/ Page logic + shared helpers (api, toasts, modal, theme, reminders)
static/manifest.json, static/sw.js, static/icons/ PWA install support
Every /api/* route requires a logged-in session and only ever touches
that account's own data (verified server-side, not just filtered in the UI).
| Method | Path | Purpose |
|---|---|---|
| GET/POST | /api/habits |
list / create habits |
| PUT | /api/habits/<id> |
update a habit |
| PATCH | /api/habits/<id>/archive |
archive / restore |
| DELETE | /api/habits/<id> |
permanently delete |
| GET | /api/completions |
completions in a date range |
| POST | /api/completions/toggle |
mark/unmark a habit done — today only |
| GET/POST | /api/notes |
read/write a day's journal entry — today only to write |
| GET | /api/dashboard |
today's list + streaks + progress + insight |
| GET | /api/habits/<id>/stats |
streaks + 30-day rate for one habit |
| GET | /api/reports/weekly |
this week's numbers + trend vs. last week |
| GET | /api/reports/overview |
category breakdown + heatmap for a month |
- Backend: Flask, Flask-SQLAlchemy, Flask-Login, SQLite (or Postgres — see Deploying below)
- Frontend: Jinja templates, hand-written CSS design system, vanilla JS (no build step, no framework)
- Fonts: Fraunces (display), Inter (body), JetBrains Mono (data)
- Push this folder to a GitHub repo.
- On railway.app, New Project → Deploy from GitHub repo.
- Railway auto-detects the
Procfileand installsrequirements.txt— no build config needed. - Add Postgres: in the project, New → Database → PostgreSQL. Railway
creates a
DATABASE_URLvariable automatically; reference it from the web service under Variables → Add Reference. (SQLite works too if you'd rather skip this, but Railway's filesystem isn't guaranteed to persist across deploys — Postgres is the safer default.) - If you added Postgres, uncomment
psycopg2-binaryinrequirements.txtand redeploy. - Set a
SECRET_KEYvariable to a long random string (e.g.python -c "import secrets; print(secrets.token_hex(32))"). Without this, the app generates a random one on each boot, which logs everyone out every time it restarts or redeploys. - Railway gives you a
*.up.railway.appHTTPS domain automatically — that's what makes the PWA install prompt available on phones. - Open the domain, register the first account, and start tracking.
The same steps work on Render or Fly.io with minor UI differences — the
Procfile and DATABASE_URL/SECRET_KEY env vars are what matter, not the
specific host.
If you're moving an existing database.db from before accounts existed:
copy it into instance/database.db next to app.py and start the app once.
It adds the missing columns and creates a locked legacy@local account that
owns all your old habits and notes — nothing is lost, but that account's
password is randomly generated and not shared anywhere, so you can't log
into it as-is. If you need to actually use that old data going forward,
the simplest path is to open the database directly (sqlite3 instance/database.db) and update user.email /
user.password_hash for that row to credentials you choose (use
werkzeug.security.generate_password_hash for the hash) — or just start
fresh with a new account, since this migration exists mainly so the upgrade
doesn't silently delete anything.