> ## Documentation Index
> Fetch the complete documentation index at: https://docs.launchmystore.io/llms.txt
> Use this file to discover all available pages before exploring further.

# App Versioning & Rollback

> Ship updates safely with semver, draft/publish flow, and per-installation rollback

# App Versioning & Rollback

LaunchMyStore tracks every release of your app as an immutable **version** record. Merchants install a version, not just an app — which means a bad release can be **rolled back per-merchant** without you re-publishing fixed code, and merchants can choose between **auto-update** (always run the latest) or **pinning** (stay on a known-good version).

This page walks through the full lifecycle: create → publish → deprecate → rollback.

## Why Versioning Matters

<CardGroup cols={2}>
  <Card title="Safe rollouts" icon="shield-check">
    Publish a version. If reports come in, deprecate it — auto-update merchants stay where they are; new installs get the previous version.
  </Card>

  <Card title="Per-merchant rollback" icon="rotate-left">
    A single merchant hitting an edge case can pin themselves to v1.4.2 while the rest of the world runs v1.5.0.
  </Card>

  <Card title="Audit trail" icon="clipboard-list">
    Every publish, deprecate, and rollback is recorded with actor + timestamp in the version changelog.
  </Card>

  <Card title="Predictable behavior" icon="lock">
    Once a version is published, its `extensions`, `functions`, and `wasmPaths` are frozen. Editing them creates a new version.
  </Card>
</CardGroup>

## Version Lifecycle

```
   ┌─────────┐   publish    ┌────────────┐   deprecate   ┌─────────────┐
   │  draft  │ ───────────▶ │ published  │ ────────────▶ │ deprecated  │
   └─────────┘              └────────────┘               └─────────────┘
        ▲                         │
        │                         │  (auto when newer version published)
        └─────────────────────────┘
```

| Status         | Meaning                          | Installable?                   | Visible in Marketplace?     |
| -------------- | -------------------------------- | ------------------------------ | --------------------------- |
| **draft**      | Created but not yet released     | Yes — owning dev store only    | No                          |
| **published**  | Current live version             | Yes (default for new installs) | Yes                         |
| **deprecated** | Previously published, superseded | Yes (rollback target)          | No (existing installs only) |

A version can never go backwards in the lifecycle — once deprecated, it cannot become `published` again. Cut a new version instead.

## Semver Requirements

Every version string must be valid [semver](https://semver.org):

* `1.0.0` ✅
* `2.3.1-beta.4` ✅
* `1.0` ❌ (missing patch)
* `v1.0.0` ❌ (no leading `v`)

**The new version must be strictly greater than the latest published version.** This is enforced server-side via `semver.gt()`.

```
Latest published: 1.4.2
✅ Allowed: 1.4.3, 1.5.0, 2.0.0
❌ Rejected: 1.4.2, 1.4.1, 1.0.0
```

Pre-release tags (`-beta`, `-rc.1`) are allowed and follow standard semver precedence: `1.5.0-beta.1 < 1.5.0-rc.1 < 1.5.0`.

## Create a Draft Version

```bash theme={null}
POST /apps/developer/:appId/versions
Authorization: Bearer <developer-jwt>
```

```json theme={null}
{
  "version": "1.5.0",
  "releaseNotes": "Adds dark mode + Czech translations.",
  "extensions": { /* extension manifest snapshot — optional */ },
  "functions":  { /* function manifest snapshot — optional */ }
}
```

If `extensions` / `functions` are omitted, the API snapshots the app's current values. **The snapshot is immutable** — re-installing the app will not push fresh extension code into a published version. Cut a new version instead.

**Response (201):**

```json theme={null}
{
  "id": "ver_01HZ...",
  "appId": "app_01H...",
  "version": "1.5.0",
  "status": "draft",
  "releaseNotes": "Adds dark mode + Czech translations.",
  "createdAt": "2026-05-06T12:00:00.000Z",
  "createdBy": "dev_01H..."
}
```

## Publish a Version

```bash theme={null}
POST /apps/developer/:appId/versions/:version/publish
Authorization: Bearer <developer-jwt>
```

What happens server-side:

1. Validates the target version exists and is in `draft` status
2. **Marks the previous published version as `deprecated`** (`deprecated_at` = now)
3. Marks the target version `published` (`published_at` = now)
4. Updates the parent app's `version` column to the new version string
5. **Cascades to installations**: every installation with auto-update enabled gets its installed version bumped to the new version
6. Records a `published` entry in the version changelog

Pinned installations (auto-update disabled) are left untouched.

<Warning>
  Publishing is not reversible. If you publish a broken version, you cannot un-publish it — the only path forward is to publish a new version that fixes the bug, or deprecate it (which removes it from new installs but keeps existing installs running it).
</Warning>

## Deprecate a Version

```bash theme={null}
POST /apps/developer/:appId/versions/:version/deprecate
Authorization: Bearer <developer-jwt>
```

Sets the version to `deprecated` and stamps `deprecated_at`. **Existing installations are not migrated** — merchants pinned to it stay there. New installs and auto-update merchants will fall back to the most recent non-deprecated version.

Use this when:

* A version has a critical bug and you've already published a fix
* You want to remove a version from the marketplace listing without forcing migrations

## List Versions

```bash theme={null}
GET /apps/developer/:appId/versions
Authorization: Bearer <developer-jwt>
```

Returns all versions (drafts + published + deprecated), newest first.

## Version Stats

```bash theme={null}
GET /apps/developer/:appId/versions/stats
Authorization: Bearer <developer-jwt>
```

Returns install-count breakdown per version — useful for deciding when it's safe to deprecate an older version.

```json theme={null}
[
  { "version": "1.5.0", "status": "published",  "publishedAt": "...", "installCount": 1240 },
  { "version": "1.4.2", "status": "deprecated", "publishedAt": "...", "installCount": 86 },
  { "version": "1.4.1", "status": "deprecated", "publishedAt": "...", "installCount": 3 }
]
```

## Per-Installation Rollback (Merchant Side)

A merchant who hits a bug in `1.5.0` can roll their store back to `1.4.2` without affecting any other merchant.

```bash theme={null}
POST /apps/store/installations/:installationId/rollback
Authorization: Bearer <merchant-jwt>
```

```json theme={null}
{ "targetVersion": "1.4.2" }
```

What happens:

1. Verifies the target version exists and is `published` or `deprecated` (cannot rollback to `draft`)
2. Updates the installation:
   * `installedVersion = '1.4.2'`
   * `pinnedVersion = '1.4.2'`
   * `autoUpdate = false`  ← key side effect

The merchant is now **pinned**: future publishes will not touch them.

### Resume Auto-Update

When the merchant is ready to re-join the auto-update channel:

```bash theme={null}
POST /apps/store/installations/:installationId/resume-auto-update
Authorization: Bearer <merchant-jwt>
```

This:

* Sets `auto_update = true`
* Clears `pinned_version`
* Updates `installed_version` to the current `published` version

## Auto-Update vs Pinned

| Setting                        | When publishes happen                                         | Use case                                                                             |
| ------------------------------ | ------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `auto_update = true` (default) | Installation jumps to new version immediately                 | Most merchants — they want bug fixes & features                                      |
| `auto_update = false` (pinned) | Installation stays on `pinned_version` until manually changed | Enterprise merchants on a change-control schedule, or merchants who hit a regression |

Pinning is **always** a per-installation decision — there is no app-wide "freeze auto-updates" setting.

## Snapshotting What Goes In a Version

A version captures three things at publish time:

1. **`extensions`** — JSON manifest of every block, snippet, checkout extension, post-purchase extension, and admin extension shipped by the app
2. **`functions`** — JSON manifest of every function (cart\_transform, shipping\_rate, etc.), including their input schema and target
3. **`wasmPaths`** — R2 storage paths for compiled WASM modules

Once published, these are read-only. The renderer and function-runner load extension/WASM bytes by version — meaning two merchants on different versions of the same app run different code paths simultaneously.

## Breaking Changes & Migrations

If a new version requires merchants to do something (re-grant scopes, configure new settings, etc.):

1. **Bump the major version** (`1.x.x` → `2.0.0`) so the change is visible
2. Document the breaking change in `releaseNotes`
3. Consider keeping the previous major version `published` for 30+ days so merchants have time to migrate
4. Track uptake by polling the installations list or the [version stats](#version-stats) endpoint — there is no publish webhook, so detect which installations have picked up the new version by their `installedVersion`

## CLI Workflow

The `lms` CLI wraps the version endpoints. Run any of these from inside an app project — `--app` is inferred from `.lmsrc.json` and only needs to be passed explicitly when you're outside the project directory.

<CodeGroup>
  ```bash Create + publish theme={null}
  lms app version create --version 1.5.0 --notes "Adds dark mode"
  lms app version publish 1.5.0
  ```

  ```bash Deploy (one-shot) theme={null}
  # Wraps `version create` + `version publish` in a single step.
  # Use this once you've finished testing and you're ready to ship.
  lms app deploy --version 1.5.0 --notes-file CHANGELOG.md

  # Cut a draft without publishing (equivalent to `version create`):
  lms app deploy --version 1.6.0-beta.1 --no-publish
  ```

  ```bash List theme={null}
  lms app version list
  ```

  ```bash Deprecate theme={null}
  lms app version deprecate 1.4.0
  ```

  ```bash Stats theme={null}
  lms app version stats
  ```
</CodeGroup>

### Merchant-side rollback

These commands act on a specific **installation** (a merchant's install of your app), not on the app itself. Use them when a merchant reports a regression or wants to opt back into auto-updates.

<CodeGroup>
  ```bash Rollback an install theme={null}
  # Pin installation `inst_01HZ...` to v1.4.2.
  # Side effect: sets auto_update = false on that installation.
  lms app rollback inst_01HZ... 1.4.2
  ```

  ```bash Resume auto-update theme={null}
  # Un-pin the installation and jump it to the current published version.
  lms app resume-auto-update inst_01HZ...
  ```
</CodeGroup>

`lms app rollback` calls `POST /apps/store/installations/:id/rollback` and `lms app resume-auto-update` calls `POST /apps/store/installations/:id/resume-auto-update` — see [Per-Installation Rollback](#per-installation-rollback-merchant-side) above for the server-side behavior.

## Best Practices

<AccordionGroup>
  <Accordion title="Test before you cut a version, not after">
    Drafts are **records, not installable artifacts** — the install path requires a `published` or `deprecated` version, so there's no "install this draft on a test store" shortcut. Instead, test before you bump the version:

    * Iterate locally with `lms app dev` (live-reloads extensions into a dev store) and `lms function test` (runs declarative functions against fixture inputs).
    * Install your app on a developer-owned test store from a previous published version, then point that install at your in-progress code via the dev tunnel.
    * Only run `lms app deploy` (or `version create` + `version publish`) once the change is green on the test store.

    A published version is permanent — you cannot un-publish, only deprecate or supersede.
  </Accordion>

  <Accordion title="Keep at least one prior published version available">
    Don't deprecate the previous version the same minute you publish a new one. Wait 24-48 hours so any auto-update merchants who hit a bug have a known-good rollback target.
  </Accordion>

  <Accordion title="Use semver to communicate intent">
    PATCH (1.4.2 → 1.4.3) for bug fixes. MINOR (1.4.0 → 1.5.0) for backwards-compatible features. MAJOR (1.x.x → 2.0.0) for breaking changes that need merchant action.
  </Accordion>

  <Accordion title="Write release notes for humans, not machines">
    `releaseNotes` is shown to merchants in the dashboard before they auto-update. "Fixed bug in cart" is useless. "Fixed: cart total was off by \$0.01 when discount + tax both applied" is useful.
  </Accordion>

  <Accordion title="Keep extension code in your app repo, not in the version snapshot">
    Treat the version snapshot as a build artifact. Your source-of-truth is your git repo + CI pipeline that calls `lms app version create` on every release tag.
  </Accordion>
</AccordionGroup>

## Related

* [App Lifecycle](/getting-started/app-lifecycle) — install/uninstall events
* [CLI Setup](/getting-started/cli-setup) — `lms app version *` commands
* [Webhooks](/api-reference/webhooks/overview) — delivery guarantees + the topics your app can subscribe to
