The problem: your chatbot keeps notes about each user. In September Sam said "ship to Boston." Last week Sam moved to Denver. Store both notes in a table, search them by meaning, and the old one can win: "Sam lives in Boston and wants orders shipped there" sounds more like a shipping question than "Sam moved to Denver." The package goes to the wrong city.
What you want is a store that knows a newer fact replaces an older one, keeps the history, and gives you one answer you can act on. In SynapCores that's a MEMORY. This recipe has Claude Code build the module for you.
We build SynapCores. Everything here runs on the free Community Edition.
What you need
- SynapCores Community Edition 2.4.8 or later, running locally (by default on port 8080). Download it.
- Claude Code, opened in an empty folder (or in your app's repo).
- An API key. Log in to get a session token, then create a key with it:
# 1. Log in. Copy access_token from the response.
curl -s -X POST http://localhost:8080/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"username": "admin", "password": "<your admin password>"}'
# 2. Create a key. It's in raw_key, and it's shown once.
curl -s -X POST http://localhost:8080/v1/api-keys \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{"name": "claude-code", "permission": "FullAccess"}'
- Two environment variables in the shell you start Claude Code from:
export SYNAPCORES_URL=http://localhost:8080
export SYNAPCORES_API_KEY=... # the raw_key from step 2
The prompt tells Claude Code to read the key from the environment and never print it.
Step 1: Paste this prompt into Claude Code
Add long-term memory to this app using SynapCores, an AI-native database with a
built-in MEMORY store. Use the MEMORY statements below; don't design your own
memory table.
Connection: SynapCores is running at $SYNAPCORES_URL. Send each SQL statement as
POST $SYNAPCORES_URL/v1/query/execute
Headers: Authorization: Bearer $SYNAPCORES_API_KEY, Content-Type: application/json
Body: {"database": "app", "sql": "<one statement>"}
Results come back in data.columns (a list of {"name", "data_type"}) and data.rows
(a list of rows, each a list of values in column order). Create the database
first with CREATE DATABASE IF NOT EXISTS app (send that one with "database": "main").
MEMORY syntax:
- Once: CREATE MEMORY app_memory IDENTITY user_id WITH (llm_extraction = false)
- Store: REMEMBER app_memory FOR user_id = <id> '<what the user said>'
WITH (source_type = 'verified_user_statement',
attribute = '<snake_case_key>', value = '<value>')
- One value: CURRENT app_memory FOR user_id = <id> ATTRIBUTE <key>
- Question: RECALL app_memory FOR user_id = <id> ABOUT '<question>'
Each MEMORY statement returns one row with a JSON string in the "envelope" column.
Parse it. In RECALL, data.current_state is what's true now, data.temporal is what
changed, and data.episodic holds the original sentences. A newer REMEMBER with the
same attribute replaces the old value and keeps it as history.
Build:
1. memory.py (Python 3, standard library only) with remember(user_id, text,
attribute, value), current(user_id, attribute) and recall(user_id, question).
Create app_memory on first use if it doesn't exist. Escape single quotes in
text by doubling them. Read the URL and key from the environment and never
print the key.
2. demo.py that:
a. remembers for user 42, in this order:
"Sam lives in Boston and wants orders shipped there." (ship_city = Boston)
"Sam is allergic to wool, so never suggest wool sweaters." (allergy = wool)
"Sam moved to Denver last week; ship everything to Denver from now on." (ship_city = Denver)
b. prints current(42, "ship_city")
c. prints the current_state and temporal parts of recall(42, "where do we send the package")
d. prints current(7, "ship_city") for user 7, who has no memories
Run demo.py and show me the output.
The prompt carries the syntax, so Claude Code doesn't have to guess it from older docs.
Step 2: What Claude Code builds
Two files: memory.py and demo.py. In our run on 2.4.8, Claude Code wrote a small _execute() helper that posts one statement to /v1/query/execute, an _envelope() helper that parses the JSON in the envelope column, and then the three functions. Here they are as the agent wrote them (your run may differ in the details):
def remember(user_id, text, attribute, value):
_ensure_memory()
sql = (
"REMEMBER {name} FOR user_id = {uid} '{text}' "
"WITH (source_type = 'verified_user_statement', "
"attribute = '{attr}', value = '{val}')"
).format(
name=_MEMORY_NAME,
uid=user_id,
text=_escape(text),
attr=_escape(attribute),
val=_escape(value),
)
return _envelope(_execute(sql))
def current(user_id, attribute):
_ensure_memory()
sql = "CURRENT {} FOR user_id = {} ATTRIBUTE {}".format(
_MEMORY_NAME, user_id, attribute
)
return _envelope(_execute(sql))
def recall(user_id, question):
_ensure_memory()
sql = "RECALL {} FOR user_id = {} ABOUT '{}'".format(
_MEMORY_NAME, user_id, _escape(question)
)
return _envelope(_execute(sql))
The three functions are thin. Each one sends a single statement. All the logic that matters (which fact is current, what it replaced, which user it belongs to) runs inside the database.