Skip to main content

What It Is

The storage layer is the set of repositories that give every durable Parmana concept (a business transaction, an execution trust record, a refusal record, a pending policy change, a policy approval record, and, as of 2026-09-16, live policy content itself) a place to live beyond a single process’s memory. It is accessed through one interface, StorageProvider (packages/storage/src/StorageProvider.ts), with two real implementations selected at startup: an in-memory one for tests and local development, and a Postgres-backed one for anything real.

Why It Was Built

Parmana’s whole value proposition depends on durable, independently verifiable evidence. A signed execution trust record that only exists in process memory and disappears on restart proves nothing to anyone after the fact. The storage layer exists to make every artifact this system produces outlive the process that produced it, without forcing every package that needs to persist something to independently decide how.

How It Works

The interface

StorageProvider exposes five repositories:
(packages/storage/src/StorageProvider.ts:15-42)

Two implementations, chosen by StorageFactory

StorageFactory.create({ provider }) (packages/storage/src/StorageFactory.ts) switches on the configured provider: StorageFactory.createFromEnvironment() is the real entry point every caller actually uses. It has one hardcoded override: under NODE_ENV=test, it always returns MemoryStorageProvider, regardless of what PARMANA_STORAGE is set to. This exists because of a real incident (G-15, docs/VERIFICATION-GAPS.md): this method used to construct whatever PARMANA_STORAGE named as a pure import-time side effect, which crashed test collection with supabase-js’s generic "supabaseUrl is required." the moment SUPABASE_* was unset, regardless of what any individual test actually needed.

Lazy construction, everywhere that matters

packages/api/src/repositories.ts wraps every repository export in a Proxy via a local lazyRepository() helper, so importing repositories.ts (which every route file transitively does) never itself constructs a live Supabase client. Only the first actual method call on a repository triggers StorageFactory.createFromEnvironment(). This exact discipline was violated, then fixed, in this codebase on 2026-09-16. The first version of packages/api/src/application.ts’s new policyRepository export constructed SupabasePolicyRepository(PostgresPoolFactory.create()) directly at module scope:
Because PostgresPoolFactory.create() is a process-wide singleton (see below), this opened a real connection against whatever DATABASE_URL happened to be set at the moment application.ts was first imported, before any caller had a chance to configure it differently. This broke examples/tutorials/89-readiness-probe/run.ts’s own “genuinely unreachable database” scenario: the tutorial tries to swap in a bad DATABASE_URL for one specific test case, but the pool had already been created against the real, working one by the time that scenario ran, so the readiness check kept reporting READY when it should have reported NOT_READY. The fix (packages/api/src/application.ts, current version) wraps policyRepository in the identical Proxy-based lazy pattern repositories.ts already uses, deferring construction to the first real load()/save()/listAll() call.

Direct Postgres, not PostgREST

PostgresPoolFactory (packages/storage/src/postgres/PostgresPoolFactory.ts) is a lazy, process-wide singleton pg.Pool. Every Supabase-backed repository in this codebase connects through it directly, not through supabase-js’s REST-based client (PostgREST). The class that used to provide that client, SupabaseClientFactory, was deleted 2026-09-09 (docs/VERIFICATION-GAPS.md G-34) after PostgresPoolFactory’s own pattern had already replaced it everywhere it was still used. This removes an entire layer (PostgREST) from the failure modes of every table these repositories touch, an incident class this codebase’s own history names explicitly (see SupabaseCallerAuditSink’s own comments for the originating case). As of 2026-09-16, PostgresPoolFactory.create()’s Pool is constructed with connectionTimeoutMillis: 5000. Before that, an unreachable or misconfigured DATABASE_URL hung a connecting query indefinitely rather than failing fast, found via the same readiness-probe tutorial above, whose “genuinely unreachable database” scenario never returned before this was added.

The rate-limit store, a Postgres-backed class with no repository interface

PostgresRateLimitStore (packages/storage/src/postgres/PostgresRateLimitStore.ts) is not part of StorageProvider, it implements express-rate-limit’s own Store contract directly (structurally, via a locally-defined RateLimitStoreLike interface, so @parmana/storage never depends on the express-rate-limit package itself). It exists to close a fleet-wide gap: express-rate-limit’s default MemoryStore counts per-process, so on a horizontally-scaled deployment the effective ceiling for a caller becomes limitPerMinute * machineCount rather than the configured limit. See Chapter 16 for the full rate-limiting story, including a real ERR_ERL_STORE_REUSE bug this class’s prefix field exists to resolve.

An orphaned repository, honestly

packages/storage/src/memory/MemoryPolicyRepository.ts exists, is exported from @parmana/storage’s public index, and implements PolicyRepository. As of this writing, it is not referenced anywhere in MemoryStorageProvider, StorageProvider, or application.ts, the actual in-memory policy path uses FilePolicyRepository against a scratch or configured directory instead. This is the same category of finding the existing docs/book/ chapter 16 already documents for @parmana/receipt/@parmana/replay: real, presumably tested code that is not part of the live wiring. Confirm this yourself before relying on it (see “How to Validate” below), it may simply be unused, or it may be a recent addition not yet wired in.

The schema itself

supabase/migrations/*.sql is the real source of truth for every table these repositories read and write. As one directly-verified example, the policies table added 2026-09-16 (supabase/migrations/20260916060000_add_policies_table.sql) is a two-column-key table:
Note this table is read and written by SupabasePolicyRepository (packages/policy/src/SupabasePolicyRepository.ts), which lives in the @parmana/policy package, not @parmana/storage, it is constructed directly in application.ts, not exposed through StorageProvider. This is a real architectural asymmetry: every other Postgres-backed repository in this codebase goes through the StorageProvider facade; policy content does not. Neither the code nor any comment currently explains why this one repository sits outside the facade rather than being added to it as a sixth field, treat this as an open question rather than an assumed design decision. docs/architecture/DATABASE_SCHEMA_REFERENCE.md is a detailed, table-by-table reference covering every table in this schema; this chapter verified one table directly against the migration SQL rather than only citing that document, and recommends the same discipline for any other table you need to rely on.

How It Enables Things, With a Concrete Example

examples/tutorials/116-supabase-policy-repository/run.ts exercises SupabasePolicyRepository directly against a minimal fake pg.Pool (no real network), proving save() then load() round-trips exactly, load() on a missing entry throws PolicyNotFoundError, and listAll() reports what was saved, the same contract FilePolicyRepository upholds, just backed by a table instead of the filesystem. examples/tutorials/115-per-limiter-rate-limit-stores/run.ts exercises PostgresRateLimitStore against a similar fake pg.Pool, proving the real ERR_ERL_STORE_REUSE failure mode and its fix (Chapter 16 covers this fully).

How to Validate This Yourself

  • packages/storage/src/StorageProvider.ts, StorageFactory.ts, the interface and the selection logic.
  • packages/storage/src/memory/MemoryStorageProvider.ts, packages/storage/src/supabase/SupabaseStorageProvider.ts , the two real implementations.
  • packages/storage/src/postgres/PostgresPoolFactory.ts, the shared connection pool, its own comments name the real incidents that shaped it.
  • packages/api/src/repositories.ts and packages/api/src/application.ts, the lazy construction pattern, and the place it was once violated.
  • supabase/migrations/*.sql, the real, current schema. Read the actual files; a reference document can drift, the migration files cannot (they are the thing that ran).
  • packages/storage/tests/unit/postgres-pool-factory.test.ts, confirms the exact Pool constructor arguments, including connectionTimeoutMillis.
  • Search the codebase yourself for MemoryPolicyRepository usage before trusting this chapter’s claim that it is orphaned; that kind of claim is exactly the kind that goes stale fastest.

Integration Requirements