Publishing guidelines
This page covers the plugin package format, signing, distribution channels, updates, and the publishing checklist. We recommend running the whole package → sign → publish flow with theqomicex CLI: qomicex pack to build the package, qomicex verify --package to check the signature, and qomicex publish to publish in one step. See the CLI reference for the full command set.
1. Package format
A.qplugin is really a ZIP archive, and manifest.json must sit at the root.
- If
manifest.jsonis not at the root → installation fails (Invalid plugin package) - The backend does not validate the file extension, so any valid zip passes the upload endpoint; still, please always use
.qpluginso packages are recognizable - Avoid
../relative paths inside the package (zip-slip risk, and files may be written outside the intended location) - Entry paths inside the package must use forward slashes
/(a\produced by older Windows tools is rejected by the security checks)
Packaging a WASM plugin (L3)
Plugins with L3 capabilities must shipplugin.wasm (a wasmtime core module) inside the package:
- Build:
rustup target add wasm32-unknown-unknown && cargo build --release --target wasm32-unknown-unknown - Rename the build artifact to
plugin.wasmand put it in the package root (the gateway loads the fixed file nameplugin.wasm, so you do not declare a path in the manifest) - The manifest
layersmust include"l3", andpermissionsmust include"wasm:execute" - See WASM plugins (L3) for details
Theme content (optional publishable assets)
Themes, icons, and fonts are optional publishable content. Declare them throughcontributes; they do not need a separate package format:
All three ship as resources inside the
.qplugin package, so there is nothing extra to package. See Theme system for details.
2. Naming conventions
3. Permission and security guidelines
- Principle of least privilege: declare only the permissions you actually use. The install details page groups them by risk (dangerous permissions such as
shell:execute,filesystem:write, andplugin:installare highlighted) - Never hard-code secrets or credentials inside a plugin
- Reach the internet through
proxyFetch(built-in SSRF protection) instead of making direct intranet requests - Plugin scripts run in an iframe sandbox (opaque origin) by default, so do not rely on globals to share state across plugins
- Move heavy computation off the UI main thread (Web Worker / WASM / backend proxy).
qomicex verifywarns statically aboutwhile(true)and unboundedsetIntervalloops
4. Plugin signing (Ed25519 three-level trust chain)
The store and the launcher use an Ed25519 three-level trust chain (ADR-050) to prevent supply-chain poisoning:- Trust chain: the store root key issues the developer public key (
signature.cert.json, which contains the developer public key plus the root key signature) → the developer signs the package with the private key (signature.json). The launcher ships the store root public key, so it can verify signatures fully offline. - Signature format:
signature.json(in the package root):
signedHashis the SHA-256 of the canonicalized manifest plus file list:SHA-256(canonical JSON of { manifest: sha256(raw bytes of manifest.json), files: [{path, sha256}] }). Canonicalization means keys sorted recursively with no whitespace in the JSON, which keeps the hash reproducible.- Where verification is enforced: new versions uploaded to the store and
.qpluginfiles manually uploaded to the launcher are always verified (a missing signature or failed verification returns 422signature_invalid/ rejects the install). Store installs and local development directory installs are not enforced (older versions stay compatible and the development path is allowed through). - Backward compatibility: older releases without signatures can still be installed and do not have to be re-signed. If a private key is rotated or leaked, generate a new key pair and upload the new public key; old certificates stop working automatically.
- Rotating the root key: after the store changes its signing root key, the embedded root public key must be updated everywhere (
STORE_ROOT_PUBLIC_KEY_B64in the CLI’ssignature.ts,ROOT_PUBLIC_KEY_B64in the launcher’splugin_signature.rs, and the CLI docs insigning.md). Packages signed under old certificates then fail verification and must be republished withqomicex publishto get a new certificate.
Signing tools
- Generate a key pair:
openssl genpkey -algorithm Ed25519 -out dev-key.pem, ornode scripts/plugin-keygen.mjs(repository script, no subcommands; optional flags--out <path>,--json,--force, and it writes toplugin-key.jsonby default) - Sign:
qomicex pack --key ./dev-key.pem(builds a signed.qplugin), orqomicex publishto run the whole chain (device-flow login → upload the public key and get a certificate → sign → upload) - Verify:
qomicex verify --package ./release/xxx.qplugin
Local development signing (no store certificate)
Uploading to the store requires a developer certificate issued by the store root key. During development, when no certificate is available, you can run the full three-level verification chain with a local root key:- Generate a local root key pair plus a developer key pair (
openssl/plugin-keygen.mjsas above), have the local root private key issue the developer certificate (signature.cert.json, same structure as a store certificate), then sign the package body with the developer private key (signature.json). - Launcher side: run a debug build (
pnpm tauri dev) with the environment variableQOMICEX_DEV_ROOT_PUBLIC_KEY=<local root public key base64>— the launcher then verifies the certificate against the local root public key. Release builds do not compile this override in; they always use the bundled store root key, so the production trust chain is unaffected. qomicex verify --packagereadsQOMICEX_DEV_ROOT_PUBLIC_KEYtoo, so locally signed packages can be re-verified offline.
qomicex publish to obtain a store certificate.
⚠️ The environment variable must be set on the backend process. In dev mode the Tauri shell does not spawn the backend sidecar (the startup log shows [backend] placeholder (0 bytes), skipping); the frontend talks directly to the axum backend on :5000, which is started by pnpm dev:backend in a separate terminal. Setting the variable on the pnpm tauri dev terminal has no effect:
[plugin-signature] line in the backend trace for the root key in effect — “local development override” means the variable took effect, “bundled store root key” means the backend process never received it.
5. Installation methods
-
Upload a .qplugin file: Settings → Plugins → Install plugin, then pick a
.qplugin/.zipfile (signature verification is enforced; a missing signature or a failed check is rejected) - Install from the store: plugin store page → Install (verified when signed; older unsigned versions are allowed through)
-
Directory install (developer debugging): backend
POST /api/plugins/installwith body{"sourceDir": "/path/to/dir"}pointing straight at a directory containingmanifest.json(signature not enforced; theplugins-dev/development path) - The upload endpoint does not check dependencies; dependency checks run only for directory install (install) and the store install endpoint. Before publishing, make sure dependent plugins are documented along with the plugin.
- Enabling or disabling a plugin (state changes) takes effect only after you restart the launcher.
6. Updates, upgrades, and rollback
- Update checks: after startup the launcher silently polls the store
POST /plugins/check-updates(with the launcher version and installed plugin list) and shows an upgrade button on the plugin management page when an update exists. - Staged rollout (not implemented): the store does not expose a rollout column yet, so
rolloutPercentalways returns100(full rollout). If it is implemented later, the launcher will usehash(slug@latestVersion) % 100 < rolloutPercentto decide whether a user sees the upgrade prompt, with a stable result for the same user. - Automatic telemetry pause: the store applies staged-rollout protection per version based on 24h error report counts — once a version exceeds the threshold (default 10) in 24h, the store stops offering it as an update (installed users are unaffected). See Staged rollout telemetry for details.
- Upgrade flow: click the upgrade button → confirm → the store install pipeline runs (download → SHA-256 check → signature verification → overwrite install).
- Rollback snapshot: when overwriting, the package is first extracted to the temporary directory
.{id}.tmp-{uuid}, then the old directory is renamed toplugins/{id}.bak-{oldVersion}, and finally the new directory is moved into place with an atomic rename (fresh installs produce no snapshot). If a plugin misbehaves after upgrading, click Rollback to restore it (POST /api/plugins/{id}/rollback).PluginInfo.hasRollbackindicates whether a usable snapshot exists. - Uninstall: Settings → Plugins → delete button (removes the whole
plugins/{id}/directory).
7. Distribution channels
The early “GitHub repository + PR” distribution model has been replaced by the online store. Official publishing always goes through plugins.qomicex.top.1. Local dev install (self-testing)
While developing, upload the.qplugin to the launcher to verify it, or use qomicex dev with the debug harness (see Debugging and hot reload).
2. Store publishing (official channel)
- Automatic publishing: packages that are pure L3 with no dangerous permissions are published automatically after upload, with no manual review.
- Manual review: everything else (anything with a UI layer or dangerous permissions) goes into the review queue; see the version list for results.
- To update a plugin, upload a new version under the same slug; the store deduplicates versions (409
version_exists).
3. Self-hosted mirror / private registry (Open Registry Protocol)
If you want a private plugin source for yourself or your team, the Open Registry Protocol (see Open Registry Protocol in the store sidebar) lets third parties implement compatible mirrors:- Core endpoints: implement just
GET /registry,GET /plugins,GET /plugins/:slug, andGET /plugins/:slug/versions/:version/downloadand the launcher recognizes your source - Two download channels: when a mirror re-serves packages it must preserve
signature.jsonandsignature.cert.jsonbyte for byte; do not repackage - Discovery: the launcher’s
registryUrlsetting accepts a list of sources and falls back automatically when the primary source is unreachable - Deployment options: copy a Cloudflare Worker (shared load), pure static hosting (read-only), or a full self-built registry (independent source)
8. Data and directories
Plugins can read and write their own data (isolated per plugin id):- Windows:
%LOCALAPPDATA%\qomicex-launcher - Linux:
$XDG_DATA_HOMEor~/.local/share - macOS:
~/Library/Application Support - The
QOMICEX_HOMEenvironment variable overrides all of them (portable mode)
9. Publishing checklist
-
manifest.jsonis in the package root and the JSON is valid -
idis unique and will not change, andversionincreases semantically (useqomicex bump patch|minor|major) -
permissionscontains only what is needed (check minimality withqomicex verify) -
dependencieson other plugins is declared correctly (required/optional, version range) -
entry.frontendis declared (otherwise the plugin is never activated) - Page and overlay scripts do not use globals for cross-plugin communication
- No
while(true)or unboundedsetIntervalon the main thread (both areqomicex verifywarnings) - The package is signed with
qomicex pack --keyandsignature.jsonis in the package root - Local install test passes (upload → enable → restart → verify features)
-
qomicex verify --packagepasses - If you call other plugins, your docs name the required library plugin and its version
- If you contribute icon or font themes, they are declared in
contributes.iconTheme/contributes.fontLinks(see Theme system) - Watch the error rate after publishing (the telemetry pause threshold defaults to 10/24h, see Staged rollout telemetry)
10. Version compatibility guidance
minLauncherVersionmust be filled in: the store filters incompatible versions by semver inPOST /plugins/check-updatesandGET /plugins?minLauncherVersion=, and the CLI (qomicex verify) reports an error when the field is missing; only the launcher install path does not validate it currently- The plugin API may evolve, so state the supported launcher version range when you publish
- For major changes (method signatures, permission changes), bump the major version