defineReadProjection
defineReadProjection<
TOwnerTable,TOwnerLocal,TAs,TColumns>(owner,opts):SyncTableEntry<TOwnerTable,ProjectionLocalTable<TOwnerTable,TAs,TColumns[number]>> &object
Defined in: packages/contracts/src/registry.ts:847
Define a read projection: a second client shape over a table an owner entry already owns. The
projection reads the SAME physical rows under a DISTINCT local identity (as) and its own narrower
shape — a typed column subset and/or an admin/role-scoped rowFilter — without owning, migrating, or
RLS-guarding any new table. The first use is a light admin view of a heavy authoring table (titles,
not the jsonb), while the learner keeps reading the full table through the owner’s shape.
It is the obvious, DRY way to express “another shape over this table”, versus the bare
shape.electricTable string it replaces (a footgun — config that silently un-asserts table
ownership):
- Owns nothing. The returned entry’s
tableISowner.table(the same object), so there is no newpgTableto migrate or to leak into a drizzle-kit schema barrel. OnlylocalTable(namedas) andshapeare its own.readProjectionis set so generators skip it. - DRY columns.
columnsis a typed subset of the owner’s column keys; the local table is built by filtering the owner’s own column definitions (never restated), and the same subset becomes the Electriccolumnsallow-list so an omitted (e.g. heavy jsonb) column never crosses the wire. The primary key is always kept. Omitcolumnsto sync every column. - Source is derived, never named. The physical Electric target is taken from the owner — there is no consumer-facing source field to get wrong (see ShapeSpec.electricTable).
- Readonly. A projection has no write path; the engine resolves an incoming shape request by its
unique
shapeKey(=as) and consults the derived physical target only on egress.
The rowFilter callback receives the OWNER’s full columns — customWhere runs in Electric against
the physical table, so it may reference a column the local subset omits. RLS for the projection’s
reads lives on the OWNER’s table (a projection adds no DDL to a table it does not own); its
customWhere must be a subset of what that RLS allows.
The owner may be a defineSyncTable entry OR an asReadonly of one — an asReadonly projection
preserves the full read contract (physical table, columns, primary key) and only drops the write path,
so projecting off it is equivalent to projecting off its writable source. A CHAINED read projection
(an owner that is itself a defineReadProjection) is rejected, because it composes wrongly — see below.
Server-side egress redaction (serverProjection + serverOnlyColumns)
Section titled “Server-side egress redaction (serverProjection + serverOnlyColumns)”A projection may carry its own serverProjection (a ServerProjectionSpec, typically a
rowTransform) — resolved by the projection’s shapeKey and run on the proxy egress path for this
shape only. The order on egress is transform first, then omission: the transform runs against the
fetched row, then column omission strips this projection’s omitted columns (the client keep-set is
columns ∪ primaryKey) before the row reaches the client wire. This lets a “secure window” over a
keyed table stream the body while stripping the keys per row.
serverOnlyColumns are owner column keys the transform must READ but that are NOT in the client shape
(e.g. a keysWithheld control flag). Such a key stays omitted from the client keep-set, yet is ADDED
to the Electric fetch allow-list — so it is fetched from Electric, visible to the transform, and then
stripped on egress by the same omission pass, never reaching the client. It requires a
serverProjection.rowTransform (a fetch no transform reads is dead weight) AND columns (with
columns omitted every column is already kept, so “server-only” is a contradiction), and must be
disjoint from columns and the primary key.
No inheritance — ENFORCED. A projection does NOT inherit its owner’s serverProjection. That is
deliberate: an inherited transform whose input column is absent from the projection’s fetch list would
read undefined and silently fail OPEN (serving the un-redacted body) — half-protection worse than
none. Because a bare projection over a redacting owner would therefore egress RAW owner rows, the
registry no longer merely warns: when owner.serverProjection?.rowTransform exists, this function
THROWS at definition time unless the projection declares a posture. You must either declare your own
serverProjection on the projection (typically the same transform fn, plus serverOnlyColumns for its
control-flag inputs), or — only after confirming the projection’s kept columns leak nothing — opt out
explicitly with the literal serverProjection: "unredacted", which attaches no transform (egress raw)
but records that as a visible, reviewed decision at the definition site. The opt-out is meaningful only
where it applies: "unredacted" over an owner with NO egress rowTransform is itself rejected, so a
stale opt-out cannot silently pre-authorize a leak the day the owner grows one.
Type Parameters
Section titled “Type Parameters”TOwnerTable
Section titled “TOwnerTable”TOwnerTable extends AnyPgTable
TOwnerLocal
Section titled “TOwnerLocal”TOwnerLocal extends AnyPgTable
TAs extends string
TColumns
Section titled “TColumns”TColumns extends readonly Extract<keyof TableColumnsShape<TOwnerTable>, string>[] = readonly Extract<keyof TableColumnsShape<TOwnerTable>, string>[]
Parameters
Section titled “Parameters”SyncTableEntry<TOwnerTable, TOwnerLocal>
TAs
The projection’s distinct local identity — its PGlite table name AND its shapeKey.
columns?
Section titled “columns?”TColumns
Column keys (of the owner) to sync locally + fetch from Electric. The PK is always kept. Omit → all.
consistencyGroup?
Section titled “consistencyGroup?”string
retention?
Section titled “retention?”rowClass?
Section titled “rowClass?”string
Row classification (ADR-0052) for THIS projection. Defaults to the OWNER’s rowClass — a projection
reads the owner’s rows, so it carries the owner’s classification (and the invariants bound to it)
unless you say otherwise. Override when the narrower shape genuinely changes the KIND of row the
client receives (e.g. a redacting window over private rows that egresses only public fields).
rowFilter?
Section titled “rowFilter?”(columns) => RowFilterSpec
Row filter for this shape; the callback form receives the owner’s full (physical) columns.
serverOnlyColumns?
Section titled “serverOnlyColumns?”readonly Extract<keyof TableColumnsShape<TOwnerTable>, string>[]
Owner column keys the serverProjection.rowTransform must READ but which are NOT part of the client
shape (e.g. a keysWithheld control flag). They are added to the Electric fetch allow-list so the
transform can see them, then stripped on egress before the client wire. Requires
serverProjection.rowTransform and columns; must be disjoint from columns and the primary key.
serverProjection?
Section titled “serverProjection?”ServerProjectionSpec | "unredacted"
Server-side egress projection (ADR-0004) for THIS shape — typically a rowTransform that redacts a
sub-document of a kept column conditionally on row data. A projection does NOT inherit its owner’s
serverProjection (see the docblock’s no-inheritance caution): an inherited transform whose input
column is unfetched would fail OPEN, so inheritance is refused, not silent. When the OWNER declares
an egress rowTransform, this is therefore required — the registry throws at definition time
unless you either declare your own spec here (typically the same transform fn, plus
serverOnlyColumns for its control-flag inputs) OR opt out with the literal "unredacted". Use
"unredacted" only after confirming this projection’s kept columns leak nothing; it attaches NO
egress transform (the shape streams raw owner rows), but records that as a visible, reviewed
decision at the definition site. "unredacted" over a transform-less owner is itself rejected — a
stale opt-out would silently pre-authorize a leak the day the owner grows a transform.
subscription?
Section titled “subscription?”Returns
Section titled “Returns”SyncTableEntry<TOwnerTable, ProjectionLocalTable<TOwnerTable, TAs, TColumns[number]>> & object