How the registry works
Where doze gets modules, what the signed index holds, where the key lives, and what a module must meet to get in.
This page explains where doze gets its modules and what a module has to meet before the registry will carry it.
One registry, one key
Section titled “One registry, one key”Every module <engine> "…" block in a doze.hcl resolves to the module
doze/<engine> in the doze registry, and nowhere else. doze 0.2 reads it from
registry.doze-dev.com. There is no setting that points doze at another
registry, another publisher or a local build. Anything the registry does not
carry runs as your own service.
The registry is static files and signatures: no server, no database, no login. For each module it serves two files:
registry.doze-dev.com/doze/<name>/index.yaml # the module index, signedregistry.doze-dev.com/doze/<name>/meta.yaml # the module's docs, generated from its codeThe plugin archives themselves live on GitHub Releases. The index points at them and carries their signed checksums, so the archive host does not need to be trusted. You can browse the modules at doze-dev.com/registry.
The module index
Section titled “The module index”index.yaml is schema 1. This is the shape, trimmed from the real
doze/valkey index:
schema: 1module: valkey # must equal the directory namenamespace: doze # always dozereleases: "0.2.1": # the MODULE version (not the engine version) protocol: 1 # the doze plugin protocol this build speaks engines: ["8", "9"] # engine majors it supports; absent = versionless artifacts: aarch64-apple-darwin: url: https://github.com/doze-dev/doze-modules/releases/download/valkey/valkey-plugin-0.2.1-aarch64-apple-darwin.tar.gz sha256: 4d4ccc27… # lowercase hex sig: uY6lh8M7… # ed25519 over the hex sha256, base64 aarch64-unknown-linux-gnu: { … } x86_64-unknown-linux-gnu: { … }channels: stable: "0.2.1" # the release a fresh resolve preferssignature: vGTQgqEU… # ed25519 over the canonical payload, belowTwo version numbers meet here. The engine version is what a user writes
(version = 9). The module version is the plugin’s own release number, which
moves on every change to the module. engines connects them: it lists the
engine majors a release supports, and it is generated from the module’s
Describe().Versions (see Write it in doze-modules).
A release may also carry doze:, the oldest doze that can run it. An older doze
skips such releases and, when none fits, says which doze to install.
Two signatures, two jobs
Section titled “Two signatures, two jobs”Each artifact has a sig: ed25519 over the archive’s lowercase-hex SHA-256.
This decides what doze will run. A tampered archive fails, whoever serves it.
The index has a signature: ed25519 over a canonical JSON form of
module, namespace, releases and channels (keys sorted at every level,
no extra whitespace, empty optional fields left out). This decides what doze
will pick. Without it, a hostile host could serve an index that lies about
which engine majors a release supports, points stable back at an old release,
or hides releases, while every archive still verified. The signer
(doze-registry’s scripts/lib.mjs) and the verifier (doze-sdk’s
modindex.Verify) produce the same bytes.
What doze does with it
Section titled “What doze does with it”When a project declares a module, doze:
- fetches
doze/<name>/index.yamland checks its signature against the key compiled into doze; - picks a release: one that speaks doze’s plugin protocol and supports every
engine major the project declares, preferring the
stablehead, otherwise the newest that fits; - downloads the archive for this machine and checks its checksum and
sig; - caches it under
~/.doze/modules/doze/<name>/and pins the version and checksum indoze.lock(see doze.lock).
Errors name the cause: “requires plugin protocol >= N … upgrade doze”, or “no release of postgres supports postgres 19; latest supports 14, 15, 16, 17, 18”.
A pinned, cached project never asks the registry again. Pins move only when
you run doze modules upgrade; doze modules upgrade --check reports what
would move and exits 1 if anything would.
Who can do what
Section titled “Who can do what”| Party | Can | Cannot |
|---|---|---|
| Archive host (GitHub Releases) | Refuse to serve | Change what runs (checksums are signed) |
| Registry host (Cloudflare) | Refuse to serve, serve an old but valid index | Forge an index, move a channel, change engine support, swap the key |
| Holder of the registry key | Sign a new release | Change what a locked project runs (doze.lock pins checksums) |
| doze | Refuse anything that does not verify | Drift silently (pins move only on doze modules upgrade) |
Even a fully compromised registry can only reach a project at the moment its owner resolves or upgrades on purpose.
Where the key lives
Section titled “Where the key lives”The private key exists in one place: an Actions secret (DOZE_SIGNING_KEY) in
doze-registry. Module repos never
see it, so doze-modules asks the registry to sign rather than signing itself.
The public key is compiled into doze (RegistryKey in
internal/modules/modules.go). doze never fetches it, so there is no first
contact to intercept. The cost: rotating the key takes a doze release, and
every older doze stops trusting the registry until it is upgraded. The key is
rotated on suspected compromise, not on a schedule. Locked and cached projects
keep working through a rotation, because they do not contact the registry.
How a release gets in
Section titled “How a release gets in”doze-modules PR → test.yml → merge → release.yml (acceptance against the real backend, build three platforms, upload to the module's GitHub release, notify the registry) │doze-registry publish.yml ◀────────┘ waits for the release to appear, signs every new artifact and the index, validates every signature and re-downloads every archive, commits registry/doze/<name>/index.yaml, deploysThe registry’s publish workflow also runs every hour and signs any release
it has not seen yet, so a missed notification only delays a release. The Kafka
and AWS modules are built in doze-kafka and doze-aws and enter the registry the
same way. Submit and release walks the contributor’s side.
What a submission must meet
Section titled “What a submission must meet”The registry signs only what passes these, and doze refuses anything else:
- Schema 1. An index without
schema: 1is rejected. - Three platforms. Artifacts for
aarch64-apple-darwin,aarch64-unknown-linux-gnuandx86_64-unknown-linux-gnu. - Releases only grow. A published release is never deleted, so a
doze.lockwritten last year still resolves. The registry’svalidateworkflow fails any change that drops or alters a published artifact. - Published bytes never change. A
(version, platform)pair is built once. Any change to a module means a new module version. - Generated, not hand-written.
enginesandmeta.yamlcome from the module’sDescribe(), so the docs and the version gate cannot drift from the code.
meta.yaml is documentation only. It is not signed, and nothing doze enforces
reads it.
For the user’s side of this, see the trust model.