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.
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'));
EOFA 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;
}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}`));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.tsWith 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| Check | Command / Why it matters |
|---|---|
| Server starts without errors | Run npx ts-node src/server.ts and see the console log 🚀 server on http://localhost:3000 |
| POST stores a value | The curl command returns {\"status\":\"ok\"} |
| GET retrieves the same value | The second curl prints {\"key\":\"greeting\",\"value\":\"Hello world\"} |
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.