a multi-tenant memory layer for every ai agent

The memory your agents keep forgetting — kept on file, in order, under glass.

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.

14
MCP tools, one URL
1536d
pgvector embeddings
RFC 8707
audience-bound bearers
3
data types · memory · task · code

Bearer · oat_* · audience-bound (RFC 8707)

pmd · multi-tenant · RLS per row

  1. MEMORIES0001–0427
    427
    pmd:m:0042Twin chat handlers must stay in sync — sutra + setu-nextjs.
    pmd:m:0118Audit trigger redacts the embedding column from to_jsonb(new).
    pmd:m:0203RFC 8707 audience binding — tokens scoped per /api/mcp.

Three drawers

One store, three shapes of memory.

Same Postgres project, same RLS policy, same auth surface. Different access patterns for different data.

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

task

Tasks

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

Code chunks

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

One Postgres tenant. Three drawers. Fourteen tools. One Bearer header.

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.

Bearer · oat_* · audience-bound (RFC 8707)

pmd · multi-tenant · RLS per row

  1. MEMORIES0001–0427
    427
    pmd:m:0042Twin chat handlers must stay in sync — sutra + setu-nextjs.
    pmd:m:0118Audit trigger redacts the embedding column from to_jsonb(new).
    pmd:m:0203RFC 8707 audience binding — tokens scoped per /api/mcp.
    pmd:m:0309pg_cron rows are the source of truth for routine schedules.
    pmd:m:0412Soft-delete on rename keeps the embedding cache warm.
  2. TASKS0001–0118
    118
    pmd:t:0044Wire pm_oauth refresh-rotation revocation chain (RFC 6749 §10.4).
    pmd:t:0061Bump ef_search to 100 per RPC; benchmark recall vs 64.
    pmd:t:0079Add daily spend cap on /api/sync — OpenAI rate-limit gate.
    pmd:t:0094Switch /auth/confirm to safeNext() before redirect.
    pmd:t:0117Move /admin entry to rel="nofollow noindex" on touch.
  3. CODE0001–9821
    9,821
    pmd:c:1402sutra/src/main/mcp/sse-stream.ts · L5-58 · channel relay
    pmd:c:2811setu-nextjs/lib/auth/dispatch.ts · L9-21 · bearer prefix dispatch
    pmd:c:3947project-memory/lib/admin/oauth/access-tokens.ts · L99-119 · rotate
    pmd:c:5210project-memory/supabase/migrations/0002_memory.sql · L60-80 · RLS
    pmd:c:7388sutra/src/main/routines/dispatcher.ts · L42-71 · channel branch

pgvector · HNSW · m=16 · ef_construction=64 · ef_search=100 per RPC

OAuth 2.1PKCE S256Dynamic Client RegistrationRFC 8707 audience-boundpgvector · HNSWaudit_log on every write

Install

Sixty seconds. No token to copy.

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.

  1. Sign in

    Free account — /login. Email + password, no team plan to pick.
  2. Drop this into ~/.cursor/mcp.json

    {
      "mcpServers": {
        "project-memory": {
          "url": "https://pm.devfrend.com/api/mcp"
        }
      }
    }
  3. Approve in browser

    Cursor opens the consent screen on its first tool call. Click Approve. From then on the agent has access to your memories, tasks, and code.

How it's built

Four decisions that earned their keep.

Each in production. Each has a debug-session story behind it — the kind of detail recruiters skim past on a resume.

Case 01

OAuth 2.1 by the book — written as Next.js route handlers

Cursor and Claude Code arrive with no token. Both expect to self-register, mint scoped credentials, and rotate them — without paying Auth0 or Clerk.

RFC 7591 / 8707DCR + audience-bound
  1. PKCE S256 + Dynamic Client Registration + RFC 8707 audience-bound access tokens, implemented straight into /oauth/* route handlers.

  2. Refresh tokens carry a rotation chain (rotated_to column). A replayed revoked token will revoke the entire descendant tree — RFC 6749 §10.4.

  3. 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.

excerpt · 01.code

Case 02

pgvector with HNSW, tuned per RPC

Semantic search has to feel instant from a tool call but stay accurate enough to compete with a curated knowledge base.

1536dtext-embedding-3-small
  1. Embeddings live as vector(1536). HNSW index with m=16 / ef_construction=64 at build time; ef_search=100 per RPC for recall.

  2. 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.

  3. Idempotency: server hashes the call payload into client_payload_hash; replays return the same row, hash mismatch returns idempotency_conflict.

excerpt · 02.code

Case 03

Bearer prefix dispatches the verifier

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.

3 pathsone bearer header
  1. pm_* — long-lived tokens. SHA-256 lookup in pmd_long_lived_tokens, mint a fresh 5-minute JWT, attach app_actor='mcp'.

  2. oat_* — OAuth access tokens. Audience-bound (RFC 8707) per /api/mcp, refresh-rotated.

  3. Anything else — fall through to a raw Supabase session JWT. The transport pipes it straight into supabase-js so RLS authors the row.

excerpt · 03.code

Case 04

Code-sync that doesn't re-embed the world

Watching dozens of repos and re-embedding on every keystroke would burn an OpenAI budget and DDoS the dedup index.

10 patternssecret scrubber
  1. 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.

  2. A 10-pattern scrubber strips OpenAI keys, AWS keys, Stripe secrets, and private-key headers BEFORE the chunk ever reaches the API.

  3. 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.

excerpt · 04.code

Open to work

I designed the schema, wrote the OAuth, and shipped every layer — I can do the same for you.

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.

Senior / staff full-stackAI engineer · MCP & agent infraNext.js · TypeScript freelanceRAG + Postgres + pgvector