The agent is storage-agnostic. Every read/write goes through one injected object — the StorageAdapter (adapter/storage.d.ts). The pure cores never touch storage; the backend shell hands them an adapter. Swap the adapter and you swap the database — the cores are unchanged.
Do I need a database?
No. For a small deployment, back the agent with the bundled in-memory store:
import { makeMemoryStore } from 'seo-agent/adapter/storage.memory.mjs'
const store = makeMemoryStore() // zero config, no database
// … run the agent loop against `store` …
const snapshot = store.snapshot() // persist to a JSON file if you want durability
// next boot: makeMemoryStore(JSON.parse(fs.readFileSync('seo-state.json')))snapshot() returns a plain object you can JSON.stringify to a file; pass it back as the seed to restore. That gives file-backed durability with no database server. Use this for a single-site deployment, local development, tests, or the runnable example.
Backing it with a real database
Implement the StorageAdapter interface against your store. Reference implementations:
| Backend | How |
|---|---|
| Supabase (reference) | The methods map 1:1 to the SQL + RPCs shipped in db/schema*.sql. This is the fully-featured reference host (Bea's Stories). |
| Postgres / MySQL / SQLite elsewhere | Same tables (db/schema*.sql is portable SQL); implement the adapter with your driver. |
| Document / NoSQL (Mongo, DynamoDB, Firestore…) | Store each collection as documents; the interface is collection-oriented already (proposals, jobs, rank rows, …). No joins are required by the core loop. |
| In-memory / file | adapter/storage.memory.mjs — the bundled zero-DB store. |
Only the methods the core loop needs are required; content (E3), self-heal revert (E4), and competitor-matrix (E8) methods are additive — omit them and those features simply produce nothing.
Security — the one thing you must not skip
The Supabase reference enforces access with Row-Level Security + SECURITY DEFINER RPCs. Those do not exist outside Postgres. When you use any other backend, *authorization is the adapter's job*:
1. Admin-only agent state. Settings, proposals, jobs, rank history, budget, apply-log, notifications — only an authenticated admin context may read or write them. The in-memory store performs no authz: whoever holds the object has full access. Gate it in the surface that constructs it. 2. Public serving reads are narrow. The only things a public/anonymous caller may read are published guides and approved page-meta, and only their public columns — never drafts, briefs, internal scores, or budget. In Supabase these are SECURITY DEFINER RPCs (get_published_guide, get_page_seo_meta); on another backend, expose equivalently narrow, read-only, published-only endpoints.
A storage adapter with no authorization is an open database. Treat the RLS the SQL reference provides as a *specification* your adapter must reproduce.