Build Your Own Backend¶
Adding a storage backend is intentionally small: implement the SessionStorage interface (5 methods) and wrap it in a KeyValueSessionManager. Everything else — the full Strands SessionRepository, message indexing, restore, pagination, offsetting, tool-use repair — comes from the core.
The mental model¶
Your backend never imports Strands types and never parses payloads. It's a plain ordered keyed-record store that moves opaque dicts:
KeyValueSessionManager (core: knows Strands, does all serialization)
│ passes/receives records: {"pk": str, "sk": str, "data": str}
▼
SessionStorage (you: just persist and return those records)
▼
your database
pk/skare opaque strings — treat them as identifiers; never parse them.datais an opaque serialized string — store and return it verbatim; never inspect or modify it.
That's the whole reason a backend is ~30 lines: all the Strands knowledge lives in the core.
The record¶
Every method deals in a single record shape:
The contract¶
Implement these five methods. Semantics matter — the core relies on them:
| Method | Signature | Must do | Returns |
|---|---|---|---|
put |
put(item: dict) -> None |
Upsert the record by (pk, sk) — insert or overwrite |
None |
get |
get(pk: str, sk: str) -> dict \| None |
Fetch one record | The record dict, or None if absent |
query |
query(pk: str, sk_gte: str \| None = None, limit: int \| None = None) -> list[dict] |
Records under pk, ordered by sk ascending; if sk_gte given, only sk >= sk_gte (inclusive); if limit given, at most that many |
List of record dicts ([] if none) |
delete |
delete(pk: str, sk: str) -> None |
Delete one record if present — idempotent (no error if missing) | None |
delete_partition |
delete_partition(pk: str) -> None |
Delete all records under pk — idempotent |
None |
Ordering is load-bearing
query must return records sorted by sk ascending. Message sort keys are zero-padded (MSG#00000000000000000007) so lexical order equals numeric order — this is what makes list_messages(offset, limit) correct. If your store sorts differently, sort explicitly.
Notes:
- get/query return records in the same {pk, sk, data} shape.
- put is an upsert, not insert — the core calls it for both creates and updates (e.g. message redaction).
- Duplicate-session detection, "does this agent exist", timestamp preservation, etc. are handled by the core on top of these primitives — you don't implement them.
Worked example — a SQLite backend¶
A complete, runnable backend in ~30 lines using Python's built-in sqlite3:
import sqlite3
from strands_agents_session import KeyValueSessionManager, SessionStorage
class SQLiteSessionStorage(SessionStorage):
def __init__(self, path: str = "sessions.db"):
self.conn = sqlite3.connect(path)
self.conn.execute(
"CREATE TABLE IF NOT EXISTS sessions ("
" pk TEXT NOT NULL, sk TEXT NOT NULL, data TEXT NOT NULL,"
" PRIMARY KEY (pk, sk))"
)
self.conn.commit()
def put(self, item):
self.conn.execute(
"INSERT INTO sessions (pk, sk, data) VALUES (?, ?, ?) "
"ON CONFLICT(pk, sk) DO UPDATE SET data = excluded.data",
(item["pk"], item["sk"], item["data"]),
)
self.conn.commit()
def get(self, pk, sk):
row = self.conn.execute(
"SELECT data FROM sessions WHERE pk = ? AND sk = ?", (pk, sk)
).fetchone()
return {"pk": pk, "sk": sk, "data": row[0]} if row else None
def query(self, pk, sk_gte=None, limit=None):
sql = "SELECT sk, data FROM sessions WHERE pk = ?"
params = [pk]
if sk_gte is not None:
sql += " AND sk >= ?"
params.append(sk_gte)
sql += " ORDER BY sk ASC"
if limit is not None:
sql += " LIMIT ?"
params.append(limit)
return [{"pk": pk, "sk": sk, "data": data}
for sk, data in self.conn.execute(sql, params).fetchall()]
def delete(self, pk, sk):
self.conn.execute("DELETE FROM sessions WHERE pk = ? AND sk = ?", (pk, sk))
self.conn.commit()
def delete_partition(self, pk):
self.conn.execute("DELETE FROM sessions WHERE pk = ?", (pk,))
self.conn.commit()
class SQLiteSessionManager(KeyValueSessionManager):
def __init__(self, session_id, path="sessions.db"):
super().__init__(session_id=session_id, storage=SQLiteSessionStorage(path))
Use it exactly like any provider:
from strands import Agent
agent = Agent(session_manager=SQLiteSessionManager(
session_id="user-123", path="sessions.db",
))
agent("Hi, I'm Kamal")
This is essentially the SQL provider
The official strands-session-sql provider is this idea generalized over SQLAlchemy so one backend covers SQLite, Postgres, and MySQL. If you only need SQLite, the snippet above is all it takes.
Backend responsibilities (not the core's)¶
The core stays storage-neutral, so these are yours to decide per backend:
- Indexing — ensure
(pk, sk)lookups and orderedskrange scans are efficient (a composite index / primary key). - TTL / expiry — if you want automatic session expiry, add it in your storage (e.g. a TTL index / column). It's optional.
- Concurrency / connections — connection pooling and thread-safety are the backend's concern.
- Serialization — none. The core serializes to the
datastring; you store it as-is.
Testing your backend¶
Point the existing test shape at your manager — build a session, create agents/messages, and assert round-trips, ordering, offset/limit, redaction, and isolation. For a zero-dependency reference, the core's InMemorySessionStorage and its test suite show the exact expectations every backend must satisfy.