> For the complete documentation index, see [llms.txt](https://basedapp.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://basedapp.gitbook.io/docs/integrations/mini-apps-platform/09-distribution-and-updates.md).

# Distribution & updates

How a mini-app gets from a partner's machine to a user's device, how it's updated, how we revoke it, and how it behaves offline.

## Bundle format

A bundle is a zip file with a fixed structure:

```
my-mini-app-1.4.2.basedapp/
├── based.json            # manifest (required)
├── main.js               # entry script (required, compiled from TS)
├── locales/
│   ├── en.json
│   └── ja.json
├── assets/
│   ├── icon.png
│   └── splash.png
└── BASED-SIG/
    ├── manifest.sha256   # SHA-256 of manifest, hex
    ├── content.sha256    # SHA-256 of canonical content tree, hex
    └── signature         # Ed25519 signature over content.sha256
```

Source maps are **not** part of the public bundle. They are uploaded to the platform side-channel during publish, kept private, and used only to symbolicate errors in partner dashboards. The CDN never serves `.map` files.

* **Maximum size:** 4MB compressed, 16MB uncompressed.
* **Compression:** standard zip with deflate; no external compression.
* **File names:** ASCII only; lowercase recommended; no symlinks.
* **Asset manifest:** all asset paths referenced from the manifest must resolve to a real file in the bundle.

The bundle is referred to with a `.basedapp` extension. It's still a zip — `unzip foo.basedapp` works for inspection.

### Canonicalization

To make signatures stable across compressors, we compute the **content hash** over a canonical representation:

1. Sort all paths lexicographically.
2. For each path, append `<path>\n<sha256-of-bytes-as-hex>\n` to a buffer.
3. The content hash is `sha256(buffer)`.

The signature is over this content hash, not the zip bytes themselves. Re-zipping with a different compressor doesn't invalidate the signature.

## Build pipeline (partner-side)

```mermaid
flowchart LR
    SRC["Source code (TS/JS)"]
    MF["based.json"]
    AS["locales/, assets/"]
    CLI["based-cli build<br/>(esbuild + checks)"]
    OUT["unsigned .basedapp"]
    SRC --> CLI
    MF --> CLI
    AS --> CLI
    CLI --> OUT
```

What `based-cli build` does:

1. Refuses to build if the project has no `tsconfig.json` or has runtime `.js` source (TypeScript is required — see `10-developer-experience.md`).
2. Runs TS type-check against `@based/sdk-types`.
3. Bundles via esbuild from `.ts`/`.tsx`: target ES2020, no externals, no `eval` references.
4. Generates source maps (kept private — uploaded side-channel during publish, never served from CDN).
5. Static checks: forbidden globals, bundle size cap, manifest schema.
6. Lints: missing accessibility labels, unhandled `Promise` rejections, suspect `console.log` in prod build.
7. Writes the zipped `.basedapp` (unsigned).

Partners then upload to the platform CI for review and signing.

## Sign and publish (platform-side)

```mermaid
flowchart LR
    UP["Partner upload"]
    CI["Platform CI"]
    SG["Sign"]
    CDN["Publish to CDN"]
    CHK["static checks (re-run)<br/>malware scan<br/>manifest review (manual for first-time)<br/>approval queue"]
    UP --> CI
    CI --> CHK
    CI --> SG
    SG --> CDN
```

Platform CI:

1. Re-runs all static checks.
2. Runs a malware/virustotal-style scan.
3. For first releases, a human review is required (platform team SLA).
4. For subsequent versions, automated checks plus a diff review for material changes (new permissions, new domains, etc.).
5. On approval, the bundle is signed: platform's Ed25519 private key signs the canonical content hash. Signature is written to `BASED-SIG/signature` and a re-zipped `.basedapp` is produced.
6. Bundle is uploaded to CDN at `https://miniapps.based.app/v1/bundles/<id>/<version>.basedapp`.
7. Manifest entry added to the registry index.

## Registry index

The registry is the catalog the host queries to discover, install, and update mini-apps.

```
GET https://miniapps.based.app/v1/registry?since=<timestamp>
```

Response:

```json
{
  "asOf": "2026-04-26T12:00:00Z",
  "entries": [
    {
      "id": "com.partner.cool-yield",
      "version": "1.4.2",
      "minHostVersion": "2.5.0",
      "size": 482910,
      "sha256": "…",
      "url": "https://miniapps.based.app/v1/bundles/com.partner.cool-yield/1.4.2.basedapp",
      "manifest": { … inline summary … },
      "channels": ["stable"],
      "rollout": { "percent": 100, "tenants": ["based","hyena"] }
    }
  ]
}
```

Entries are immutable. A new version → new entry. Removal is via the kill list, not by deleting the entry.

The registry is the **source of truth** for what's available; the bundle URL is just where to fetch.

## Channels

Three channels exist:

* `stable` — what real users get.
* `beta` — opt-in via developer setting in host; partner can ship to beta first.
* `internal` — first-party only, used by host engineering teams pre-release.

A bundle can be published to multiple channels at different times (typical: beta → stable after 1 week of beta).

## Staged rollout

A registry entry's `rollout` field controls who receives a version:

```json
"rollout": {
  "percent": 25,
  "tenants": ["based"],
  "platforms": ["ios"],
  "minHostVersion": "2.6.0"
}
```

The host computes a stable hash of `(installId, miniAppId)` mod 100 and compares to `percent`. This gives consistent in/out membership without server cooperation; a user is either on the new version or not, deterministically.

Rollouts can be paused (`percent: 0`) to halt distribution while leaving the entry intact; existing installs of the version keep working.

## Install model

Two install patterns are supported:

| Pattern        | Trigger                                     | UX                                                                               |
| -------------- | ------------------------------------------- | -------------------------------------------------------------------------------- |
| **On-demand**  | First time the user navigates to a mini-app | Loading screen with progress; bundle fetched, verified, cached.                  |
| **Pre-cached** | First-party "featured" mini-apps            | Background-fetched on app start; user sees a prompt list of installed mini-apps. |

In both cases, the cache stores the verified bundle keyed by `(id, version)`.

## Update model

Updates are **fetch-at-foreground, apply-at-cold-boot**. Specifically:

1. On host foreground, the host polls the registry (with cache and exponential backoff: 5 min → 1 hr).
2. For each installed mini-app, if a newer version exists in the appropriate channel and the user is in the rollout cohort:
   * The new bundle is fetched in the background.
   * It is verified (signature + manifest).
   * It is staged in the cache as `pending`.
3. On the next cold boot of that mini-app, the pending version is promoted; the previous version is kept for one more boot for fast rollback.
4. After successful boot of the new version, the old one is deletable.

We deliberately do **not** apply updates mid-session. That avoids:

* The mini-app's state machine being inconsistent across versions.
* Surprising the user with a UI change while they're in the middle of a flow.

## Rollback

If the new version fails to boot (manifest validation, signature mismatch, JS exception during boot, host crash within first 5s), the host:

1. Marks the new version as `quarantined` for this device.
2. Reverts to the previous cached version automatically.
3. Reports a `boot_failed` telemetry event.
4. After 3 quarantines from different installs, the platform team is paged.

Manual rollback is also possible via the registry: re-publish the previous version with a higher version number, or use the kill list to invalidate the bad version.

## Kill switch

The kill list (described in `08-security-and-sandbox.md`) is the platform's emergency lever:

* Endpoint: `GET https://miniapps.based.app/v1/killlist`
* Polled on host foreground (\~5 min cache, but cache-bypassed on suspected compromise).
* Each entry: `{ id, fromVersion, reason, scope: "tenant" | "all" }`.
* A killed mini-app: refuses to launch, shows a host-rendered notice, optionally clears its storage.

Kill is reversible — removing the entry from the list lets the mini-app run again on next poll. For irrevocable removal (legal, severe abuse), publish a permanent block via a separate `revoked` registry table.

## Offline behavior

When the device is offline:

* Cached mini-apps **boot and run** offline if their last successful boot succeeded.
* Network calls fail with `code: "network"` — partners are expected to handle this gracefully (skeleton, retry button, degraded mode).
* Updates are skipped silently; they're retried at next foreground.
* The kill list cache is honored. If a kill entry was already cached, the mini-app refuses to boot offline. (Cannot bypass kill by going offline.)

## Cache management

Cache eviction policy:

* Bundles last accessed > 30 days ago are eligible for eviction under disk pressure.
* Mini-apps the user has explicitly "favorited" are pinned and not evicted.
* Eviction logs a telemetry event so we can detect bad cache thrash.

Storage (the per-mini-app KV store) follows a separate, slower eviction policy:

* Storage is **never** evicted automatically. Quota enforcement on write is the sole gate.
* Users can clear a mini-app's storage manually from the mini-app's settings.
* When a mini-app is uninstalled, its storage is deleted after a 30-day grace period (in case the user re-launches).

## Telemetry channels

Four telemetry streams flow back to the platform:

| Stream         | What it carries                                                             | Sampling                  |
| -------------- | --------------------------------------------------------------------------- | ------------------------- |
| **Boot**       | Boot duration, success/fail, version, host version, tenant, rollout cohort. | 100%                      |
| **Errors**     | Uncaught JS errors, bridge rejections, signature failures.                  | 100%                      |
| **Permission** | Calls denied by permission/scope.                                           | 100% (rate-limited)       |
| **Usage**      | Mini-app foreground time, screen counts, partner-emitted analytics.         | 10% sample (configurable) |

All streams scrub PII. Partners get aggregate dashboards; platform team gets full streams.

## Versioning across host upgrades

When the host updates and bumps `sdkVersion`:

* All cached mini-apps are re-validated against the new `minHostVersion` requirement.
* Mini-apps targeting older `targetSdkVersion` continue to work via compat shims (see `06-bridge-and-codegen.md`).
* The host emits a one-time event letting installed mini-apps know `sdkVersion` changed (rare; informational).

When the host downgrades (rare; rollback):

* Mini-apps with `minHostVersion` exceeding the downgraded host are unrunnable.
* Cache is preserved; the mini-app reports "Update Based to use this mini-app" until host catches up.

Read [`10-developer-experience.md`](/docs/integrations/mini-apps-platform/10-developer-experience.md) next.
