Habits
10Track frequency, log days, surface streaks. The habit calendar lives in the same store every agent reads from.
saathi · सहायक · companion
Sathi keeps your habits, tasks, finance, goals, documents, memories, and skills under one MCP roof. Same store that powers your dashboard answers every Claude question your other agents ask — your reading list, your stash of decisions, your morning routine — without the agent ever having to guess.
today · 14 may
2 of 5 done
Good morning. Three things to look at, two already moving.
Morning brief
Workout · push day
Sankalp · review draft queue
Read · 30 min
Evening reflection
All five rows live in the same Supabase project as your habits, goals, and memories. The 21:30 reflection writes back into the memory vault automatically — no copy-paste between apps.
Eight pillars
Each pillar is its own typed tool group inside the same MCP server — same auth, same RLS, same Supabase project. No bespoke ETL between life areas.
Habits
10Track frequency, log days, surface streaks. The habit calendar lives in the same store every agent reads from.
Tasks
17Subtasks, projects, real-time tracking, dependency-aware "what next" — and an auto-link memory pipeline so the agent never asks twice.
Goals
9Outcome goals auto-track against habits + tasks + finance. Milestone goals stay manual. Reviews aggregate across pillars.
Finance
11Spending categories, transaction list, summaries by date range — Claude can pull a weekly money read in one tool call.
Memory
9pgvector + tsvector hybrid search. Near-duplicate detection at save time. Stale hints for low-importance items.
Documents
7Signed-URL upload, chunked extraction, semantic search across the wallet. Bills, IDs, receipts — agent-reachable.
Skills
5Platform-aware skill registry. Bidirectional sync with ~/.claude/skills via a hand-written meta-skill.
Graph
4Soft links between any two entities. Cross-pillar context: a task can reference a memory can reference a goal.
How it's built
Each is in production traffic. Each has a debug session behind it — the kind of detail recruiters skim past on a resume.
Decision 01
A persistent MCP server inside a Vercel function would never survive cold-start budgets, and a singleton would fork state when two instances spun up concurrently. Sathi needed an MCP shape that didn't fight the runtime.
Every POST /api/mcp call constructs a fresh MCP server with the requesting user's scope baked in. No singleton, no cross-request leakage.
Bearer token verifier dispatches by prefix (sha-256 lookup for stored tokens, jose verify for OAuth access tokens, raw JWT fallthrough for Supabase sessions).
GET /api/mcp returns 405 — MCP Streamable HTTP clients were treating a 200 JSON response as a dropped SSE stream and reconnecting at 2 Hz (99k edge requests in 12 hours on 2026-04-20). POST-only.
// app/api/mcp/route.ts
export const runtime = 'nodejs'
export async function POST(req: Request) {
const user = await verifyBearer(req)
const server = createMcpServer({ userId: user.id })
return await streamableHttp(server).handle(req)
}
export const GET = () => new Response(null, { status: 405 })
export const HEAD = GETDecision 02
Pure cosine-similarity over text-embedding-3-small misses exact-keyword matches (project names, person names, dates). Pure tsvector misses semantic adjacency. The memory vault needed both at once.
pa_memory_items carries both an embedding (vector 1536) and a search_vector (tsvector, maintained by trigger).
pa_hybrid_search RPC scores each candidate by weighted sum of semantic_score + keyword_score, returns top-K with both scores attached for tunable reranking.
Save-time duplicate detection runs pa_match_memories at threshold 0.9 and returns "duplicates_found" instead of inserting — agents stop saving the same fact thirty times across sessions.
create function pa_hybrid_search(
user_id uuid, query text, query_embedding vector,
category text default null, project text default null,
match_count int default 10
) returns table (...) as $$
select id, title, content,
1 - (embedding <=> query_embedding) as semantic_score,
ts_rank_cd(search_vector, websearch_to_tsquery(query)) as keyword_score,
/* weighted blend */
(1 - (embedding <=> query_embedding)) * 0.7
+ ts_rank_cd(search_vector, websearch_to_tsquery(query)) * 0.3 as final_score
from pa_memory_items
where user_id = $1 and is_active = true ...
$$ language sql stable;Decision 03
Tasks reference memories, memories reference goals, goals depend on habits. Hard-FK relationships across that graph would lock the schema and make a single soft-delete cascade everything.
pa_entity_links carries (from_type, from_id, to_type, to_id, link_type, notes) — no FK to source tables. Soft links survive deletes.
link_type ∈ {related_to, references, blocks, part_of}. blocks links power get_task_dependencies + get_next_tasks (ready-to-start computation).
Every mutating MCP call appends a row to pa_audit_log with changed_fields JSONB. The link history is auditable years after the source row is gone.
-- the entire graph in one table
create table pa_entity_links (
id uuid primary key,
user_id uuid references auth.users,
from_type text not null, from_id uuid not null,
to_type text not null, to_id uuid not null,
link_type text default 'related_to',
notes text, active bool default true,
unique (user_id, from_type, from_id,
to_type, to_id, link_type)
);Open to work
Sathi is one of seven dogfooded apps I ship in parallel. Same Supabase project, same MCP fabric, same Claude Code stack. Happy to walk a hiring panel through any layer live — the hybrid-search RPC, the audit log, the OAuth broker, or the cross-pillar link graph.