The problem
Lifekeep began as a replacement for a personal spreadsheet that tracked habits against dates for years. Spreadsheets get one thing right that most habit apps get wrong: the past stays the past. Change your target from 30 minutes to 40 and a typical app quietly re-scores last month against the new number. Miss a day because you were sick and the streak resets as if you'd failed.
Lifekeep's starting principle is that plans are hypotheses, not moral contracts. The plan should adapt to the person, with evidence, and never by rewriting history. Most of the architecture follows from that sentence.
Architecture
Lifekeep is a TypeScript monorepo. The web app is Next.js 16 on React 19 with Tailwind 4 and TanStack Query. The API is a thin REST layer under /api/v1: authenticate, parse with Zod, call a use case, serialise. There are no server actions for mutations, so the web app, the native clients and external AI clients all go through the same contract. An OpenAPI 3.1 document is generated from the Zod schemas.
Below the routes, the code is split into three layers, enforced by lint rules:
src/core: pure TypeScript with no framework, database or model imports. The planner, the adaptation signals, recurrence, consistency maths, the Islamic calendar and the operation language live here, which makes them fast to test.src/application: use cases, one database transaction per mutation.src/infrastructure: Drizzle, auth, model providers and integrations.
History that can't be rewritten
Every edit to a habit creates a new row in habit_versions covering a half-open date range [effective_from, effective_until). By default an edit takes effect tomorrow, or at the next cycle boundary for weekly-quota habits. All rendering goes through one function, resolveVersion(versions, date), so a day in June is always judged by the rules that applied in June.
We didn't want this to depend on application code being careful. Postgres enforces it with an EXCLUDE USING gist constraint over the date ranges, a check on interval sanity, and a partial unique index allowing only one open version. The same pattern covers prayer settings, Hijri date adjustments and goal versions. A habit's type is immutable: turning a yes/no habit into a measured one creates a successor habit linked to the original.
A handful of other invariants live in code and tests rather than in settings:
- "Missed" is computed, never stored, so backfilling a day always fixes it.
- Avoidance habits are never inferred from silence.
- Excused skips leave the denominator instead of earning partial credit.
- A day you couldn't track is a gap, not a zero.
- There is no single discipline score and no faith score.
Weekly adaptations: rules with evidence, not vibes
The weekly review's suggestions ("it usually takes you 50 minutes, not 30") come from nine deterministic detectors in src/core/adaptation, not from a language model. They include duration_bias, window_preference, weekday_preference, recurrence_mismatch, capacity_overcommitment, task_estimation_bias, minimum_level_recovery, energy_load_response and plan_rejection_pattern.
They share one evidence vocabulary: observations, distinct days, eligible days, coverage, a time window, and a mandatory, non-empty list of limitations. Confidence is one shared function. "High" needs at least 12 distinct days and 60% coverage, and "moderate" needs 8 days and 40%. Proposals offer alternatives with none pre-selected. Learned values become bounded settings; the task-estimate factor, for example, is constrained to between 0.5 and 3.0 by the database. Accepting a suggestion writes a new effective-dated version, so it changes the future and leaves the past alone.
A planner that explains itself
The day planner, planDay() in src/core/dayflow, is a pure function: "a lens, not a scheduler". It gathers fixed items first: prayer times (computed with the adhan library, including travel, arrive-early and stay time) and calendar events as hard blocks. Then it places flexible habits and tasks into free gaps inside their preferred windows, ordered by importance and then by your own ranking. Modes add directives: Ramadan mode can move training to after iftar, and a low-energy day caps suggestions at 90 minutes and sheds items you've marked as optional.
Every block carries the factors that placed it, and trace.ts turns them into sentences like "≈45m, learned from your last 3 sessions". If calendar availability is unknown, the planner pauses suggestions rather than planning over meetings it can't see. Replanning never touches items you've already decided on, and something you rejected doesn't sneak back in.
AI proposes, deterministic code writes
Lifekeep supports ten model providers (Anthropic, Gemini, OpenAI, Mistral, Groq, DeepSeek, xAI, Together, OpenRouter and Perplexity) through plain fetch, with your own key. Prompts are versioned files in the repo, and untrusted text is wrapped as a defence against prompt injection. The model's job is narrow: turn language into a typed proposal.
Writes go through LCAP, Lifekeep's operation language: 39 typed operation kinds, assembled into ChangeSets with impact manifests and risk classes. A ChangeSet's validation status is derived and can never be supplied by the caller. Approval creates a grant bound to the exact digest, the target revisions, the risk class and an expiry, and it is re-checked inside the commit transaction. Every executed operation produces a receipt and a compensating undo, visible on the /changes screen. A generated capability matrix classifies every mutating route, and CI fails if a new one isn't classified.
The same rails power an MCP endpoint with its own OAuth 2 authorisation server and eight scopes, so you can connect an AI client of your choice. It can only propose, through the same approvals.
Tenant isolation, tested against a real database
Every table has row-level security switched on and forced. The app connects as a non-owner role that cannot bypass it. Each request runs inside withUserTransaction, which sets the user id with SET LOCAL, so a query with no user context returns nothing rather than everything. This works with transaction-pooled connections. More than a thousand isolation tests run against real Postgres, and a deploy check refuses to proceed unless RLS is forced on every table. The schema doesn't depend on any one auth provider's tables, which keeps it portable.
Faith features, done carefully
For people who want them, Lifekeep anchors the day around prayer and supports Hijri recurrence, Ramadan mode and Qur'an memorisation plans. Hijri dates come from the ICU islamic-umalqura calendar via Intl, pinned to UTC and checked against published anchors. Local moon-sighting adjustments are themselves effective-dated, so past days never re-render. Prayer times expose method, madhhab, high-latitude rule, offsets and manual overrides, and are presented as calculations rather than as authoritative. Hijri and civil dates are only allowed to meet inside one module. All of it is opt-in and never inferred.
Stack and testing
| Layer | What we use |
|---|---|
| Web | Next.js 16, React 19, Tailwind 4, shadcn/Radix, TanStack Query, React Hook Form, Zod 4, Recharts |
| Data | Postgres 17 with Drizzle ORM, UUIDv7 keys, btree_gist and pgvector |
| Auth | Magic link and Google sign-in, cookie or bearer token behind one credential chokepoint |
| Jobs | An HTTP job registry (plan-digest, reminders, scheduled actions) that any scheduler can drive |
| Native | SwiftUI for iPhone and Apple Watch, Kotlin for Android; the server owns the domain logic |
| Tests | Vitest (unit and isolation against Postgres), Playwright with axe accessibility checks, migrations verified both fresh and incremental |
What's next
Lifekeep is in pre-launch on the web. The native iPhone, Watch and Android apps run as working builds but aren't released yet. Next up: a public deployment, actionable push reminders (today there's an email digest), wider reach for the conversational assistant (only part of the operation language is reachable by typing a sentence so far), and spreadsheet import with history, which we won't claim until it ships. Goal Journeys forecast your goals and also grade their own past forecasts. The early grades were humbling, which is why we show them.
A planned integration with Chamber would let Chamber own the recall side of memorisation. See how the products fit together.