# Prebuilt extensions
The Theseus `postgresql-binaries` archives that `pg_embedded_setup_unpriv`
downloads carry only the in-tree contrib extensions. Out-of-tree extensions
such as `pgvector` are installed by the extension hook: before the server
starts, the hook fetches a digest-pinned manifest, selects the archive built
for the running PostgreSQL major and for this crate's compile target, verifies
the archive against the digest in the manifest, and copies its files into the
embedded tree. The hook never compiles anything; when no matching, verified
archive exists it fails closed and the server is not started.
The archives and manifest are published by
[`df12-pg-extensions`](https://github.com/leynos/df12-pg-extensions), whose
release workflow is the estate's one permitted source build.
## Declaring extensions
Table: Environment variables read by the extension hook.
| `PG_EXTENSIONS` | Comma-separated `CREATE EXTENSION` names, for example `vector`. Unset, empty or whitespace leaves the hook inert. |
| `PG_EXTENSIONS_MANIFEST` | Location of `manifest.json`: an `https://` URL, a loopback `http://` URL, or a filesystem path. Required when `PG_EXTENSIONS` is set. |
| `PG_EXTENSIONS_MANIFEST_SHA256` | Lower-case hex SHA-256 of the manifest bytes. Required for a URL manifest, loopback included; optional for a path. Pinning the manifest digest pins every archive it describes. |
| `PG_EXTENSIONS_CACHE_DIR` | Where verified archives are kept between runs. Defaults to `$XDG_CACHE_HOME/pg-embedded/extensions`, then `~/.cache/pg-embedded/extensions`, then a temporary directory. |
`TestCluster`, `bootstrap_for_tests`, `run()` and the
`pg_embedded_setup_unpriv` CLI all honour these variables. A consumer that
manages its own `Settings` can call `extensions::install_extensions` directly
with an `ExtensionRequest`.
```bash
export PG_VERSION_REQ="=17.11.0"
export PG_EXTENSIONS="vector"
export PG_EXTENSIONS_MANIFEST="https://github.com/leynos/df12-pg-extensions/releases/download/v1.0.0/manifest.json"
export PG_EXTENSIONS_MANIFEST_SHA256="<digest from manifest.json.sha256>"
```
Matching is on the PostgreSQL major **and minor** together with the compile
target triple (`PG_EMBED_TARGET`, exported by `build.rs`). Theseus's third
component is a build number rather than a PostgreSQL release, so it is not
compared.
Selection is deliberately narrower than loading. A module built for one major
would load into every minor of that major, because the server's `Pg_magic_func`
block checks the major and the layout constants only. The manifest nonetheless
pins one digest per name, version and target, so accepting a neighbouring minor
would install bytes the consumer's pinned manifest digest does not describe for
the server actually running, and the exactness of that chain is the point of
the hook. There is no cross-minor fallback; it is out of scope and would be an
explicit opt-in.
So `PG_VERSION_REQ` and the manifest have to agree on the minor: pinning `^17`
while the manifest offers only 17.10 fails closed when the resolved server is
17.11.
## Manifest schema
Schema version 1. Every field is required; a manifest with another
`schema_version` is rejected outright.
```json
{
"schema_version": 1,
"release": "v1.0.0",
"generated_at": "2026-09-06T00:00:00+00:00",
"extensions": [
{
"name": "vector",
"package": "pgvector",
"version": "0.8.6",
"source": {
"repository": "https://github.com/pgvector/pgvector",
"tag": "v0.8.6",
"commit": "8ee86c96f0fd72390f890aa8a336fda6d3ab4c6c"
},
"artifacts": [
{
"postgresql": "17.11.0",
"target": "x86_64-unknown-linux-gnu",
"file": "pgvector-0.8.6-pg17.11.0-x86_64-unknown-linux-gnu.tar.gz",
"url": "https://github.com/leynos/df12-pg-extensions/releases/download/v1.0.0/pgvector-0.8.6-pg17.11.0-x86_64-unknown-linux-gnu.tar.gz",
"sha256": "<64 lower-case hex digits>",
"size": 102961,
"files": [
"lib/vector.so",
"share/extension/vector--0.8.6.sql",
"share/extension/vector.control"
]
}
]
}
]
}
```
An archive is a gzip tar of regular files under exactly two prefixes relative
to the PostgreSQL install root: `lib/` (one level deep) and `share/extension/`.
Directory entries are tolerated. Symlinks, hard links, device nodes, absolute
paths, `..` components, any other prefix, and any difference between the
archive contents and the manifest `files` list are rejected before a single
file is written.
## Install procedure
1. Fetch the manifest (size-capped at 1 MiB), verify its digest when one is
pinned, parse and validate it.
2. Identify the running PostgreSQL from the versioned installation
directory name, falling back to `bin/pg_config --version`.
3. For each declared name, select the artefact for that name, PostgreSQL
major and target.
4. Reuse `<cache>/<sha256>/<file>` when its digest still matches; otherwise
download to a temporary file under a per-digest lock, verify, and rename
into place.
5. Validate every archive entry, then write each file to a temporary
sibling and rename it over the destination (so a mapped shared object is
replaced by a new inode, never truncated). Modes are `0o755` under `lib/` and
`0o644` under `share/extension/`; on Unix the files are chowned to the
installation directory's owner so the demoted worker can remove them. A
destination that already holds identical bytes is left untouched.
6. Log one `pg_embed::extensions` event per extension and expose the
result through `ClusterHandle::installed_extensions()`.
The hook runs in the parent process after `Setup` and before `Start` in every
lifecycle (synchronous, asynchronous and the CLI's setup-only path).
Binary-cache population moves to just before the hook, so the shared binary
cache never contains extension files.
## Cache layout
```plaintext
{extensions_cache_dir}/
.locks/
<sha256>.lock
<sha256>/
pgvector-0.8.6-pg17.11.0-x86_64-unknown-linux-gnu.tar.gz
```
## Failure modes
Every failure surfaces as a `BootstrapError` whose kind names the cause. The
server is not started and nothing is compiled.
Table: Extension error kinds.
| `ExtensionConfigInvalid` | Names declared without a manifest, a URL manifest without a digest, a malformed name or digest |
| `ExtensionManifestUnavailable` | The manifest path is missing or the URL cannot be fetched |
| `ExtensionManifestDigestMismatch` | The manifest bytes do not hash to `PG_EXTENSIONS_MANIFEST_SHA256` |
| `ExtensionManifestInvalid` | Invalid JSON, wrong `schema_version`, missing field, malformed digest or version |
| `ExtensionUnavailable` | No artefact for the name, running PostgreSQL major, and target; also an unidentifiable PostgreSQL |
| `ExtensionArchiveUnavailable` | The archive cannot be downloaded and no valid cached copy exists |
| `ExtensionArchiveDigestMismatch` | The downloaded bytes do not hash to the manifest digest |
| `ExtensionArchiveInvalid` | A forbidden entry, a path outside the prefixes, or contents that differ from the manifest file list |
| `ExtensionInstallFailed` | An I/O failure writing into the tree; the message lists the files already written |
## Install sequence
Figure: Extension installation between `Setup` and `Start`.
```mermaid
sequenceDiagram
participant startup as startup module
participant hook as extension_hook module
participant cache as binary cache
participant extensions as extensions module
participant fs as installation tree
startup->>startup: Setup (download or cached binaries, initdb)
startup->>hook: run_post_setup(bootstrap, PostSetup)
hook->>hook: refresh_worker_installation_dir
opt cache miss
hook->>cache: try_populate_binary_cache(pristine tree)
end
opt PG_EXTENSIONS declared
hook->>extensions: install_extensions(request, install_dir)
extensions->>extensions: load manifest, verify digest, parse
extensions->>extensions: running_version(install_dir)
loop each declared name
extensions->>extensions: select artefact (major, target)
extensions->>extensions: acquire archive (cache or download), verify sha256
extensions->>extensions: validate every entry
extensions->>fs: write temp sibling, chmod, chown, rename
end
extensions-->>hook: Vec<InstalledExtension>
end
hook-->>startup: Ok
startup->>startup: Start
```