Storage

Whether you need a database, how to back it with one, and the security step you must not skip.

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:

BackendHow
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 elsewhereSame 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 / fileadapter/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.