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

# Tauri release notes

> Add native release windows and app-owned update controls to a Tauri 2 app.

Use the optional `@feedbackloop/react/tauri` entry point from the MIT-licensed `0.2.1` package in a React 19/Tauri 2 app. This guide includes the root, updater adapter and capabilities; no private repository example is required. Your app retains update eligibility, native handles, persistence, installation and restart. [Publish a release note](https://docs.feedbackloop.page/integrations/release-notes.md#prepare-a-release) with the exact native app version, including build metadata.

## Install and mount the host

The optional native entry point opens reusable release-reader and update windows. It reads the app name and installed version through Tauri, rather than the frontend package version. Mount the same `ReleaseNotesHost` root before your app's startup providers and router in each app window. Keep the main window's root mounted, including while it is hidden; it remains the sole owner of update callbacks and preferences. SDK child windows use the same entry point and are handled automatically. Keep app startup effects and background update checks in the main window.

Install the published SDK and official Tauri 2 JavaScript peers in your existing React 19/Tauri 2 application:

```sh theme={null}
pnpm add @feedbackloop/react@0.2.1 @tauri-apps/api@^2 @tauri-apps/plugin-store@^2 @tauri-apps/plugin-opener@^2
```

Add the corresponding Rust store/opener plugins from your `src-tauri` directory:

```sh theme={null}
cargo add tauri-plugin-store@2 tauri-plugin-opener@2
```

Register them on your existing Tauri builder:

```rust theme={null}
.plugin(tauri_plugin_store::Builder::new().build())
.plugin(tauri_plugin_opener::init())
```

```tsx theme={null}
import { createRoot } from 'react-dom/client'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { ReleaseNotesHost, type AppUpdater } from '@feedbackloop/react/tauri'
import '@feedbackloop/react/styles.css'
import { createAppUpdater } from './app-updater'
import { flushAppState } from './app-state'
import { App } from './app'

const isMain = getCurrentWebviewWindow().label === 'main'
const owner = isMain ? createAppUpdater(flushAppState) : null
const appUpdater: AppUpdater = owner?.updater ?? {
  async check() {
    throw new Error('Updater callbacks execute only in main.')
  },
  async install() {
    throw new Error('Updater callbacks execute only in main.')
  },
}

function Root() {
  return (
    <ReleaseNotesHost
      projectId="YOUR_PROJECT_ID"
      icon="/icon.png"
      updater={appUpdater}
      theme="system"
    >
      <App />
    </ReleaseNotesHost>
  )
}

createRoot(document.getElementById('root')!).render(<Root />)
```

`App` and `flushAppState` are your application modules. The flush must await real database/document/media writes and reject when they cannot be saved; do not substitute an empty function. Keep `appUpdater` stable across React renders. The default host label is `main`; only supply `hostWindowLabel` when your existing sole owner has a different label. If you override it, change the `isMain` guard and the host capability's `windows` label to that same value so the updater owner initializes in the correct window.

SDK child detection prevents `<App />` from rendering in SDK windows, but module imports still execute. Keep native resource construction behind the main-window guard above, and keep startup/background-check effects in main. Adapt `owner.dispose()` to your app's updater shutdown lifecycle. Main owns cleanup; closing Settings must not dispose its updater.

### Consumer-owned updater

The native entry point exports the complete types below. `check()` decides whether a candidate is eligible, including any app-specific rollout or rollback policy. `install()` owns the exact-candidate guard, app-state flush, installation and automatic restart. `notes` is optional plain-text updater content; published Feedback Loop Markdown remains the primary release document.

```ts theme={null}
export interface AvailableUpdate {
  version: string
  notes?: string
}

export type InstallProgress =
  | { phase: 'downloading'; downloadedBytes: number; totalBytes?: number }
  | { phase: 'installing' | 'restarting' }

export interface AppUpdater {
  check(): Promise<AvailableUpdate | null>
  install(
    version: string,
    reportProgress: (progress: InstallProgress) => void,
  ): Promise<void>
  cancel?(): Promise<void>
}
```

The adapter flushes before installation and again immediately before restart so edits made during download are saved. A failed final flush prevents restart and retains the installed version for retry without another download or installation. The flush must drain real writes and reject on save conflicts. After installation, `check()` returns the retained pending-restart version without acquiring a new native handle; closing a failed restart window and choosing Check now therefore preserves the restart retry.

The root above uses the following consumer-owned `app-updater.ts` adapter for Tauri's official updater and process plugins. Supply `flushAppState` from your app's persistence layer, and release the returned owner through your app's updater shutdown path. Construct this adapter only in `main`; ordinary secondary windows receive the harmless callbacks shown above, which the SDK does not invoke there.

Install and register the optional app-owned updater/process plugins as well as the SDK's store/opener requirements:

```sh theme={null}
pnpm add @tauri-apps/plugin-updater@^2 @tauri-apps/plugin-process@^2
cargo add tauri-plugin-updater@2 tauri-plugin-process@2
```

```rust theme={null}
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_process::init())
```

Configure your own updater endpoint, matching public signing key and update artifacts in Tauri. No private signing key belongs in the frontend. The example adapter deliberately omits `cancel`: official `downloadAndInstall` does not expose supported cancellation. The SDK hides Cancel, and closing the progress window leaves installation running.

> **Complete official updater adapter**
>
> ```ts theme={null}
> import { check, type Update } from '@tauri-apps/plugin-updater'
> import { relaunch } from '@tauri-apps/plugin-process'
> import type { AppUpdater } from '@feedbackloop/react/tauri'
>
> /** Creates an app-owned updater; the SDK receives callbacks, never its native handle.
>  * @param flushAppState - Consumer persistence step before installation/restart.
>  * @returns The updater and its host-unmount cleanup operation.
>  */
> export function createAppUpdater(flushAppState: () => Promise<void>): {
>   updater: AppUpdater
>   dispose: () => Promise<void>
> } {
>   let candidate: Update | null = null
>   let disposed = false
>   let installing = false
>   let generation = 0
>   let pendingRelaunch: string | null = null
>   const updater: AppUpdater = {
>     async check() {
>       if (disposed || installing)
>         throw new Error('The app updater is busy or closed.')
>       if (pendingRelaunch) return { version: pendingRelaunch }
>       const request = ++generation
>       const next = await check()
>       if (disposed || request !== generation) {
>         await next?.close()
>         return null
>       }
>       const previous = candidate
>       candidate = next
>       await previous?.close()
>       return next
>         ? { version: next.version, ...(next.body ? { notes: next.body } : {}) }
>         : null
>     },
>     async install(version, reportProgress) {
>       if (pendingRelaunch === version && !disposed && !installing) {
>         installing = true
>         try {
>           await flushAppState()
>           reportProgress({ phase: 'restarting' })
>           await relaunch()
>         } finally {
>           installing = false
>         }
>         return
>       }
>       const selected = candidate
>       if (disposed || installing || !selected || selected.version !== version)
>         throw new Error('The selected app update is stale. Check again.')
>       installing = true
>       generation++
>       try {
>         // A real app flushes its database/documents here before any installer can quit it.
>         await flushAppState()
>         let downloadedBytes = 0
>         let totalBytes: number | undefined
>         await selected.downloadAndInstall((event) => {
>           if (event.event === 'Started') {
>             totalBytes = event.data.contentLength
>             reportProgress({
>               phase: 'downloading',
>               downloadedBytes: 0,
>               totalBytes,
>             })
>           } else if (event.event === 'Progress') {
>             downloadedBytes += event.data.chunkLength
>             reportProgress({
>               phase: 'downloading',
>               downloadedBytes,
>               totalBytes,
>             })
>           } else reportProgress({ phase: 'installing' })
>         })
>         await selected.close().catch(() => undefined)
>         candidate = null
>         pendingRelaunch = version
>         // Edits can arrive during download; preserve the restart retry if this flush fails.
>         await flushAppState()
>         reportProgress({ phase: 'restarting' })
>         await relaunch()
>       } finally {
>         installing = false
>       }
>     },
>     // Official downloadAndInstall has no cancellation contract. The SDK hides Cancel.
>   }
>   return {
>     updater,
>     async dispose() {
>       disposed = true
>       generation++
>       // Closing an in-use resource would interfere with a consumer-owned installation.
>       if (!installing) {
>         await candidate?.close()
>         candidate = null
>       }
>     },
>   }
> }
> ```

Keep native update handles in this adapter. If your app already checks in the background, pass its candidate to `useReleaseNotes().notifyUpdateAvailable(candidate)`; the SDK does not start another checker. `notifyUpdateAvailable()` respects the skipped exact version. The **Check now** control in `ReleaseNotesSettings` ignores a previously skipped version. `useReleaseNotes()` in `0.1.0` exposes only `open()` and `notifyUpdateAvailable()`; it has no public manual-check method. Installation failures stay retryable; the sample also retains a successful-install/pending-relaunch state so relaunch can be retried without reinstalling.

### Manual checks from app menus

Published SDK `0.1.1` includes `useReleaseNotes().checkForUpdates(): Promise<void>`, which exposes the same manual action as **Check now** for an app menu or tray bridge. It checks through the main updater, bypasses Skip, and returns focus to the requesting Settings window after the result closes. `0.1.0` does not include this method.

A fresh Settings caller waits up to 15 seconds for the authenticated main connection before sending its request. Catch rejected connection or transport promises in the app action and allow retry. Failed checks appear in the SDK update window with **Try again**. Installation or restart failures keep the **Install update** button available to retry the app-owned operation. If one tray request arrives through both a query parameter and a native event, coalesce that delivery in your app before calling the action. Keep main mounted; reloading its WebView still requires reopening connected windows.

Use the same app-owned updater owner for background and Settings checks so a notified candidate refers to the retained handle that `install()` will validate.

Inside the host, add the supplied Updates settings and a menu action. `ReleaseNotesSettings` takes no props and includes **Show changelog after each update**, **What's new** and **Check now** controls:

```tsx theme={null}
import {
  ReleaseNotesSettings,
  useReleaseNotes,
} from '@feedbackloop/react/tauri'

export function Updates() {
  return <ReleaseNotesSettings />
}

export function WhatsNewButton() {
  const releases = useReleaseNotes()
  return <button onClick={() => void releases.open()}>What's new</button>
}
```

### Existing secondary settings window

If your app already opens a separate settings window, mount the same root in that window and render `ReleaseNotesSettings` in its Updates tab. The settings controls connect to the main host; they stay disabled until its current state arrives. The secondary window does not run the updater callbacks or persist preferences. Keep the main root mounted even when the settings window is closed.

The reader and update prompt return focus to the settings window that opened them when closed. The main window can remain hidden. A single-window app uses the normal host setup without this additional capability.

Add this optional local capability, replacing `settings-window` with your existing window's actual label:

```json theme={null}
{
  "identifier": "consumer-settings",
  "windows": ["settings-window"],
  "permissions": [
    "core:event:allow-listen",
    "core:event:allow-unlisten",
    "core:event:allow-emit-to"
  ]
}
```

Keep this capability local, without remote origins. Retain your settings window's existing app permissions separately; the release integration adds only these event permissions there. Store, window-creation and updater permissions belong to the main host.

### Native capabilities

Create separate local files under `src-tauri/capabilities/`. These exact labels match the default main host and SDK-created children. Do not grant updater/store access through a wildcard window capability. Review overlapping existing capabilities: adding a narrow file does not revoke a broader grant elsewhere.

`release-notes-host.json` below includes the official updater/process permissions used by this guide's adapter. Remove those three plugin permissions if your app uses a different app-owned updater. Window hiding is an app-owned action, not an SDK requirement; retain `core:window:allow-hide` separately if your own tray or Settings code needs it.

```json theme={null}
{
  "identifier": "release-notes-host",
  "windows": ["main"],
  "permissions": [
    "core:app:allow-name",
    "core:app:allow-version",
    "core:event:allow-listen",
    "core:event:allow-unlisten",
    "core:event:allow-emit-to",
    "core:webview:allow-create-webview-window",
    "core:window:allow-get-all-windows",
    "core:window:allow-show",
    "core:window:allow-set-focus",
    "core:window:allow-destroy",
    "core:window:allow-close",
    "store:allow-load",
    "store:allow-get",
    "store:allow-set",
    "store:allow-save",
    "core:resources:allow-close",
    "updater:allow-check",
    "updater:allow-download-and-install",
    "process:allow-restart"
  ]
}
```

`release-notes-children.json` permits only presentation events, self-close and HTTP(S) links:

```json theme={null}
{
  "identifier": "release-notes-children",
  "windows": [
    "feedback-loop-release-notes-reader",
    "feedback-loop-release-notes-update"
  ],
  "permissions": [
    "core:event:allow-listen",
    "core:event:allow-unlisten",
    "core:event:allow-emit-to",
    "core:window:allow-close",
    {
      "identifier": "opener:allow-open-url",
      "allow": [
        {
          "url": "https://*"
        },
        {
          "url": "http://*"
        }
      ]
    }
  ]
}
```

Keep these capabilities local, without `remote` origins. Preserve permissions your existing app features require, while keeping SDK preferences and updater resources main-owned. An existing secondary Settings window uses the event-only capability above; it receives no SDK updater/store permissions.

### Theme and content security policy

Pass the app's current `system`, `light` or `dark` preference to the main host's `theme` prop. Open SDK windows update when it changes; `system` tracks operating-system changes live. In embedded Settings, application variables `--background`, `--foreground`, `--muted-foreground`, `--border`, `--primary` and `--primary-foreground` override the fallback palette. Import `@feedbackloop/react/styles.css` once in the root; no Tailwind setup is required.

If your app defines a CSP, add `https://feedbackloop.page` to `connect-src`, alongside existing Tauri IPC and app-owned updater endpoints. Permit the API origin and the actual redirected storage/CDN and external Markdown image origins in `img-src`. An API-origin-only image policy can block the final signed-image redirect. Keep your existing restrictions; do not allow arbitrary remote pages access to native capabilities.

### After-update behavior

Settings persist only whether to show notes after an update, the acknowledged installed version and the skipped candidate version. First launch establishes the current version without showing old notes. A later installed upgrade opens its exact published note after it renders successfully; a failure remains retryable. The same full installed version and downgrades stay quiet; different build metadata at equal SemVer precedence counts as a new installed release. Missing or offline exact-version publication defers acknowledgement and retries on host focus/connectivity. Both windows follow the selected theme, and system changes update open windows.

## Recovery and release checks

A startup error usually means a missing Rust plugin or local app/event/window/store permission. Verify registration and exact labels, and review all overlapping capabilities. If the app has a separate Settings window, main must remain mounted even when hidden. Keep app boot, account/editor effects and background checkers main-only; SDK child windows must not start them.

During development, reloading only the main WebView replaces its private connection state. Close and reopen existing Settings windows afterward. A full app restart creates a fresh host and preferences connection.

Test an installation failure and a relaunch failure: the adapter must preserve retryability, reject stale candidate versions, and finish a real save before installation. Closing the progress window does not cancel the app-owned install. Omit `cancel` unless your own updater truly supports cancellation.

Before distributing your app, test full history and an exact published native version, existing secondary Settings, return focus with hidden main, live themes, preferences across restart, offline/reconnect, and first-launch baseline. Demonstrate automatic after-update notes using two SDK-enabled builds; the first SDK launch cannot infer the previous installed version.

Complete real signed installer replacement, persistence flush and automatic relaunch acceptance on every OS you ship. SDK macOS/Windows/Linux native fixture CI exercises real windows with simulated updates; it does not prove your app's installer. Missing exact notes stay retryable rather than acknowledging a saved copy. Publishing notes never uploads an app binary.

See the [React reader](https://docs.feedbackloop.page/integrations/release-notes/react.md) for embedded browser use and the [HTTP API](https://docs.feedbackloop.page/integrations/release-notes/http-api.md) for the wire and image contract.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.