memory
Memories
Save facts, decisions, and preferences once. Every agent on every machine queries the same store via pgvector semantic search.
text-embedding-3-small · 1536d · HNSW · cosine
a multi-tenant memory layer for every ai agent
Cursor, Claude Code, and every other agent forget what you told them last session. Project Memory gives them a shared, persistent backbone — memories, tasks, and code — through one remote MCP, OAuth 2.1, and pgvector semantic search. Add the URL once. Every agent reads the same store.
Three drawers
Same Postgres project, same RLS policy, same auth surface. Different access patterns for different data.
memory
Save facts, decisions, and preferences once. Every agent on every machine queries the same store via pgvector semantic search.
text-embedding-3-small · 1536d · HNSW · cosine
task
A shared task board your agents read and write. Plan in Cursor, execute in Claude Code, status updates travel across.
soft-delete · audit-trail · per-project RLS
code
An Electron daemon watches your repos, chunks files with a 500-token sliding window, and syncs to Postgres. Cross-machine grep without re-indexing.
500-token windows · 6-line overlap · 10-pattern scrubber
The archive
Every row is yours — RLS-bound, audit-stamped, retrievable in 40 ms via HNSW. Cursor pulls a card. Claude Code drops a card back in. The drawer doesn't know who came in.
Install
Cursor and Claude Code self-register the moment they hit the MCP URL. You approve the consent screen once; the agent stores a refresh token and rotates it on its own thereafter.
Sign in
Drop this into ~/.cursor/mcp.json
{
"mcpServers": {
"project-memory": {
"url": "https://pm.devfrend.com/api/mcp"
}
}
}Approve in browser
How it's built
Each in production. Each has a debug-session story behind it — the kind of detail recruiters skim past on a resume.
Case 01
Cursor and Claude Code arrive with no token. Both expect to self-register, mint scoped credentials, and rotate them — without paying Auth0 or Clerk.
PKCE S256 + Dynamic Client Registration + RFC 8707 audience-bound access tokens, implemented straight into /oauth/* route handlers.
Refresh tokens carry a rotation chain (rotated_to column). A replayed revoked token will revoke the entire descendant tree — RFC 6749 §10.4.
Short-lived (5 minute) HS256 JWTs propagate user_id + an app_actor claim into Postgres. The audit-log trigger reads app_actor via current_setting and stamps every write with its origin surface.
// app/oauth/token/route.ts
export const runtime = 'nodejs'
const { row, replayed } = await findRefreshToken(input.token)
if (replayed) await revokeChainFrom(row) // RFC 6749 §10.4
const newRefresh = await rotateRefreshToken(row)
return issueShortJwt(row.user_id, 'mcp')Case 02
Semantic search has to feel instant from a tool call but stay accurate enough to compete with a curated knowledge base.
Embeddings live as vector(1536). HNSW index with m=16 / ef_construction=64 at build time; ef_search=100 per RPC for recall.
Audit trigger redacts the embedding column from to_jsonb(new) — without that one fix, a bulk re-embed doubled the audit table by hundreds of MB.
Idempotency: server hashes the call payload into client_payload_hash; replays return the same row, hash mismatch returns idempotency_conflict.
create index pmd_memories_embedding_hnsw
on pmd_memories
using hnsw (embedding vector_cosine_ops)
with (m = 16, ef_construction = 64);
-- per-call: set local hnsw.ef_search = 100;Case 03
External agents arrive with three flavors of credential — long-lived tokens, OAuth access tokens, and raw Supabase session JWTs. The MCP transport has to honor all three and still bind RLS correctly.
pm_* — long-lived tokens. SHA-256 lookup in pmd_long_lived_tokens, mint a fresh 5-minute JWT, attach app_actor='mcp'.
oat_* — OAuth access tokens. Audience-bound (RFC 8707) per /api/mcp, refresh-rotated.
Anything else — fall through to a raw Supabase session JWT. The transport pipes it straight into supabase-js so RLS authors the row.
// lib/auth/dispatch.ts
const bearer = req.headers.get('authorization')?.replace(/^Bearer /, '')
if (bearer?.startsWith('pm_')) return verifyLongLived(bearer)
if (bearer?.startsWith('oat_')) return verifyOAuthAccessToken(bearer)
return verifySupabaseSession(bearer) // raw JWT fallthroughCase 04
Watching dozens of repos and re-embedding on every keystroke would burn an OpenAI budget and DDoS the dedup index.
Electron sync daemon chunks files at 500 tokens with a 6-line overlap, then dedups on (path, start_line, end_line, sha256(content)) before queuing for embed.
A 10-pattern scrubber strips OpenAI keys, AWS keys, Stripe secrets, and private-key headers BEFORE the chunk ever reaches the API.
Soft-delete on rename — the old row is marked deleted_at, the new row gets the new path, the embedding cache stays warm across rebases.
// chunker — line-aligned, no token-budget overshoot
for (const block of slidingWindow(file, 500, 6)) {
const text = scrub(block.text) // 10 redaction patterns
const dedupKey = sha256(text)
if (await alreadyEmbedded(dedupKey)) continue
await enqueueEmbed({ path, ...block, text })
}Open to work
Project Memory 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 — OAuth broker, RLS policy, pgvector tuning, or the Electron sync daemon.