Tauri Plugin OTA Self Update
Self-hosted OTA updates for Tauri v2 web assets.
This project provides:
- A Rust plugin for runtime update checks and apply flow.
- A guest JS API package for frontend usage.
- A universal GitHub Action (
action.yml) for publishing OTA artifacts.
Table of contents
- Features
- Platform support
- Installation
- Usage
- Manifest contract
- Rust-side access
- Publishing
- GitHub Action
- Landing
- Permissions
- Security
- Development
- License
Features
- Self-hosted OTA model (no vendor cloud lock-in).
- Update channels (
stable,beta, custom). - JS API:
check(),checkWithMeta(),setChannel(),getCurrentVersion(),Update.apply(). - Rust-side access via
app.ota_self_update(). - Single Rust runtime is used on all targets (no separate Kotlin/Swift OTA bridge required).
- Multiple publish targets: GitHub Releases, Bitbucket Downloads, S3-compatible, custom HTTP server.
- Marketplace-style reusable GitHub Action in repo root (
action.yml).
Platform support
| Platform | Status |
|---|---|
| macOS | Supported |
| Windows | Supported |
| Linux | Supported |
| Android | Supported |
| iOS | Supported |
Update providers
| Provider | Status | Notes |
|---|---|---|
| GitHub Releases | Supported | Runtime queries GitHub Releases API and selects a release that contains channel manifest asset (stable.json / beta.json). |
| Bitbucket Downloads | Supported | Publisher uploads archive + channel manifest (stable.json/beta.json) to repo downloads; runtime resolves manifest from Bitbucket downloads URL. |
| S3-compatible storage | Supported | Publisher uploads manifest/archive and maintains releases.json; runtime resolves latest released entry for channel/track, then falls back to manifest/<channel>.json. |
| Custom HTTP server | Supported | Same resolution model as S3 (releases.json with status filtering, fallback to manifest/<channel>.json) plus token-protected upload/admin API. |
Installation
Automatic (recommended)
From your Tauri app root (where package.json and the tauri script live):
Rust (src-tauri/Cargo.toml)
[]
= "0.1"
JavaScript
Usage
Backend initialization
let context = generate_context!;
let = init;
default
.plugin
.run
.expect;
Plugin config (tauri.conf.json)
GitHub mode note:
- Set
baseUrltohttps://github.com/<owner>/<repo>. stablechannel resolves the latest non-prerelease release assetstable.json.betachannel resolves the latest prerelease assetbeta.json.
activationPolicy values:
nextLaunch: apply assets and activate them on next app start.softReload: apply assets and mark as active immediately for runtime reload flows.
Frontend flow
import { check, getCurrentVersion, setChannel } from "tauri-plugin-ota-self-update-api";
await setChannel("stable");
const update = await check();
if (update) {
const applyResult = await update.apply();
const version = await getCurrentVersion();
console.log("Effective OTA version:", version.effectiveVersion, "source:", version.source);
if (applyResult.status === "appliedNow") {
location.reload();
}
}
Periodic check example
import { check, getCurrentVersion, setChannel } from "tauri-plugin-ota-self-update-api";
const CHECK_INTERVAL_MS = 15 * 60 * 1000; // every 15 minutes
async function checkAndApplyUpdate() {
try {
await setChannel("stable");
const update = await check();
if (!update) return;
const result = await update.apply();
const version = await getCurrentVersion();
console.log("OTA effective version:", version.effectiveVersion);
// For softReload policy, refresh immediately to use new assets.
if (result.status === "appliedNow") {
location.reload();
}
} catch (error) {
console.error("OTA periodic check failed:", error);
}
}
// run once on app start
void checkAndApplyUpdate();
// run periodically
setInterval(() => {
void checkAndApplyUpdate();
}, CHECK_INTERVAL_MS);
Auto-update on startup example (silent)
import { check, getCurrentVersion, setChannel } from "tauri-plugin-ota-self-update-api";
export async function runStartupOta() {
// shows effective version from plugin (native or OTA)
console.log(await getCurrentVersion());
await setChannel("stable");
const update = await check();
if (!update) return;
const result = await update.apply();
if (result.status === "appliedNow") {
// Soft reload policy: activate now.
location.reload();
}
// Next launch policy: assets are already cached as active OTA payload and
// will be used on next app start automatically.
}
Manifest contract
Example manifest/stable.json:
Rust-side access
You can call plugin logic from Rust commands/plugins through the extension trait:
use Manager;
use OtaSelfUpdateExt;
async
Publishing
Local publisher script:
OTA_PUBLISH_MODE=github \
OTA_CHANNEL=stable \
OTA_VERSION=1.2.3 \
OTA_BASE_URL=https://updates.example.com/ota \
OTA_TARGET_REPO=owner/repo \
Modes:
github: uses GitHub REST API via nativefetch.bitbucket: uploads artifacts to Bitbucket Downloads.s3: uses AWS SDK v3 (@aws-sdk/client-s3).server: uses nativefetchPUT for archive + manifest upload.
Cross-repo publish (private -> public) is supported in github mode:
- Build in a private repository, publish OTA assets to another repository via
OTA_TARGET_REPO=owner/public-repo. - Use
OTA_GITHUB_TOKEN(orGITHUB_TOKEN/GH_TOKEN) with write access to the target repository releases. - In GitHub Actions, default
GITHUB_TOKENis often scoped to the current repo only; use a PAT/FGPAT secret for cross-repo publish.
Example (private CI -> public OTA repo):
OTA_PUBLISH_MODE=github \
OTA_CHANNEL=stable \
OTA_VERSION=1.2.3 \
OTA_TARGET_REPO=owner/public-repo \
OTA_GITHUB_TOKEN=ghp_xxx \
For s3 and server modes, publisher also maintains releases.json index:
stableresolves latest non-prerelease entry.betaresolves latest prerelease entry.- Client falls back to
manifest/<channel>.jsonwhenreleases.jsonis unavailable.
Bitbucket mode example:
OTA_PUBLISH_MODE=bitbucket \
OTA_CHANNEL=stable \
OTA_VERSION=1.2.3 \
OTA_BITBUCKET_REPO=workspace/repo \
OTA_BITBUCKET_USERNAME=your-user \
OTA_BITBUCKET_APP_PASSWORD=your-app-password \
Alternative auth for Bitbucket mode:
OTA_BITBUCKET_TOKEN(Bearer token), orOTA_BITBUCKET_USERNAME+OTA_BITBUCKET_APP_PASSWORD.
GitHub Action
This repository exposes a reusable action in action.yml.
- name: Publish OTA
uses: s00d/tauri-plugin-ota-self-update@v1
with:
mode: github
channel: stable
version: 1.2.3
dist_dir: dist
base_url: https://updates.example.com/ota
target_repo: owner/repo
Primary inputs:
mode:github | bitbucket | s3 | server(required)version: OTA version (required)channel,dist_dir,out_dir,base_url,notestarget_repo,release_tag,github_token(github mode)bitbucket_repo,bitbucket_token,bitbucket_username,bitbucket_app_password(bitbucket mode)s3_bucket(s3 mode)server_token(server mode)manifest_signature,archive_signaturedry_run(true|false)
Validation workflow example is provided at .github/workflows/example-build.yml.
Cross-repo action usage notes:
target_repocan point to a different repository than the workflow repository.- For cross-repo publish, pass a PAT/FGPAT via
github_tokenthat has release write permissions ontarget_repo.
Self-hosted OTA server (Docker)
This repository includes a ready-to-run OTA server in server/.
- Server source:
scripts-src/ota-server.ts - Dashboard UI source:
scripts-src/server-ui/index.html,scripts-src/server-ui/dashboard.js - Built server runtime:
server/ota-server.cjs - Built dashboard assets:
server/dist/* - Docker image spec:
server/Dockerfile - Docker Compose:
server/docker-compose.yml - Upload auth:
Authorization: Bearer <OTA_SERVER_TOKEN>
Quick start:
Local run without Docker:
OTA_SERVER_TOKEN=super-secret \
PORT=8080 \
OTA_DATA_DIR=.ota-server-data \
Generate OpenAPI file on startup:
OTA_SERVER_TOKEN=super-secret \
OTA_OPENAPI_OUTPUT=.ota-server-data/openapi.json \
OpenAPI endpoint is always available at:
GET /openapi.json
Interactive online docs:
GET /docs
Then set plugin config:
Use publisher action/script in mode=server with server_token equal to OTA_SERVER_TOKEN.
Server dashboard:
GET /- web UI with token auth for release lifecycle operations.GET /api/info- release counters and runtime info (requires token).GET /api/releases- full release list (requires token).POST /api/releases/:channel/:version/confirm- publish draft.POST /api/releases/:channel/:version/revoke- revoke published version.DELETE /api/releases/:channel/:version?purge=true- remove entry and OTA files.
Publish to this server (manual example):
OTA_PUBLISH_MODE=server \
OTA_BASE_URL=http://127.0.0.1:8080 \
OTA_SERVER_TOKEN=super-secret \
OTA_CHANNEL=stable \
OTA_VERSION=0.2.1 \
OTA_RELEASE_STATUS=released \
OTA_DIST_DIR=examples/tauri-app/dist \
Beta/pre-release example:
OTA_PUBLISH_MODE=server \
OTA_BASE_URL=http://127.0.0.1:8080 \
OTA_SERVER_TOKEN=super-secret \
OTA_CHANNEL=beta \
OTA_VERSION=0.2.1-beta.1 \
OTA_RELEASE_STATUS=draft \
OTA_DIST_DIR=examples/tauri-app/dist \
The server stores:
manifest/stable.jsonandmanifest/beta.json<channel>/ota-dist-<version>.tar.gzreleases.jsonindex (used by client to resolve latest stable/beta)
Landing
A dedicated multi-page marketing + docs portal is included in landing/ with EN/RU translations.
- Stack: Vue 3 + Vite + Tailwind CSS v4
- i18n: content-driven (
site.en.ts/site.ru.ts) - Routes:
- Marketing home:
/and/ru - Full docs portal:
/docsand/ru/docs - Docs pages (examples):
installation,quick-start,channels-lifecycle,publisher-modes,server-dashboard,troubleshooting
- Marketing home:
- Local run:
pnpm --dir landing dev
- Build:
pnpm --dir landing build
- Deployment:
- Separate workflow:
.github/workflows/landing-pages.yml - Triggers:
workflow_dispatchand version tagsv*
- Separate workflow:
Permissions
Default permission set: ota-self-update:default.
Granular permissions are generated under permissions/ for:
check_for_updatesapply_updateset_channel
Security
- Always set
pubkeyand signatures in production. - Empty
pubkeyor empty signatures skip verification (development-only behavior). - Keep release keys in repository/org secrets and rotate periodically.
Development
License
MIT OR Apache-2.0