Persistent Context Store with Claude‑Mem (TypeScript)

A minimal clone you can spin up in 45 minutes using Node.js and SQLite

Welcome to another official UVF IT build guide! In this tutorial we’ll craft a tiny version of the Claude‑Mem persistent context store – a simple key/value memory that an AI agent can read and write to across runs.

You’ll use plain Node.js, TypeScript, and a local SQLite file, so no paid APIs or cloud services are required. By the end you’ll have a running HTTP server that accepts JSON {key,value} pairs and persists them for later queries.

Before you start…

Minimal Claude‑Mem Clone Architecture
LOCAL RUNTIME & DB HTTP requests reads/writes HTTP Client (curl or browser) Node.js Express Server TypeScript + SQLite SQLite File persisted memory
1

Phase 1: Scaffold the project

Create a fresh npm workspace, add TypeScript, Express, and SQLite packages, and initialise a TypeScript config.

mkdir claude-mem-clone && cd claude-mem-clone
npm init -y
npm install express sqlite3
npm install -D typescript @types/node @types/express ts-node-dev
npx tsc --init --rootDir src --outDir dist --esModuleInterop true --module commonjs --target es2022
mkdir src
cat > src/index.ts <<'EOF'
import express from 'express';
const app = express();
app.use(express.json());
app.listen(3000,()=>console.log('Server running on http://localhost:3000'));
EOF
2

Phase 2: Define the memory model

A tiny SQLite wrapper creates a mem table and provides set and get helpers.

src/db.ts

import sqlite3 from 'sqlite3';
import { open, Database } from 'sqlite';

let db: Database<sqlite3.Database, sqlite3.Statement>;

export async function initDb(path = 'mem.db') {
  db = await open({ filename: path, driver: sqlite3.Database });
  await db.exec('CREATE TABLE IF NOT EXISTS mem (key TEXT PRIMARY KEY, value TEXT)');
}

export async function setMem(key: string, value: string): Promise<void> {
  await db.run('INSERT OR REPLACE INTO mem (key, value) VALUES (?, ?)', key, value);
}

export async function getMem(key: string): Promise<string | null> {
  const row = await db.get<{value:string}>('SELECT value FROM mem WHERE key = ?', key);
  return row ? row.value : null;
}
3

Phase 3: Build the API

We’ll wire up an Express server that talks to the SQLite database you created earlier. The server will expose two tiny endpoints: a POST /mem that receives a JSON {key, value} and stores it, and a GET /mem/:key that reads the stored value back. Keeping the routes minimal makes the clone easy to understand and extend later, for example by adding expiration timestamps or vector embeddings.

We’ll also add a small helper to run the database queries asynchronously, so the API stays responsive. All the code lives in src/server.ts and can be run directly with ts-node without a separate build step.

src/server.ts

import express from 'express';
import bodyParser from 'body-parser';
import sqlite3 from 'sqlite3';
import { open } from 'sqlite';

// Async wrapper to get a DB connection
async function getDb() {
  return open({
    filename: './data/mem.db',
    driver: sqlite3.Database,
  });
}

const app = express();
app.use(bodyParser.json());

// POST /mem – store a key/value pair
app.post('/mem', async (req, res) => {
  const { key, value } = req.body;
  if (!key || value === undefined) {
    return res.status(400).json({ status: 'error', message: 'key and value required' });
  }
  const db = await getDb();
  await db.run('INSERT OR REPLACE INTO mem (key, value) VALUES (?, ?)', [key, value]);
  await db.close();
  res.json({ status: 'ok' });
});

// GET /mem/:key – retrieve a stored value
app.get('/mem/:key', async (req, res) => {
  const { key } = req.params;
  const db = await getDb();
  const row = await db.get('SELECT value FROM mem WHERE key = ?', [key]);
  await db.close();
  if (!row) {
    return res.status(404).json({ status: 'error', message: 'not found' });
  }
  res.json({ key, value: row.value });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`🚀 server on http://localhost:${PORT}`));
4

Phase 4: Run locally

Now that the API code is ready, you can launch it straight from the command line using ts-node. Because we’re using native ES modules and SQLite, there’s no need for a separate compilation step – ts-node will transpile on the fly. The server will listen on port 3000 by default, and you’ll see a friendly log confirming it’s up and running.

npx ts-node src/server.ts
5

Phase 5: Verify & extend

With the server running, a quick curl test proves the clone works: first store a value with POST /mem, then fetch it with GET /mem/:key. If the responses match the expectations, you have a functional persistent store you can call from any LLM‑agent. From here you might add fields like expires_at or a vector column to support richer memory semantics.

# Store a value
curl -X POST http://localhost:3000/mem \
  -H 'Content-Type: application/json' \
  -d '{"key":"greeting","value":"Hello world"}'

# Retrieve the same value
curl http://localhost:3000/mem/greeting

Verifying the Stack

CheckCommand / Why it matters
Server starts without errorsRun npx ts-node src/server.ts and see the console log 🚀 server on http://localhost:3000
POST stores a valueThe curl command returns {\"status\":\"ok\"}
GET retrieves the same valueThe second curl prints {\"key\":\"greeting\",\"value\":\"Hello world\"}

You did it!

Your tiny Claude‑Mem clone now persists context across requests. Plug it into any LLM‑agent that needs a simple key/value store, or evolve it with vector embeddings and multi‑tenant support later.