Skip to content

Downstream data contracts: consumers read the live contract, never a copy

Context

The producer owns the CVR hub and every read contract over it — views, PostgREST functions, enum vocabularies, response shapes. Consumers (the sales application, analysis repositories) read those contracts and never write the store. Two failures recur:

  1. Under-coverage. A consumer story that should use the whole of a surface uses only the part already wired in code. Company filters were the trigger: search_companies accepts four p_filters keys, companies holds far more attributes, and filter stories kept shrinking to the four.
  2. Silent drift. The producer changes a contract; the consumer keeps its old model until something breaks in production, not in CI.

A generated field catalog copied into each consumer was proposed and rejected: it duplicates the migration DDL, goes stale between the producer merge and the next sync, and needs a sync job to stay honest.

Decision

This applies to every producer-to-consumer data contract, not only filters.

  1. The producer artifacts are the source of truth. supabase/migrations/*.sql (schema, column comments, enum and reference tables) plus docs/reference/read-contract.md (surface semantics). No consumer holds a copy of a column list, an enum vocabulary, or a response shape.

  2. One schema reference indexes the whole read surface. docs/reference/schema-reference.md names every published view and function and points to its migration and its read-contract.md section. It describes shape at a high level and never restates a column list.

  3. Consumers point, and enumerate from the true source at spec time. Each consumer repository carries a short pointer file and a CLAUDE.md rule: any feature that reads platform evidence is scoped by reading the current contract from the producer working tree, not from what the application already reads. The developer has both repositories checked out.

  4. Contracts grow incrementally through cross-repo tickets. When a consumer needs a field, an enum value, or a shape a contract does not expose, the work fans out into a producer "expose X" ticket and the consumer ticket, linked by a blocking edge. A surface grows one or more additions per ticket; there is no speculative "expose everything" surface.

  5. Every contract surface describes its own shape, and a consumer conformance test reads it live. A view exposes its columns; a function exposes its parameters and accepted values from the place it validates a request; an enum vocabulary is a readable reference table or a ..._vocabulary() function. For each surface it consumes, the consumer keeps a test that reads the live shape and asserts the consumer's model, types, and UI match it — no field dropped, no enum value unhandled, no removed field still referenced. A producer change turns a consumer test red, in CI, before release.

  6. The producer tracks its known consumers. docs/reference/consumers.md records which surfaces each downstream repository depends on, so a contract change knows which conformance suites must go green. This makes the existing "read contracts are released with their consumers" posture actionable.

First application — company filters

  • schema-reference.md is created.
  • search_companies exposes its accepted p_filters keys and each key's allowed values from where it validates them.
  • The sales consumer gains the pointer file, the CLAUDE.md rule, and a conformance test over the accepted filter-key set.
  • Each filter story fans out producer "expose key X" tickets with blocking edges.

Considered options

  • Generated field catalog (JSON) in each consumer repo — rejected: duplicates DDL, goes stale, needs a sync job.
  • PostgREST introspection function over information_schema — rejected: advertises RLS-blocked and non-contract columns, and re-opens the "base-table columns are a contract" leak that removing company_registry_detail (read-contract.md) closed.
  • Pinned git submodule of the producer — rejected: still lags until someone bumps it, a weaker form of the staleness the copy has.

Consequences

  • Consumer CI depends on producer contract shapes. This coupling is deliberate: it is the drift alarm.
  • New producer work: each surface needs a self-describing shape. PostgREST already gives view columns and function signatures; validated key sets and enum vocabularies are the gap.
  • consumers.md is a small manual register that must be updated when a consumer starts or stops using a surface.
  • Every artifact is only useful to a person or agent with the producer repository checked out. That is the accepted working setup.