Persistence
Bloom stores each project's run state (the workflow position + shared state) keyed by
conversation thread, behind a StateStore interface. Two backends ship:
| Backend | Config | Use |
|---|---|---|
| In-memory (default) | BLOOM_STORE_BACKEND=memory | Dev/tests. State is lost on restart. |
| Supabase Postgres | BLOOM_STORE_BACKEND=supabase | Durable. Survives restarts/redeploys. |
The Supabase backend connects directly to the project's Postgres with asyncpg and stores one
JSONB row per thread in a bloom_runs table. Supabase is Postgres, so a direct pooled
connection is the lightest durable option for this key-value shape - no extra SDK, real
upserts, a real connection pool.
Setup
- Get the connection string. Supabase Dashboard -> Project Settings -> Database ->
Connection string. Prefer the pooler host (Supavisor). Append
?sslmode=requireif your network requires it. It looks like:postgresql://postgres.<ref>:<password>@aws-0-<region>.pooler.supabase.com:6543/postgres - Configure Bloom (in
.env):BLOOM_STORE_BACKEND=supabaseBLOOM_SUPABASE_DB_URL=postgresql://postgres.<ref>:<password>@...pooler.supabase.com:6543/postgres - Start Bloom. The
bloom_runstable is created automatically on startup (idempotentCREATE TABLE IF NOT EXISTS) - no manual migration needed. The equivalent SQL, if you prefer to run it yourself in the Supabase SQL editor:CREATE TABLE IF NOT EXISTS bloom_runs (thread_id text PRIMARY KEY,state jsonb NOT NULL,updated_at timestamptz NOT NULL DEFAULT now());
Outbound message ledger
Every outbound message (Telegram today; any future channel plugs in at the same seam) is also
persisted to a bloom_outbound_messages table (#216): thread id, channel, target, kind
(narration / question / review / error), delivery status, timestamp, and a truncated preview
plus a SHA-256 of the full content - so "did Bloom actually say X, where, and when" is
answerable from the server. Each send additionally emits a structured info-level
outbound.message.sent log line (or a warning-level outbound.message.failed). The table is
created on startup like the others; the in-memory ledger backs dev/tests. Query it via
OutboundMessageLedger.list_for_thread / list_recent, or SQL on bloom_outbound_messages.
Notes
- Transaction pooler compatibility. The store sets
statement_cache_size=0so it works with Supabase's transaction pooler (pgbouncer), which does not support prepared statements. - Security. The connection authenticates as a Postgres role, so row-level security does not
apply. Keep
BLOOM_SUPABASE_DB_URLsecret and server-side only (it is gitignored via.env). - Verify the store contract. The store's integration suite runs offline against an
in-process Postgres (PGlite):
apps/server/src/persistence/supabaseStore.integration.test.ts, part of the regularpnpm run test. (The Python-era opt-in live round-trip test retired withapps/api; it runs at gitd69f789.) - Swapping backends is a config change only; the orchestrator depends on
StateStore, not a concrete store.