Lifekeep: history that can't be rewritten and a planner that explains itself

Inside Lifekeep's design: effective-dated versions enforced by Postgres, evidence-gated weekly adaptations, an explainable day planner that respects prayer times and your calendar, and an operation language that keeps every AI write approvable and undoable.

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 architectureWeb, native and external AI clients call a REST API and an MCP endpoint. Thin routes validate input and call use cases, which call a pure domain core containing the planner, adaptation signals, the operation language, habits and the Islamic calendar. Every database touch runs inside a per-user transaction against Postgres with forced row-level security. Language models, Google Calendar, email and a jobs scheduler sit outside.src/core · pure TypeScriptWeb appNext.js 16NativeiOS · AndroidAI clientsvia MCP + OAuth/api/v1 · /api/v1/mcpauthenticate → Zod parse → use case → serializeUse casessrc/application · one transaction per mutationdayflowthe planneradaptation9 signalslcapops · undohabitsversionsconsistency28-day windowcalendarsHijri · prayer timeswithUserTransactionPostgres 17 + DrizzleFORCE row-level security · btree_gist · pgvectorLLM providers10 · your keyCalendarGoogle · read-onlyJobsPOST /api/jobs/*
Layering is enforced by lint: the domain core imports no framework, database or model SDK. Tap to zoom.

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.

Effective-dated habit versionsA habit's history is a chain of versions, each covering a half-open date range. Editing the habit closes the open version and starts a new one from tomorrow, so past days still resolve to the version that applied then. A Postgres exclusion constraint makes overlapping ranges impossible.habit_versions for “Morning run”1 Jun14 Augtomorrowv1 · 30 min[1 Jun, 14 Aug)v2 · 40 min[14 Aug, tmrw)v3 · 50 min[tmrw, ∞)EXCLUDE USING gist(habit_id WITH =, daterange WITH &&)one open version · gaps reportedpast days render v1
Editing a habit never rewrites the past. The database itself refuses overlapping versions. Tap to zoom.

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.

AI proposes, deterministic code writesText from the person goes to a versioned prompt and a language model, which must return a typed proposal. Zod and the domain rules validate it, then it becomes a ChangeSet with an impact manifest and a risk class. The person approves with a grant bound to the exact digest, and execution produces a receipt and a compensating undo.“I ran 5.2km, skipped stretching”Versioned prompt + your modelday-log-v1 · untrusted text wrappedTyped proposalvalidated by Zod + domain rulesChangeSetimpact manifest · risk classApproval grantbound to digest, revisions, expiryExecute → receiptcompensating undo on /changesmodel'spart ends
The model only proposes. Validation, approval and the write itself are deterministic code. Tap to zoom.

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

LayerWhat we use
WebNext.js 16, React 19, Tailwind 4, shadcn/Radix, TanStack Query, React Hook Form, Zod 4, Recharts
DataPostgres 17 with Drizzle ORM, UUIDv7 keys, btree_gist and pgvector
AuthMagic link and Google sign-in, cookie or bearer token behind one credential chokepoint
JobsAn HTTP job registry (plan-digest, reminders, scheduled actions) that any scheduler can drive
NativeSwiftUI for iPhone and Apple Watch, Kotlin for Android; the server owns the domain logic
TestsVitest (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.

See Lifekeep All posts