Publishing
duckweb and svg_charts are built in this tree and exist in no upstream repository, so everything below is about the gap between “it builds here” and “someone else’s DuckDB can load it”.
What a DuckDB actually fetches
A local repository is read as bare files — INSTALL duckweb FROM '/nix/store/…-duckdb-extension-repo' opens <repo>/v2.0.0/linux_amd64/duckweb.duckdb_extension, which is what the extensions-load check does. Over http(s) it is not the same request. DuckDB asks for exactly one URL per extension:
<repo>/<duckdb version>/<platform>/<name>.duckdb_extension.gz
That <duckdb version> is the asking DuckDB’s own, not the C API version in the footer, so the two answer different questions: the footer decides whether a build loads, the path decides whether it can be installed. One build that would load into several DuckDB releases is still only installable from the directories that exist, so serving more than one release means publishing the same bytes under each version’s path.
gzipped, and with no fallback to the uncompressed name — a mirror that publishes the store path verbatim answers 404 to every INSTALL. That is what packages.duckdb-extension-repo-http is for: the same tree with every file gzipped. The extension-repo-http check installs from it over loopback and compares the installed bytes against the repository’s, so the claim is tested rather than remembered.
Serving it from duckweb
duckweb can host its own distribution, and the check proves it: a megabyte of binary through nested paths comes back byte for byte, and
SELECT duckweb_serve('0.0.0.0', 443, '/srv/extension-repo', 'app.main',
{domains: 'duckweb.example', contact: 'admin@example.com',
cache: '/var/lib/duckweb-web/acme'});makes INSTALL duckweb FROM 'https://duckweb.example/_duckweb' work, because /_duckweb/ is the one path the server reads the asset directory for and everything outside it goes to the app’s render.
To drop the prefix, route the files in the app and answer with duckweb_asset, which hands the request back to the same asset path without putting the bytes through SQL:
UNION ALL SELECT duckweb_asset(m.version || '/' || m.platform || '/' || m.name)
FROM duckweb_match(route, '/:version/:platform/:name') mThat serves the repository at the root — INSTALL duckweb FROM 'https://duckweb.example' — from the same server as the app. The extension-repo-http check installs both ways round, so neither claim is merely remembered. Nothing about the format needs duckweb: a plain static vhost serves it just as well.
Signing
Whatever is hosting them, both extensions are unsigned, so loading either one needs a DuckDB that has been told to allow that:
duckdb -unsignedallow_unsigned_extensions is instance configuration, not a session setting: SET allow_unsigned_extensions = true on a running database is Invalid Input Error: Cannot change allow_unsigned_extensions setting while database is running. It has to be in the config the instance opens with — -unsigned for the CLI, duckdb.connect(config={'allow_unsigned_extensions': 'true'}) from Python, the same key in a JDBC URL. So it is a decision the operator of the process makes, not something an app can arrange for itself.
That is not a formality to be tidied away later; the rest of this section is what it would take to remove it, and the answer is that it cannot be removed cheaply.
What a signature is, and who can make one
The 512-byte footer this repo appends ends in 256 bytes reserved for a signature, and nix/append-extension-metadata.py writes them as zeros — “unsigned” is literal. DuckDB’s own upload script does the same when no key is configured, and otherwise fills those bytes with an RSA-2048 PKCS#1 v1.5 signature over a two-level SHA-256 of everything before them: the file is split into 1 MB segments, each hashed, and the concatenated digests hashed again (scripts/extension-upload-single.sh, scripts/compute-extension-hash.sh, extension.hpp).
The keys that signature is checked against are C string literals in the binary: public_keys[] and community_public_keys[] in src/main/extension/extension_helper.cpp. Both INSTALL and LOAD verify against that list (extension_install.cpp, extension_load.cpp). custom_extension_repository changes where bytes come from, not who is trusted; allow_extensions_metadata_mismatch only relaxes the version and platform fields, and only on a path already gated by allow_unsigned_extensions.
A user can, however, add a key of their own — see CREATE EXTENSION REPOSITORY under Recommendation, which is how a self-hosted repository gets trusted without a flag.
So a third party cannot sign an extension that a stock DuckDB will load without the user first trusting that repository’s key. Signing is otherwise orthogonal to hosting — a community-signed extension mirrored on your own bucket installs with no flags — but a key you hold is not one any stock binary knows.
That leaves three options.
Option 1: submit to duckdb/community-extensions
A pull request adding extensions/duckweb/description.yml — name, license, maintainers, and a repo.github plus a commit ref. The code stays here; DuckDB’s CI builds it for the whole platform matrix, signs it with the community key, and publishes it, so a user runs INSTALL duckweb FROM community; LOAD duckweb; and nothing else (docs, a real descriptor).
What it costs:
- Source has to be public and buildable from a git ref. The Foundation states plainly that it does not review submissions but does require the source and take over building, signing and distribution (securing extensions). Any licence appears to be accepted in practice — MIT dominates, but GPL, AGPL and BSL entries exist.
- The latest-stable treadmill. Community extensions are built only for the newest stable DuckDB; everyone on an older release sees the extension frozen in time, and every DuckDB release rebuilds all of them (development, UPDATING.md). There is also no way for a user to install an older build — the repository serves one version per DuckDB release, and maintainers who need pinning are told to publish elsewhere.
- Rust is fine. The FAQ still claims Rust needs C++ glue, but the repository disagrees: dozens of descriptors declare
language: Rustandbuild: cargotoday. - The name has to be unique and the Foundation can refuse or rename it.
That treadmill is the awkward part for a pre-1.0 tree. It is blunter here than it looks, because the footer declares the stable C_STRUCT ABI: that is satisfied by any DuckDB at or above the declared C API version rather than by an exact match, so one build survives a DuckDB release rather than needing a rebuild per version.
Option 2: ship a forked DuckDB with our key in it
Adding a PEM to public_keys[] and building DuckDB works, and it is the only way to have signature checking pass with a key we hold. It also means asking people to run our engine rather than theirs, which is a far larger thing to ask than a flag.
Option 3: what we do now — a self-hosted repository, unsigned
INSTALL duckweb FROM 'https://…' against the gzipped repository above, loaded by a DuckDB started with -unsigned. Everything under our control, one flag on the user’s side, and no dependency on anyone’s release cadence.
Recommendation
Stay with option 3 while duckweb is pre-1.0. The API here still moves, and the community repository is built for exactly one DuckDB version at a time with no user-side pinning, so publishing there now would trade a documented flag for a distribution channel that reflects whatever the tip of this tree happened to be at DuckDB’s last release. Revisit it when the seam has been validated outside this repo.
There is a fourth option now. CREATE EXTENSION REPOSITORY lets a user trust a self-hosted repository’s own key and then install from it with signature checking still on (test):
CREATE EXTENSION REPOSITORY duckweb
WITH PREFIX 'https://duckweb.jamesst.one'
USING PUBLIC KEY 'MIIBIjANBgkq…';USING PUBLIC KEYS '…', '…' takes several, to allow rotation, and omitting the clause entirely fetches them from <prefix>/.well-known/duckdb-extension-repo.json. Two settings gate it: allow_extension_repositories must be 'allowed' (it defaults to 'undecided', which keeps existing repositories but blocks new ones), and the trust anchor extension_repository_directory can only be set at startup while signature checking is on. That is option 3 without the flag, and it is the thing to move to — it needs a signing key and a .well-known endpoint this tree does not yet have.
One caveat about the documentation
DuckDB’s extension distribution page says a repository is searched for both name.duckdb_extension.gz and then the uncompressed name.duckdb_extension. Over HTTP at v2.0.0 that second request does not happen: serving the uncompressed tree produces one 404 on the .gz URL and an immediate HTTP Error: Failed to download extension. Hence the gzipped variant, and the check that would notice if that ever changed.
Getting the source somewhere people can find it is a different job: the mirroring guide.