ADR-0038: The transactional schema is extension-free
ADR-0038: The transactional schema is extension-free
Status
Accepted
Date
2026-07-05
Context
The Postgres migrations hard-required two extensions — postgis and
pgcrypto — created in migrations/postgres/000001_init_extensions.up.sql.
That single file made the schema un-runnable on any PostgreSQL that does not
ship those extensions: the dev stack and CI were pinned to the
postgis/postgis image, and the schema could not be hosted on an
extension-less engine.
Two directions made that lock-in a real constraint rather than a convenience:
- One-binary / embedded deployment. The platform direction is a kernel that
operates its own stores end to end —
postgresql_embeddedon the server, and (at the small-scale / browser tier) PGlite or DuckDB — none of which can be assumed to carry PostGIS/pgcrypto. See ADR-0007 (lean profile first) and ADR-0004 (sqlx, portable SQL over an ORM). - A browser target. Running the kernel in WASM with a WASM relational store (PGlite / DuckDB-wasm) is only possible if the schema needs no native C-extension.
Auditing actual usage showed the dependency was almost vestigial: no
geometry/geography columns or ST_* calls exist in any migration or in
Rust SQL — geometry already lives as GeoJSON inside jsonb traits, validated
Rust-side, and spatial ingest already runs through DuckDB. The only
pgcrypto-adjacent usage is gen_random_uuid() defaults, which is a core
PostgreSQL built-in since PG 13 (we target 17), independent of the extension.
Decision
The transactional schema is extension-free. It uses only a portable
relational subset that runs on any vanilla PostgreSQL, and by extension on
postgresql_embedded, PGlite, and (through its own adapter) DuckDB.
Concretely:
- No
create extension.postgisandpgcryptoare removed from000001. UUID defaults keepgen_random_uuid()(core PG 13+, not pgcrypto). - Spatial belongs to the analytical engine, not the store. Geometry at rest is GeoJSON (later WKB) in plain columns; spatial operators run in the analytical plane (DuckDB over Parquet / duckdb-wasm), never as PostGIS in the transactional path.
- Crypto and identifiers are application-side or core built-ins — no
pgcryptodigest()/crypt()/pgp_*; encryption stays in Rust (chacha20poly1305), uuids staygen_random_uuid()/ uuid-v5. - Time series stay plain — append tables with a time index, no extension-backed types.
- JSON usage stays portable — plain jsonb + core GIN (
jsonb_ops); no extension-only operator classes (btree_gin,pg_trgm, …).
Enforcement is mechanical, not aspirational: a filesystem guard test
(migrations_are_extension_free in vinxi-adapter-postgres) fails on any
create extension / geometry( / geography( / ::geometry / ::geography
/ tsvector token, and the dev + CI Postgres images are vanilla postgres:17
— a green suite on an image that has no PostGIS is the permanent proof.
Consequences
- Unblocks the embedded and browser DB tiers. The same schema now backs
real Postgres (scale),
postgresql_embedded(single-binary server), and PGlite / DuckDB-wasm (browser / low-scale) behind the existing StatePort adapter seam. This is the prerequisite for the one-binary platform direction. - Spatial responsibility moves to the analytical plane. Spatial queries are
answered by DuckDB (native or wasm) over materialized Parquet, not by the
transactional store. At the server/scale tier this introduces a
materialization/staleness concern (the existing
materializationschema and derivation evaluator are the hooks); the embedded/browser tiers collapse it by using one engine for operational + analytical + spatial. - A one-time migration-checksum break. Editing the applied
000001invalidates sqlx’s stored checksum on existing long-lived databases; those are re-created (./dev resetper worktree). CI is unaffected (fresh DB per run). This was a sanctioned, deliberate one-time exception to the “never edit an applied migration” rule; the rule stands afterwards. - Regression is guarded. The guard test plus vanilla-image CI prevent the extension surface from creeping back.
- Supersedes nothing (the extension requirement was never an ADR); it records a now-binding constraint referenced by the one-binary platform direction.