Skip to main content

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 the qomicex 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.json is 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 .qplugin so 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 ship plugin.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.wasm and put it in the package root (the gateway loads the fixed file name plugin.wasm, so you do not declare a path in the manifest)
  • The manifest layers must include "l3", and permissions must include "wasm:execute"
  • See WASM plugins (L3) for details

Theme content (optional publishable assets)

Themes, icons, and fonts are optional publishable content. Declare them through contributes; 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, and plugin:install are 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 verify warns statically about while(true) and unbounded setInterval loops

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):
  • signedHash is 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 .qplugin files manually uploaded to the launcher are always verified (a missing signature or failed verification returns 422 signature_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_B64 in the CLI’s signature.ts, ROOT_PUBLIC_KEY_B64 in the launcher’s plugin_signature.rs, and the CLI docs in signing.md). Packages signed under old certificates then fail verification and must be republished with qomicex publish to get a new certificate.

Signing tools

  • Generate a key pair: openssl genpkey -algorithm Ed25519 -out dev-key.pem, or node scripts/plugin-keygen.mjs (repository script, no subcommands; optional flags --out <path>, --json, --force, and it writes to plugin-key.json by default)
  • Sign: qomicex pack --key ./dev-key.pem (builds a signed .qplugin), or qomicex publish to 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:
  1. Generate a local root key pair plus a developer key pair (openssl / plugin-keygen.mjs as 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).
  2. Launcher side: run a debug build (pnpm tauri dev) with the environment variable QOMICEX_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.
  3. qomicex verify --package reads QOMICEX_DEV_ROOT_PUBLIC_KEY too, so locally signed packages can be re-verified offline.
Any single-byte change to the package content causes rejection with a signature hash mismatch; official publishing must still go through 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:
The root public key is cached per process (fixed at the first verification): after the backend has started, changing the variable requires restarting the backend. When troubleshooting “signature certificate invalid (root key verification failed)”, check the [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 / .zip file (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/install with body {"sourceDir": "/path/to/dir"} pointing straight at a directory containing manifest.json (signature not enforced; the plugins-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 rolloutPercent always returns 100 (full rollout). If it is implemented later, the launcher will use hash(slug@latestVersion) % 100 < rolloutPercent to 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 to plugins/{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.hasRollback indicates 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, and GET /plugins/:slug/versions/:version/download and the launcher recognizes your source
  • Two download channels: when a mirror re-serves packages it must preserve signature.json and signature.cert.json byte for byte; do not repackage
  • Discovery: the launcher’s registryUrl setting 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)
See Open Registry Protocol for the full implementation guide.

8. Data and directories

Plugins can read and write their own data (isolated per plugin id):
Data directory locations:
  • Windows: %LOCALAPPDATA%\qomicex-launcher
  • Linux: $XDG_DATA_HOME or ~/.local/share
  • macOS: ~/Library/Application Support
  • The QOMICEX_HOME environment variable overrides all of them (portable mode)

9. Publishing checklist

  • manifest.json is in the package root and the JSON is valid
  • id is unique and will not change, and version increases semantically (use qomicex bump patch|minor|major)
  • permissions contains only what is needed (check minimality with qomicex verify)
  • dependencies on other plugins is declared correctly (required/optional, version range)
  • entry.frontend is declared (otherwise the plugin is never activated)
  • Page and overlay scripts do not use globals for cross-plugin communication
  • No while(true) or unbounded setInterval on the main thread (both are qomicex verify warnings)
  • The package is signed with qomicex pack --key and signature.json is in the package root
  • Local install test passes (upload → enable → restart → verify features)
  • qomicex verify --package passes
  • 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

  • minLauncherVersion must be filled in: the store filters incompatible versions by semver in POST /plugins/check-updates and GET /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