# Publishing (/extensions/publishing)



The store keeps one promise: &#x2A;*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 [#submitting]

<Steps>
  <Step>
    ### Put the source in a public repository [#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](/extensions/windows#what-the-page-is).
  </Step>

  <Step>
    ### Open a pull request against the store [#open-a-pull-request-against-the-store]

    The store lives at
    [github.com/thiennguyen93/lumi-store](https://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`:

    ```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
    ```
  </Step>

  <Step>
    ### Review [#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.
  </Step>

  <Step>
    ### Merge [#merge]

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

  <Step>
    ### How people find it [#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 &#x2A;*Continue…** when the extension ships
    [an installer of its own](/extensions/windows#an-installer-of-your-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`](/extensions/manifest#extension). So is a
    demo video, [`video`](/extensions/manifest#extension) — 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`.
  </Step>
</Steps>

## Release notes [#release-notes]

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

```md
# 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]`](/extensions/manifest#tabs).

## Updates [#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*&#x2A; 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](/extensions/windows#an-installer-of-your-own) is not shown again.
If your extension is switched on, it is told with
[`Updated`](/extensions/sdk#on_lifecycleevent), 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.
