Extensions and packs provide a disciplined way to add domain capabilities (for example, vector search or geospatial) without bloating core. Packs contribute deterministic data to the contract, type-safe query surfaces, codecs for values, migration operations, and optional guardrails. The contract stays code‑free and portable; packs supply the runtime and tooling code at the edges where it is needed.
Responsibilities:
- Encode extension data deterministically into
contract.extensionPacks.<namespace>during emission, with schemas and canonicalization - Provide lane helpers/operators and deterministic lowerers gated by declared capabilities
- Supply codecs and optional guardrails at runtime as composable plugins
- Define migration operations with pre/post checks and clear idempotency
Non‑goals:
- Define every possible extension capability
- Ship a global registry service
- Allow arbitrary network access or repo cloning in hosted environments
graph LR
subgraph Authoring
PSL[PSL/TS Authoring]
end
E[Emitter]
C["contract.json
extensions"]
L["Lane (DSL/ORM)"]
P["Plan { sql|body, meta }"]
R[Runtime]
A[Adapter]
D[Driver]
X["Packs (contract, lanes, runtime, migrate)"]
PSL --> E --> C
X --> E
C --> L --> P --> R --> D
X --> L
X --> R
R --> A
See also: docs/reference/Extension-Packs-Naming-and-Layout.md for naming and filesystem conventions for extension packs (e.g., @prisma-next/extension-pgvector).
Packs integrate at well‑defined boundaries. They consume core artifacts and produce lane/runtime/migration functionality in a way that preserves determinism and contract isolation.
contract.jsonwithextensionPacks.<namespace>payloads and capabilities declared by packs- Plan objects produced by any lane that may reference pack-provided types, operators, or annotations
- Migration edges that may reference pack-provided operations
- Lane functions and lowerers bound to capability IDs
- Codecs for parameters and results
- Migration operations with pre/post checks
- Manifests declaring capability support and security policies for bundling and hosted preflight
Packs are modular and optionally provide slices for the authoring, lane, runtime, and migration phases. Each slice has a single responsibility and is governed by schemas and canonicalization rules where it touches the contract.
A pack is a TS/JS package that may provide any subset of four slices:
-
PSL/contract slice (compile-time)
- PSL attributes, blocks, and validators that contribute deterministic data to
contract.extensionPacks.<ns> - Canonicalization rules so the emitted JSON is stable
- Purely data producing, no runtime code required to read the resulting JSON
- PSL attributes, blocks, and validators that contribute deterministic data to
-
Query-lane slice (build-time)
- Relational DSL builders or helpers and deterministic lowerers for their AST nodes
- Capability IDs used to gate features at build time
- No side effects or environment dependence
-
Runtime slice (execute-time)
- Codecs for parameters/results and optional guardrails or lints
- Registered by the application or by the runtime host
-
Migration slice (plan/apply-time)
- Custom operations and their pre/post checks
- Deterministic JSON op schema plus implementation code
The data contract travels code-free - any context that needs to build, lower, or execute features provided by a pack must have the relevant pack code present.
Extension packs follow a standardized publishing and versioning flow (see ADR 112):
- npm Package: Packs are published as npm packages with
@prisma/pack-prefix - Pure ESM: Packs must be pure ESM modules with no CommonJS dependencies
- Side-Effect Free: Packs must be side-effect free at import time
- Entry Points Required: Packs must provide explicit entrypoints:
/cli: default‑export IR descriptors and family helpers for tooling (used by CLI emit; no runtime code)/runtime: export runtime factories/types for DB‑connected commands and app runtime
Optional JSON Manifest: Packs may also include a packs/manifest.json for non‑CLI consumers (e.g., cloud, bundling). The CLI does not read JSON manifests.
- Semantic Versioning: Packs follow semantic versioning (major.minor.patch)
- Compatibility Matrix: Packs declare compatibility with adapter versions
- Deprecation Policy: Packs provide migration paths for deprecated features
- Breaking Changes: Major version bumps for breaking changes
- Pack Development: Develop pack with proper manifest and tests
- Conformance Testing: Run conformance tests against target adapters
- Security Scanning: Automated security scanning of pack code
- npm Publishing: Publish to npm with proper metadata
- Registry Indexing: Pack indexed in Prisma registry for discovery
Packs are npm packages organized into clear slices corresponding to the compile-time, build-time, and execution phases:
pack-postgres/
├── package.json # npm metadata
├── packs/
│ ├── manifest.json # Capabilities, versions, integrity
│ ├── blocks/ # Top-level block registration (see [ADR 126](../adrs/ADR%20126%20-%20PSL%20top-level%20block%20SPI.md))
│ │ ├── view.js # pg.view, pg.materializedView
│ │ ├── enumType.js # pg.enumType
│ │ └── index.js # Exports and registration
│ ├── decorators/ # Attribute/decorator registration (see [ADR 104](../adrs/ADR%20104%20-%20PSL%20extension%20namespacing%20&%20syntax.md))
│ │ ├── pgType.js # @pg.type(...)
│ │ ├── pgPredicate.js # @@pg.predicate(...)
│ │ └── index.js
│ ├── operators.js # Function and operator registry (see [ADR 113](../adrs/ADR%20113%20-%20Extension%20function%20&%20operator%20registry.md))
│ ├── codecs.js # Branded types and encode/decode (see [ADR 114](../adrs/ADR%20114%20-%20Extension%20codecs%20&%20branded%20types.md))
│ ├── guardrails.js # Lint rules and budgets (see [ADR 115](../adrs/ADR%20115%20-%20Extension%20guardrails%20&%20EXPLAIN%20policies.md))
│ ├── migrations.js # Operations and pre/post checks (see [ADR 116](../adrs/ADR%20116%20-%20Extension-aware%20migration%20ops.md))
│ └── capabilities.json # Declared capability keys (see [ADR 117](../adrs/ADR%20117%20-%20Extension%20capability%20keys.md))
├── src/
│ ├── contract/ # PSL contract slice
│ │ ├── blocks/ # Block parsers and validators
│ │ ├── decorators/ # Decorator validators
│ │ └── index.ts
│ ├── lanes/ # Query lane slice
│ │ ├── operators/ # Function and operator lowerers
│ │ ├── ast-visitors.ts
│ │ └── index.ts
│ ├── runtime/ # Runtime slice
│ │ ├── codecs/
│ │ ├── guardrails/
│ │ └── index.ts
│ ├── migrations/ # Migration slice
│ │ ├── operations/ # Operation implementations
│ │ ├── checks/ # Pre/post check implementations
│ │ └── index.ts
│ ├── index.ts # Main entry point
│ └── __tests__/ # Integration tests
└── README.md # Pack documentation
PSL Contract Slice (src/contract/)
- Registers top-level blocks (views, enums, etc.) as declarative
AuthoringPslBlockDescriptorentries; the framework owns the generic parser, validator, and printer (see ADR 126). Each contributed block kind requires a matching IR class andentityTypesfactory — all three layers are tied by the shareddiscriminatorstring (see ADR 225 — Three-layer extensibility for pack-contributed entity kinds) - Registers decorators/attributes (e.g.,
@pg.type,@@pg.predicate) (see ADR 104) - No database access or file I/O
Query Lane Slice (src/lanes/)
- Registers functions and operators with signatures and lowerers
- Provides AST visitors for lowering pack-specific nodes
- Exports capability flags and their requirements
- All lowering is deterministic and testable
Runtime Slice (src/runtime/)
- Exports codecs for branded types (encode/decode)
- Implements guardrails and lint rules
- Provides optional runtime plugins for telemetry or policy enforcement
- Code is optional and only loaded if the application registers it
Migration Slice (src/migrations/)
- Exports migration operations (DDL, data tasks)
- Implements pre/post checks (see ADR 028 / ADR 188)
- Declares idempotency classification
- All operations are deterministic and reversible
- For schema-contributing extensions (extensions that introduce tables / types / domains the user's columns reference), the migration slice exposes a contract space — see Schema-contributing extensions: contract spaces below and ADR 212
{
"id": "postgres",
"version": "15.0.0",
"spiVersion": "1.0.0",
"capabilities": {
"postgres.view.base": true,
"postgres.view.materialized": true,
"postgres.view.refresh": true,
"postgres.enumType.storage": true,
"postgres.index.partial": true
},
"slices": {
"contract": true,
"lanes": true,
"runtime": true,
"migrate": true
},
"targets": {
"postgres": {
"minVersion": "12"
}
},
"integrity": {
"index.js": "sha256:abc123...",
"codecs.js": "sha256:def456...",
"operators.js": "sha256:ghi789..."
},
"policy": {
"noNetwork": true,
"noDynamicImport": true,
"noWasm": true,
"noShell": true
}
}Packs export a factory function that the emitter calls at startup:
// @prisma/pack-postgres/src/index.ts
import { registerBlocks } from './contract/blocks'
import { registerDecorators } from './contract/decorators'
import { registerOperators } from './lanes/operators'
import { registerCodecs } from './runtime/codecs'
import { registerMigrations } from './migrations'
export function registerPostgresPack(context: PackContext) {
registerBlocks(context.blockRegistry)
registerDecorators(context.attributeRegistry)
registerOperators(context.operatorRegistry)
registerCodecs(context.codecRegistry)
registerMigrations(context.migrationRegistry)
}
// Emitter calls this automatically
export default { register: registerPostgresPack }Each pack includes comprehensive tests:
src/__tests__/
├── contract/
│ ├── blocks.test.ts # View/enum block parsing and emission
│ ├── decorators.test.ts # Decorator validation
│ └── canonicalization.test.ts # Extension data hashing
├── lanes/
│ ├── operators.test.ts # Operator lowering and type inference
│ ├── lowering.golden.ts # Golden tests for SQL output
│ └── capability-branching.test.ts
├── runtime/
│ ├── codecs.test.ts # Encode/decode round-trip
│ └── guardrails.test.ts # Lint rule application
├── migrations/
│ ├── operations.test.ts # DDL and data ops
│ ├── idempotency.test.ts # IF NOT EXISTS patterns
│ └── pre-post-checks.test.ts
└── integration.test.ts # End-to-end workflows
Some extensions ship persistence structures the user's schema references — cipherstash's eql_v2_encrypted composite type that Encrypted<String> columns set as their nativeType, pgvector's parameterized vector type, and so on. Before TML-2397, these extensions installed SQL via a side-channel (databaseDependencies.init) that the framework's planner / verifier / migration runner couldn't see. The verifier rejected those objects as "extras" in strict mode; extension bumps had no on-disk record; there was no way to migrate an extension forward.
ADR 212 — Contract spaces replaces that mechanism with contract spaces: each schema-contributing extension owns a (contract.json, migrations, headRef) triple that the framework treats with the same per-space planner / runner / verifier the application uses. Extension authors who only contribute codecs / query operators don't need a contract space; the field is opt-in.
The shape is family-agnostic. ContractSpace<TContract> and ContractSpaceHeadRef live in @prisma-next/framework-components/control so every family (SQL, Mongo, future) reuses the same triple; the family-side descriptor only specialises the contract type parameter.
import type { Contract } from '@prisma-next/contract/types';
import type { ContractSpace } from '@prisma-next/framework-components/control';
import type { SqlStorage } from '@prisma-next/sql-contract/types';
export interface SqlControlExtensionDescriptor<TTargetId extends string>
extends ControlExtensionDescriptor<'sql', TTargetId> {
readonly contractSpace?: ContractSpace<Contract<SqlStorage>>;
// … existing fields …
}The framework reads the descriptor only at authoring time (during prisma-next migrate). At apply time and verify time it reads only the user's repo (pinned migrations/<space-id>/contract.json, contract.d.ts, refs/head.json, and migration directories), so apply / verify works in CI / production environments where the extension package is not even installed.
In-repo worked examples:
packages/3-extensions/pgvector/— ported fromdatabaseDependenciesto acontractSpace. Declares the parameterizedvectortype in itscontract.json; ships one baseline migration whose body isCREATE EXTENSION IF NOT EXISTS vector, carrying apgvector:install-vector-v1invariantId.
An external reference implementation is the CipherStash extension (@cipherstash/prisma-next), which lives in CipherStash's own repository. It demonstrates the full contract-space mechanism for an encryption extension: contributing composite types, codec lifecycle hooks (ADR 213), and per-(table, column) migration ops.
The extension descriptor never imports build-time-only material: everything it exposes is in-memory JSON values plus codec runtime functions. The build step (tsdown) produces self-contained descriptor values.
See packages/3-extensions/pgvector/ and packages/3-extensions/paradedb/ for the canonical on-disk layout (migrations/refs/head.json, migrations/<dirName>/..., src/contract.{ts|prisma,json,d.ts}, prisma-next.config.ts at the package root). See .cursor/rules/contract-space-package-layout.mdc for the rule spelled out.
prisma-next migrate writes (or overwrites) one pinned-artefact subtree per loaded extension:
migrations/
└── pgvector/
├── contract.json ← byte-for-byte == descriptor.contractSpace.contractJson
├── contract.d.ts ← typed interface for the pgvector schema
├── refs/head.json ← byte-for-byte == descriptor.contractSpace.headRef
└── 20240601T0000_install_vector/
└── …
Bumping an extension's package version produces a clear PR diff: updated pinned contract.json / contract.d.ts / refs/head.json, plus any new migration directories. Both halves are reviewable, hashable, and version-controlled.
Schema-driven per-column behaviour — e.g. an encryption extension registering each searchable column with its search configuration — is not a function of the extension version but of the consuming application's schema. Codecs may declare a plan-time onFieldEvent hook (synchronous, app-space-bound) that returns migration ops the planner inlines into the app-space migration alongside the user's own structural ops. See ADR 213.
When an extension ships a contract space (a separate contract.json with its own tables, as described above), application schemas can declare FK references that target those extension-owned tables. This is how public.profile.user_id can reference auth.users.id from the Supabase extension's contract space.
A cross-space FK names a target table in another contract space. The framework resolves it against the loaded extension contracts and emits a normal database FK constraint. See ADR 226 for the full decision record.
Cross-contract references are constrained by a directional acyclic graph. extensionPacks in defineContract serves as both the import declaration (which extension models are reachable) and the dependency declaration (load ordering).
flowchart LR
App["App contract"] -- extensionPacks --> Supabase["supabase contract space"]
Supabase -- extensionPacks --> Base["(no further deps)"]
The aggregate loads depended-on spaces first, then the app space. References must follow the dependency arrows: the app can reference Supabase models; the Supabase contract cannot reference app models. Cycles and reverse references are rejected at load time with a diagnostic naming the cycle members or the offending reverse reference.
Namespaces are open for extension — multiple contracts can contribute models to the same namespace. Primitives (model, enum, type) are owned by the contract that declares them.
| Concept | Open for extension? | Collision rule |
|---|---|---|
| Namespace | Yes | N/A |
| Primitive (model, enum, type) | No | Fail-fast load error naming both contributors |
If an app declares model Session {} inside namespace auth {} but the Supabase extension already declares auth.Session, the aggregate fails to load with a diagnostic naming both contributors. This surfaces a permission/ownership problem at authoring time rather than at migration time.
When the contract is compiled, each cross-space reference is resolved against the loaded extension contracts — the same set declared in extensionPacks. There is no separate PSL use directive or TS resolver call. A reference to a space that isn't declared, or to a model/column that doesn't exist in it, fails fast with a diagnostic that names the missing pack.
A future use ... as aliasing directive is reserved as an additive layer on top of this implicit resolution; it is not required today.
PSL form — colon-prefix dot-qualified type reference in field-type position:
types {
Uuid = String @db.Uuid // must match auth.users.id native type
}
namespace public {
model Profile {
id String @id @default(uuid())
userId Uuid @unique // @unique makes this a 1:1 relationship
user supabase:auth.AuthUser @relation(from: [userId], to: [id], onDelete: Cascade)
@@map("profile")
}
}supabase:auth.AuthUser reads broad-to-narrow: contract space supabase, namespace auth, model AuthUser. The @relation attribute is unchanged. The colon prefix is the only new PSL grammar element.
For a cross-contract reference to a model in __unspecified__ (e.g. a SQLite extension or a multi-tenancy Postgres extension that defers to search_path), elide the namespace dot: supabase:User.
TS form — same call shape as local FK calls; the framework distinguishes cross-contract from local via the brand on the imported model handle:
import { AuthUser } from '@prisma-next/extension-supabase/contract';
import supabaseExtension from '@prisma-next/extension-supabase/pack';
export const contract = defineContract(
{ family: sqlFamily, target: postgresPack, namespaces: ['public'], extensionPacks: [supabaseExtension] },
({ field, model }) => {
const Profile = model('Profile', {
namespace: 'public',
fields: { id: field.id.uuidv4String(), userId: field.uuidString(), username: field.text() },
});
return {
models: {
Profile: Profile.relations({
user: rel.belongsTo(AuthUser, { from: 'userId', to: 'id' }),
}).sql(({ cols, constraints }) => ({
table: 'profile',
foreignKeys: [
constraints.foreignKey(cols.userId, AuthUser.refs.id, { name: 'profile_userId_fkey', onDelete: 'cascade' }),
],
})),
},
};
},
);The AuthUser handle is branded with spaceId: 'supabase' — that brand is what causes constraints.foreignKey to lower to a carrier with spaceId set. No separate call surface (refExt, belongsToExternal) exists.
The branded column reference carries a space id, not the target column's storage type. If the target column has a non-default native type — auth.users.id is uuid — you must match that type on the source column. Postgres rejects mismatched FK column types at apply time; the framework does not coerce.
PSL: declare a named type alias in a types {} block and use it on the source field (as shown above). @db.X is a type-constructor attribute — it is valid only inside types { Name = T @db.X } declarations, not as a field attribute. Writing userId String @db.Uuid is rejected by the PSL parser.
TS: use the field builder that produces the matching native type: field.uuidNative() for a Postgres native UUID target, field.uuidString() for cross-target char(36), field.text() for text, etc.
The carrier records the target model's declared namespace via namespaceId. The planner uses that to decide whether to emit a qualified or unqualified REFERENCES clause:
| Target home namespace | Emitted DDL |
|---|---|
Named (e.g. auth) |
REFERENCES "auth"."users"("id") |
__unbound__ |
REFERENCES "users"("id") |
This is symmetric with the table-creation DDL rule for __unbound__ introduced alongside contract spaces.
Both PSL (user supabase:auth.AuthUser @relation(...)) and TS (rel.belongsTo(AuthUser, ...)) declare a relationship, not merely a column constraint. The emitter renders cross-space relations so that ORM traversal (db.public.Profile.find({ include: { user: true } })) is a compile-time error. The FK drives the database constraint (referential integrity and cascade); query and traversal across spaces are deferred to a future cross-space query model.
onDelete and the rest of the referential-action set are permitted on cross-contract FKs. No diagnostic is emitted. The developer's explicit opt-in at the call site is the audit trail, per the repo-wide policy at .agents/rules/explicit-opt-in-over-diagnostics.mdc.
Extension packs populate a function and operator registry (see ADR 113):
- Pack Registration: Packs register functions and operators at load time
- Signature Validation: Function signatures validated against pack schemas
- Type Inference: Registry provides type inference for extension functions
- Rendering Hooks: Packs provide rendering hooks for adapter-specific SQL
- DSL Integration: Registry integrates with query DSL for type-safe usage
- Capability Gating: Functions/operators gated by adapter capabilities
- Plan Annotations: Registry functions/operators included in Plan annotations
- Error Handling: Stable error codes for registry-related failures
Extension packs provide codecs and branded types (see ADR 114):
- Encode/Decode: Codecs provide bidirectional conversion between JS and DB formats
- Validation: Codecs validate extension values at runtime
- Losslessness: Codecs support configurable losslessness policies
- Performance: Codecs optimized for performance with caching
- TypeScript Integration: Branded types provide compile-time type safety
- Runtime Validation: Branded types validated at runtime
- Extension Values: Branded types prevent mixing incompatible extension values
- Type Inference: Branded types integrate with query DSL type inference
Packs may enable explicit per‑column type selection by attaching a typeId in an extension decoration. The contract remains code‑free; the decoration references a namespaced identifier. Example:
{
"extensionPacks": {
"postgres": {
"decorations": {
"columns": [
{
"ref": { "kind": "column", "table": "public.user", "column": "createdAt" },
"payload": { "typeId": "core/iso-datetime@1" }
}
]
}
}
}
}Packs export TS types for codec IO mapping that the emitter references from contract.d.ts:
// @prisma-next/target-postgres/codec-types
export type CodecTypes = {
readonly 'core/string@1': { readonly input: string; readonly output: string };
readonly 'core/int@1': { readonly input: number; readonly output: number };
readonly 'core/iso-datetime@1': { readonly input: Date | string; readonly output: string };
};At emit time, only the IDs actually used by the contract are included in contract.d.ts via imports (types‑only). Runtime codec implementations are registered by the adapter/packs and validated at execution (see Architecture Overview and Query Lanes).
Extension packs implement guardrails and EXPLAIN policies (see ADR 115):
- Capability Gating: Guardrails enforce capability requirements
- Budget Enforcement: Guardrails apply extension-specific budgets
- Policy Rules: Guardrails implement pack-specific policy rules
- Error Reporting: Guardrails provide clear error messages and remediation
- Cost Estimation: Extension operators provide normalized cost estimates
- Row Estimation: Extension operators provide normalized row estimates
- Execution Time: Extension operators provide normalized execution time estimates
- Resource Usage: Extension operators report normalized resource usage
Extension packs provide migration operations (see ADR 116):
- DDL Operations: Extension-specific DDL operations (e.g., CREATE INDEX with extension options)
- Data Operations: Extension-specific data operations (e.g., vector index population)
- Validation: Pre/post checks for extension operations
- Rollback: Rollback strategies for extension operations
- Operation Requirements: Operations declare required capabilities
- Capability Checks: Operations check capabilities before execution
- Graceful Degradation: Operations provide fallback behavior for missing capabilities
- Error Handling: Clear error messages for capability-related failures
Extension packs define capability keys (see ADR 117):
- Namespace Rules: Capability keys follow namespace rules and stability contract
- Type Definitions: Capability keys define types (boolean, string, object, array)
- Documentation: Capability keys include documentation and examples
- Compatibility: Capability keys maintain backward compatibility
- Core Capabilities: Core capabilities reserved and immutable
- Extension Capabilities: Extension capabilities prefixed by pack namespace
- Reserved Names: Reserved namespaces cannot be used by packs
- Collision Prevention: Namespace collision prevention and resolution
The system branches on declared capability keys, not on targets or adapters. Packs carry stability metadata and expose capability keys explicitly; runtimes discover capabilities at connect and validate against the contract’s declared requirements. The contract remains code‑free and isolates runtime configuration (adapter/driver/transport).
- No core feature flags. Unstable or experimental features are shipped as extension packs, not as toggles in core.
- Stability metadata in packs. Each capability declared by a pack carries a stability level
experimental | preview | stablein the pack manifest. This stability is surfaced incontract.extensionPacks.<ns>for auditability and policy. - Capability gating over flags. Runtimes negotiate capabilities at connect; lanes and plugins branch on capabilities. Experimental features are not enabled implicitly—apps must install the pack and use its namespaced surface.
- Default policy by environment.
- Development: allow experimental, emit warnings.
- CI/Preflight: warn or error per repo policy.
- Production: block experimental by default (overridable via explicit runtime policy config, not per-feature flags).
- Diagnostics and drift. The contract records pack usage and capabilities;
profileHashchanges when the capability set changes. Preflight and runtime verification catch mismatches deterministically.
Packs declare capability stability in their manifest and the emitter includes stability in the contract for verification and policy:
{
"namespace": "pgvector",
"version": "1.2.0",
"capabilities": {
"pgvector.ivfflat": { "stability": "stable" },
"pgvector.hnsw": { "stability": "experimental" }
}
}- A built-in lint
experimental-capability-in-proderrors when Plans use experimental capabilities in production. - Policy can downgrade/upgrade levels per environment; see Runtime policy configuration.
Extension packs follow bundle inclusion policy (see ADR 118):
- Pack Code: Pack code included in bundles as ESM files
- Manifest: Pack manifests included in bundles for capability negotiation
- Integrity: Pack code integrity verified via SHA-256 hashes
- Security: Pack code runs under sandbox constraints
- Signature Verification: Bundles verified via cryptographic signatures
- Policy Compliance: Bundles checked for policy compliance
- Security Scanning: Bundles scanned for security vulnerabilities
- Execution Constraints: Bundles enforce execution constraints
Two supported ways to make pack code available outside the local dev environment:
-
Certified catalog mode
- Hosted platforms like PPg preinstall vetted pack versions (e.g.
@prisma/pack-pgvector@1.2.0) - Your artifacts refer to packs by ID and version in a manifest
- The host resolves from its catalog and verifies integrity
- Hosted platforms like PPg preinstall vetted pack versions (e.g.
-
Bring-your-own bundle mode
- CLI produces a deterministic preflight bundle containing:
contract.json, migration edges, and Plan diagnostics if needed- Pack code for the slices you used as ESM files
- Pack manifests with capability IDs, versions, integrity digests, and security declarations
- Hosted runner executes in a sandbox with no network, no dynamic imports, no shell, no WASM, and resource caps
- CLI produces a deterministic preflight bundle containing:
Both modes yield the same outcomes and error taxonomy.
Extensions accept rich textual payloads via template-tagged string literals.
- Syntax:
<pack>[.<flavor>]followed by a backtick string, no interpolation - Core canonicalizes the body and routes it to the owning pack
- Packs validate and normalize, returning deterministic JSON to embed in the contract
Example:
pg.view MyView {
definition: pg.sql`
select u.id, count(p.id) as post_count
from "User" u
left join "Post" p on p.author_id = u.id
group by u.id
`
materialized: true
}
@@index([status], where: pg.predicate`status <> 'archived'`)Canonicalization rules and contract encoding are defined for packs; see ADR 104, ADR 105, ADR 106 and ADR 129 for details.
This document is the hub for all extension guidance across contract, lanes, runtime, migrate, and preflight.
- Pack anatomy: capabilities declared, contract
extschema it owns, optional codecs/functions/operators, optional migration ops and guardrails. - Publishing and compatibility: versioning, namespaces, and semantic guarantees.
- Examples: PostGIS, PGVector, Citus distribution (capabilities, contract
ext, operators, codecs, migration ops, guardrails).
Cross-references
- ADR 112 target extension packs
- ADR 113 operator registry
- ADR 114 codecs and branded types
- ADR 115 guardrails
- ADR 116 migration ops
- ADR 117 capability keys
- ADR 118 bundle inclusion policy
- ADR 129 template-tagged literals for extensions
- Packs declare canonical capability keys in their manifest and at registration time
- Adapters and runtimes expose
supports(capabilityId)and surface capability sets during connect - Lanes and plugins branch on capabilities rather than target strings
- If a required capability is missing:
- At build time, DSL compilation fails with a precise capability error
- At run time, execution is blocked with a
ERR_CAPABILITY_MISSINGerror
- See ADR 065 for the capability schema and discovery
- Developer installs a pack via npm or vendors it
- Emitter reads PSL and invokes pack contract slice to produce deterministic JSON under
extensionPacks.<ns> - DSL uses pack lane slice to build AST nodes and lower to SQL when needed
- Runtime registers pack codecs and optional lints
- Plans execute with guardrails and decoding in place
- CI invokes CLI to create a preflight bundle
- Bundle includes
contract.json, migration edges, and either:- Catalog references for packs, or
- Embedded pack code and manifests
- The host validates signatures, integrity, and policy
- The host runs preflight in a sandbox with no network and enforced limits
- Results are returned with the same diagnostics schema used locally
registerPack({ id, version, capabilities, codecs, lowerers, ops })registerCodec(typeId, codec)registerLowerer(nodeKind, lowerer)registerOp(name, impl, { pre, post, idempotent, requiresTx })
pack.manifest.jsonid,version,capabilities[]slices: { contract?: true, lane?: true, runtime?: true, migrate?: true }integrity: { file: sha256 }policy: { noNetwork: true, noDynamicImport: true, noWasm: true }targets: { postgres?: { minVersion: "14" } }
ERR_CAPABILITY_MISSING,ERR_LOWERING_UNSUPPORTED,ERR_CODEC_MISSING,ERR_PACK_POLICY_VIOLATION- Stable codes (see ADR 027) and mapping (see ADR 068)
- Registration APIs stable once published
- Contract JSON remains code-free (see ADR 010)
- Contract extensions namespace reserved per pack ID
- Codec registry (see ADR 030)
- Lowering SPI (see ADR 016)
- Custom operations loaded (see ADR 041) with the same sandbox rules
- Advisors can be delivered as packs that implement the advisor SPI and hook into preflight (see ADR 101)
- Contract emission must remain O(n) on schema size, independent of pack code size
- Lowering must be deterministic and linear in AST size
- Runtime codec and lint overhead must keep p95 impact under the runtime overhead budget
- Hosted sandboxes enforce CPU, memory, and wall clock limits per op
- Data contract contains no code and no secrets
- Hosted preflight forbids network, dynamic imports, shells, and WASM in pack code
- Artifacts and logs follow redaction policy (see ADR 024 and ADR 050)
- Manifests enumerate integrity hashes for all shipped files
- Missing pack code at build or run time → explicit capability error with remediation hint
- Version mismatch in hosted catalog mode → fail with
ERR_PACK_VERSION_UNAVAILABLE - Policy violation in bundle mode → reject with
ERR_PACK_POLICY_VIOLATION - Codec not found for a pack type at execution → block with
ERR_CODEC_MISSING
- Catalog governance for PPg scale and update cadence
- Compatibility matrix as packs evolve capabilities
- Testing strategy for cross-version lowering determinism and golden SQL
- Golden tests for contract emission with pack extensions
- Golden SQL for lowering with capability flags toggled
- Conformance suite for codecs and operations (see ADR 026)
- Bundle parity tests to assert local vs hosted outcomes match on the same artifacts