Skip to main content

Persistence

Bloom stores each project's run state (the workflow position + shared state) keyed by conversation thread, behind a StateStore interface. Two backends ship:

BackendConfigUse
In-memory (default)BLOOM_STORE_BACKEND=memoryDev/tests. State is lost on restart.
Supabase PostgresBLOOM_STORE_BACKEND=supabaseDurable. 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

  1. Get the connection string. Supabase Dashboard -> Project Settings -> Database -> Connection string. Prefer the pooler host (Supavisor). Append ?sslmode=require if your network requires it. It looks like:
    postgresql://postgres.<ref>:<password>@aws-0-<region>.pooler.supabase.com:6543/postgres
  2. Configure Bloom (in .env):
    BLOOM_STORE_BACKEND=supabase
    BLOOM_SUPABASE_DB_URL=postgresql://postgres.<ref>:<password>@...pooler.supabase.com:6543/postgres
  3. Start Bloom. The bloom_runs table is created automatically on startup (idempotent CREATE 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=0 so 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_URL secret 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 regular pnpm run test. (The Python-era opt-in live round-trip test retired with apps/api; it runs at git d69f789.)
  • Swapping backends is a config change only; the orchestrator depends on StateStore, not a concrete store.