Distributing Your Extension
There are two ways to get an extension onto someone's machine. Both work; they differ in how much the app is willing to vouch for the result.
:::info Current status
Sideloading is the day-one path and works today. The marketplace machinery described below is built, tested, and has a UI -- Preferences ▸ Extensions has Browse and Catalogs tabs -- but this build ships with no default catalog and no blocklist endpoint configured, so nothing can currently reach the marketplace tier and nothing is blocked. Users can still add a catalog of their own; extensions from one are treated exactly like a sideload. If you are shipping an extension right now, Packaging and Build plus the signing section here is all you need.
:::
Sideloading
Hand someone a .zip and they install it from the Extensions panel. No marketplace, no review, no gatekeeper. This is intentional and permanent -- it is not a fallback for extensions that failed review, because there is no review to fail.
A sideloaded extension is fully functional. Everything in the api.* surface is available to it, subject only to the permissions the user grants. What it does not get is a badge.
Signing and trust tiers
Extensions carry an optional extension.sig -- an Ed25519 signature over a hash of the package contents. Signing is never an install gate. An unsigned extension installs with a warning; a signed one installs without extra ceremony. The signature exists for provenance, not permission.
The app classifies every installed extension into one of three tiers:
| Tier | What it means |
|---|---|
untrusted | Sideloaded, unsigned, signed by a publisher the app does not know, or installed from a catalog the user added themselves. The default and the common case. |
signed | The signature is intact and the signing key is in the app's own trusted-publisher set. |
marketplace | Installed from the app's configured default catalog. |
Why a valid signature is not enough
Verification checks a package against the public key shipped inside that same package. Anyone can generate a keypair and sign their own work, so "the signature checks out" proves only that the files have not been altered since signing. It says nothing about who signed.
Promotion to signed therefore requires the key to appear in a set the app controls, compiled into the build. Adding a key there is a deliberate decision by the app maintainers, and the badge is worded as identity confirmed, not code reviewed -- nobody has audited the extension's behaviour.
The tier is recomputed, not stored
Trust tiers are derived on every read from the current trusted-publisher set and the currently configured default catalog. Adding a publisher immediately re-badges everything they have signed; changing or clearing the default catalog immediately re-badges everything installed from the old one. There is no stale badge sitting in a database.
Catalogs
A catalog is a JSON document listing downloadable extensions. Users can add their own, and the app may ship with a default one configured.
{
"format": 1,
"name": "Example Catalog",
"extensions": [
{
"id": "ext.example.hello",
"version": "1.0.0",
"name": "Hello",
"description": "Says hello",
"downloadUrl": "https://example.com/hello-1.0.0.zip",
"sha256": "a1b2c3…",
"sizeBytes": 20480,
"permissions": ["bible:read"],
"engines": "^1.0.0"
}
]
}
downloadUrl must be https. sha256 is the digest of the bundle bytes, and the app checks it before unpacking anything -- a bundle that does not match is discarded without ever reaching the extraction code. A malformed listing is skipped; the rest of the catalog still works.
Only the default catalog confers the marketplace tier
If any catalog could grant marketplace, anyone could publish a catalog.json, talk a user into adding it, and mint marketplace-tier extensions. So an extension installed from a user-added catalog is classified exactly like a sideload.
Adding a non-default catalog also requires an explicit risk acknowledgement. Until the user accepts it, the source is listed but never fetched -- the app does not so much as open a connection to it.
Catalog installs land disabled
Downloading an extension from a catalog runs the same permission prompt a sideload does, and the result is installed but not enabled. Consent covers permissions, not execution. Browsing a marketplace can never put running code on someone's machine in one click.
The blocklist
The app can refuse to run an extension that has been found to be harmful.
Blocked means refuse-to-activate, never delete. The app declines to start the extension and shows the published reason. It does not uninstall anything, and it does not touch user data. If you have ever seen an editor silently remove an extension you installed, this deliberately does not do that.
Rules are per-extension-id plus an optional semver range, so a bad 1.4.2 does not condemn 1.4.1 forever:
{
"format": 1,
"entries": [
{ "id": "ext.bad.one", "versions": ">=1.4.0 <1.4.3", "reason": "Corrupts notes on save", "url": "https://…" },
{ "id": "ext.bad.two", "reason": "Exfiltrates user data" }
]
}
Every rule must carry a reason. A rule without one is discarded rather than applied -- an extension that refuses to start without saying why is a support ticket, not a safety feature.
The blocklist is only fetched when the user checks for updates
There is no timer, no startup fetch, and no background request. Block rules arrive as part of the manual Check for Updates flow and at no other time. This app does not phone home on a schedule, and the blocklist is not an exception to that.
The cost is real and worth stating plainly: a user who never checks for updates never receives block rules. That is an accepted trade, not an oversight. Only the app's own blocklist is honored -- a catalog cannot publish block rules, because "disable this extension everywhere" is not a capability a third-party catalog should have.
API stability
:::warning The extension API is currently 0.x -- breaking changes are possible
The api.* surface is not frozen yet. Two known gaps remain: reverse-RPC endpoint binding for registration callbacks, and four declared events that no host code emits yet. Until those close, method signatures may change between releases.
:::
Once the surface reaches 1.0:
- Breaking changes only on a major version.
hostMinSupportedApiVersionstays at1.0for the life of the1.xline. - Deprecations get two minor versions. A method marked deprecated in
1.4keeps working through1.6at minimum, and the deprecation notice names the replacement. - Your manifest declares what you need.
engines.bibleAppis a semver range checked before your extension is activated -- an incompatible extension fails fast with a clear error rather than breaking halfway throughactivate().
Declare the widest range you actually support. "^1.0.0" is right for most extensions; pin tighter only if you depend on something added in a specific minor.