Lumi
Extensions

Publishing

How an extension gets into the store — source in, signed package out.

The store keeps one promise: the reviewed source is the shipped package. You submit source, pinned to a commit; the store's own build compiles it, packages it, signs it and publishes it. No binary you built is ever published, so nothing can be swapped after review.

That is also why a package you build yourself installs as not signed: only the store's build signs, and Lumi says so rather than pretending otherwise. (A package downloaded from the store and installed from a file keeps its signature if its .sig file sits beside it, as <package>.sig.)

Submitting

Put the source in a public repository

A Rust crate depending on lumi-extension-api, with manifest.toml beside its Cargo.toml. Anything under ui/ is packed exactly as it is in the repository, so commit it as source — see Building for the store.

Open a pull request against the store

The store lives at github.com/thiennguyen93/lumi-store. The pull request:

  • adds your repository as a submodule under ext/<your-extension-id>, pinned to the exact commit you are submitting, and
  • adds one entry to extensions.toml:
[[extension]]
id = "dev.you.thing"       # must equal the id in your manifest.toml
path = "ext/dev.you.thing" # the submodule
subdir = "."               # where Cargo.toml and manifest.toml live inside it

Review

Review is by a person, of the source: the capabilities the manifest declares against what the code actually calls, nothing phoning home, and no misbehaviour shipped as a feature. The pull request's checks also hold the manifest to the rules Lumi's installer applies, so a manifest Lumi would refuse fails there rather than on somebody's Mac.

Merge

Merging is publishing: the store rebuilds every listed extension from source, signs the packages and rewrites the index Lumi reads.

How people find it

Settings → Extensions is the Extension Store. Its Discover tab lists every extension in the store's index, with the featured ones turning over at the top and a filter by category; each has a page of its own — your screenshots, what it adds, what it can reach, the package — with Install on it. The store's page on lumikeys.app lists the same catalogue for people outside Lumi, and its Install button hands the extension to Lumi the same way.

Either way Lumi downloads the signed package and shows the same review sheet a file install gets — this time saying the package came from the store and was verified against its key. What the store page says is browse copy; the sheet reads the verified package. Nothing installs until the person presses the sheet's button — Install, or Continue… when the extension ships an installer of its own.

Two words about your extension are the store's, set on its entry in extensions.toml in review: category (Productivity, Writing, Windows, Design, Developer or Utilities), which is the shelf it is filtered under, and featured = true, which puts it among the plates turning over at the top of Discover. Neither is a manifest key, because neither is yours to claim. Pictures for the page are: see screenshots. So is a demo video, video — a YouTube link, drawn ahead of the pictures.

So are the page's words. Put a STORE.md beside your manifest and the page shows it in place of your one-line description. Lumi draws a small part of markdown: # headings, paragraphs, - and 1. lists, bold, italic, inline and fenced code, ---, and links that start with https://, which open in the browser. Anything else, HTML included, is shown as the characters it is. UTF-8, at most 16 KB. It is published named by its content, so an edit reaches the page without a new version. Needs nothing of Lumi's installer, so it needs no min-lumi-version; a Lumi older than STORE.md shows the description.

Release notes

Every release needs notes: a CHANGELOG.md beside your manifest, one ## <version> section per release, newest on top.

# Changelog

## [Unreleased]

## [1.2.0] - 2026-09-30
### Added
- Pin the panel so it stays open while you paste.
### Fixed
- Search keeps text cased as it was copied.

The newest section must be the version your manifest ships. The headings Keep a Changelog, release-please, git-cliff and changesets write are all read (## 1.2.0, ## v1.2.0, ## [1.2.0] - 2026-09-30); the date is optional. Versions go newest first, each once, and a section holds at most 4 KB of the same small markdown a store page does. Anything above the first ## and an Unreleased section are left out.

People read them in two places: the What's new fold on your store page, before they install, and the Changelog tab on your extension's page in Lumi after — read from the copy in the package they installed, so it says what they have, offline included. The store packs the file for you; a package you build yourself carries it the same way. Rename the tab, hide it or draw it yourself with [tabs.changelog].

Updates

An update is a pull request that moves your submodule to a new commit and raises version in the manifest. The id never changes — it is how Lumi knows the new package replaces the old one.

Raise the version everywhere it is written, in the same commit: the manifest's is the one Lumi reads, and the store refuses a pull request while your crate's version in Cargo.toml (a version.workspace = true included), its row in a committed Cargo.lock, or the version in your web/package.json names another number. After editing Cargo.toml, cargo update -p <your crate> --offline brings the lock along.

Lumi reads the store's index at launch, when its window comes forward, and once an hour in the background, at most every six hours — and whenever the person presses Check for updates on the Installed tab of Settings → Extensions, or ⌘R anywhere in that pane. The Updates tab lists what it found, with Update all. An update is offered when the store's version is higher than the installed one, compared as numbers, so 1.10.0 is newer than 1.9.0. Both must be written major.minor.patch; a version of any other shape is never offered as an update. An update whose min-lumi-version is above the running Lumi is listed as needing a newer Lumi.

An update keeps the person's settings, and your installer is not shown again. If your extension is switched on, it is told with Updated, carrying the version it replaced.

Keep capabilities as narrow in an update as in the first release: a new capability is a new line on the review sheet.

On this page