Cadence
A personal cognitive continuity system: habits, daily reflection, structured check-ins, and conversational logging that carry context from one day to the next.
FastAPI · SQLAlchemy · Alembic · SQLite · React · TypeScript
Overview
Cadence started as a habit tracker and turned into something more useful: a record of what I was doing and thinking, day by day, that does not lose the thread. Days can be closed and reopened, unfinished work carries forward as explicit threads, and a week can be reconstructed from the days that made it up.
It is the project where I took the operational parts seriously. Migrations are versioned, backups are verified and reversible, and the AI features are off until a user turns them on. Most of the work went into the parts that only matter when something goes wrong: a restore, a rate-limited model, a day left half written.
Architecture
A monorepo: a FastAPI package and a Vite React client. Routes stay thin, domain services hold the rules, SQLAlchemy models are versioned by Alembic, and SQLite is the store. External services, mail and AI, sit behind explicit consent and fall back cleanly.
React client
Vite, TypeScript, Tailwind. Dashboard, day detail, and continuity views.
FastAPI routes
Request validation and response shaping per resource.
Domain services
Habit state, day lifecycle, carry-forward threads, and summary generation.
SQLAlchemy
Alembic
Models and the migration history that keeps schema versions explicit.
SQLite
Single-file store with WAL, online backups, and verified restores.
Generating a daily summary
1. POST /api/days/{date}/summary/generate
The client asks for a summary of one closed day.
2. Check consent
External AI is off per user until enabled in settings. Without consent the manual summary path is still available, so the feature degrades rather than fails.
3. Gather bounded sources
The service collects that day's notes, check-in, and conversation entries. Bounded on purpose: the prompt is built from a known set of records, which is what makes the result traceable back to sources.
4. Redact, then call
With redaction on, email addresses and phone-like values are replaced in the outbound prompt only. Local data is never rewritten.
5. Fall back on failure
Models are tried in a ranked order. A 429 marks that model rate-limited and the runtime moves to the next eligible one.
6. Persist the artifact
The summary is stored as an editable artifact, so a generated draft can be corrected by hand and still belongs to the day.
Database schema
habits / habit_logs
A discipline, and one row per day it was completed.
- id
- integer · primary key
- user_id
- integer · owner
- name
- text
- archived_at
- datetime · archive preserves history, delete does not
- log.date
- date · unique with habit, so a toggle is idempotent
days
The unit everything else hangs off: note, status, and links.
- date
- date · unique per user
- note
- text · nullable
- status
- text · open or closed, reopenable
- summary
- text · generated or hand-edited artifact
daily_checkins
Structured self-report: energy, focus, sleep, and similar.
- day_id
- integer · one per day
- energy
- integer
- focus
- integer
- sleep
- real
conversation_entries
Chat-style log for a day, kept as ordered entries rather than one blob.
- day_id
- integer
- body
- text
- created_at
- datetime · ordering
carry_forward
An unfinished thread inherited by the next day until completed or released.
- origin_day
- date
- state
- text · open, completed, released
- context_id
- integer · nullable link to a project or area
API design
- GET/api/habits/month?month=YYYY-MMauthMonth grid in one request.
- POST/api/habits/toggleauthToggle a habit log for a date.
- PUT/api/days/{date}/checkinauthStructured check-in for a day.
- POST/api/days/{date}/summary/generateauthSource-traceable summary through AI fallback.
- PATCH/api/days/{date}/statusauthClose or reopen a day.
- GET/api/days/{date}/carry-forwardauthThreads inherited by this day.
- GET/api/continuity/weeks/{date}authReconstruct that date's week and open threads.
- GET/api/continuity/search?q=authSearch one bounded year of owned sources.
- GET/api/account/exportauthVersioned JSON export of everything the user owns.
- PUT/api/account/ai-preferencesauthExternal-AI consent and outbound redaction.
Challenges
A restore that cannot quietly destroy data
Restoring a backup is the one operation that can lose everything. Restore refuses to run while the API holds its runtime lock, requires an exact schema-version match, and takes a fresh verified safety backup of the live database first. If post-install verification fails, that safety backup goes back automatically.
Backups that are actually consistent
Copying a SQLite file with WAL enabled can capture a torn state. Snapshots go through SQLite's online backup API and are integrity-checked before they get their final filename, so a file that exists is a file that restored cleanly.
AI that cannot leak by default
External AI is off per user until enabled, redaction strips emails and phone-like values from the outbound prompt without touching stored data, and nothing is sent automatically. Manual summaries and reflections work without AI at all.
Not confirming whether an account exists
A resend-verification endpoint is an account enumeration oracle if it answers differently for known and unknown addresses. It responds the same either way.
Lessons learned
- The risky operations deserve the most design. Backup and restore took longer than the features they protect, and that was the right trade.
- Consent is a data-flow decision, not a settings screen. Off by default, bounded prompts, redaction at the boundary.
- Derive what you can and store what you must, but keep generated artifacts editable: a draft nobody can correct is worse than no draft.
- A schema-version check turns an irreversible mistake into an error message.