# Plan: Postgres store backend (phase 1) A Postgres implementation of the handler's DocumentStore interface, config-selected, with the file store staying the default. Branch: feat/postgres-store (stacked on refactor/documentstore-interface). ## Parity strategy: store the version-file bytes The file store's Document.Content is the raw stored bytes including the store frontmatter (version, archived, previous-hash, publisher meta); the handler strips it. The hash chain is computed over those raw bytes. So pgstore stores the byte-identical serialized blob the file store would write, in a `stored` bytea column. Consequences: - Get, Versions, VerifyChain, ErrNotModified dedup, and the previous-hash chain carry over exactly. - File to Postgres migration (either direction) is a blob copy. - The version-file format becomes shared vocabulary, exported from protocol/store. ## Changes to protocol/store (no behavior change) Export the format helpers so pgstore reuses them instead of duplicating logic (guidelines: don't duplicate): - SerializeVersion(version, prevRaw []byte, content, meta) refactored out of buildVersionFile (which keeps the file read and delegates). - ExtractBody, ExtractMetadata, IsArchived, ExtractPreviousHash renamed to exported. - SetArchived(data, archived) extracted from Archive's in-place frontmatter toggle. - ValidateMeta, MetaEqual, JoinContent, RetentionValue exported. File store tests must pass unchanged (behavior-identical refactor). ## New package: server/internal/pgstore pgx driver dependency lives in the server module only; protocol stays dependency-free. Schema applied at startup (CREATE TABLE IF NOT EXISTS): - documents(path text primary key, current_version int, archived bool) - versions(path text, version int, stored bytea, body_hash text, modified timestamptz, primary key(path, version)) - index on body_hash (LookupHash queries the DB directly; no in-RAM hash index) Concurrency: every write transaction row-locks the documents row (SELECT FOR UPDATE, INSERT ON CONFLICT for creation); UNIQUE(path, version) is the backstop CAS. This gives true multi-writer safety; the current-pointer regression race of the file store cannot occur. ## Semantics to replicate (from the file-store study) - WriteVersion expected-version: <0 skip check, 0 create-only, >0 exact; ErrConflict carries Document{Version: current}; post-write conflict check kept. - ErrNotModified when body and publisher meta equal the tip; allowed at expected or expected+1. - ErrArchived rejects writes to archived docs. - Archive toggles the tip's stored frontmatter in place (not a new version) plus the documents.archived column; archived docs drop out of LookupHash. - Retention: RetentionValue(meta) > 0 prunes versions <= current-keep oldest-first, contiguous, returns PruneResult. - Append: read base at expectedVersion, JoinContent, delegate to WriteVersion. - ListDir: distinct next path segments under the prefix; files are docs at exactly prefix/name; a subdirectory lists only if its subtree holds a visible doc; dot-names hidden; archived filtering per flag; sorted; os.ErrNotExist for a prefix with no docs. - Get: version 0 means current; os.ErrNotExist contract throughout. - VerifyChain: sha256 of previous stored bytes vs ExtractPreviousHash of current, oldest to newest. - WalkCurrent equivalent so main.go builds the in-RAM catalog at startup (phase 1 keeps the catalog in RAM; single replica parity). ## Testing: conformance suite server/internal/storetest.RunConformance(t, factory func(t) handler.DocumentStore) exercising the contract above. Run against the file store unconditionally and against pgstore gated on DEMARKUS_TEST_PG_DSN (t.Skip when unset) so CI without Postgres stays green. Behavior divergence between backends is a protocol bug. ## Server wiring Flags -store=file|postgres and -pg-dsn (env DEMARKUS_PG_DSN fallback). Postgres mode skips BuildHashIndex (DB-backed) and builds the catalog from pgstore's WalkCurrent equivalent. ## Phase 2 (explicitly out of scope here) Catalog moves into Postgres (LOOKUP queries the DB) enabling multi-replica world pods; deploy shape changes (Deployment, no PVC). Recorded in ADR 0002 in the deploy project's soul.