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

# Auto-updates

> Self-updating system for desktop (Cloudflare Worker proxy) and Android (Odoo endpoint) clients.

The auto-update system keeps desktop (Tauri) and Android (sideloaded APK) clients up to date. Desktop uses a Cloudflare Worker that proxies GitHub Releases from the private repo. Android uses an Odoo backend endpoint.

## Architecture

<Steps>
  <Step title="CI builds and publishes to GitHub Releases">
    The `release.yml` workflow triggers on `v*` tag push, builds the signed NSIS installer, and creates a GitHub Release with the installer `.exe` and its `.sig` signature file.
  </Step>

  <Step title="Client checks for updates on launch">
    An `UpdateGate` component wraps the app and blocks rendering until the update check completes. On web, it passes through immediately.
  </Step>

  <Step title="Worker proxies GitHub API">
    The Tauri updater plugin calls the Cloudflare Worker at `/update/check`. The Worker authenticates with the GitHub API using a server-side PAT, fetches the latest release metadata, resolves pre-signed download URLs, and returns Tauri-format JSON.
  </Step>

  <Step title="Desktop downloads and installs">
    Tauri downloads the installer from the pre-signed URL (no auth needed), verifies the Ed25519 signature, installs, and relaunches.
  </Step>

  <Step title="Android opens the APK URL">
    Android checks the Odoo endpoint via JS, then opens the APK download URL in the system browser for manual install.
  </Step>
</Steps>

## Desktop update endpoint

The desktop client does not talk to GitHub directly. A Cloudflare Worker (`workers/update-proxy/`) acts as a proxy that handles authentication for the private repo.

### How the Worker proxy works

```
Tauri app                  Cloudflare Worker              GitHub API
   │                            │                            │
   │  GET /update/check         │                            │
   │──────────────────────────►│                            │
   │                            │  GET /releases/latest      │
   │                            │  (with PAT)               │
   │                            │──────────────────────────►│
   │                            │◄──────────────────────────│
   │                            │                            │
   │                            │  Resolve pre-signed URLs   │
   │                            │  Fetch .sig contents       │
   │                            │──────────────────────────►│
   │                            │◄──────────────────────────│
   │                            │                            │
   │  Tauri-format JSON         │                            │
   │  (pre-signed download URLs)│                            │
   │◄──────────────────────────│                            │
   │                            │                            │
   │  GET pre-signed URL        │                            │
   │──────────────────────────────────────────────────────►│
   │◄──────────────────────────────────────────────────────│
   │  (binary download, no auth needed)                      │
```

The Worker:

1. Fetches the latest release from the GitHub API using a server-side PAT
2. Maps installer filenames to Tauri platform keys (e.g. `*_x64-setup.exe` → `windows-x86_64`)
3. Resolves pre-signed CDN download URLs for each asset (no auth needed to download)
4. Reads `.sig` file contents inline
5. Returns Tauri-format JSON with version, notes, platforms, and signatures

<Info>
  No PAT or auth token is embedded in the app binary. The Worker stores the GitHub PAT as a Cloudflare secret.
</Info>

### Endpoint URL

The updater plugin reads the endpoint from `tauri.conf.json`:

```
https://pos-update-proxy.jespinal.workers.dev/update/check
```

### Fallback endpoint

If the Worker endpoint is unreachable, the desktop client falls back to a dynamic Odoo endpoint (if `POS_API_BASE_URL` is configured):

```
GET {POS_API_BASE_URL}/pos-api/v1/updates/check/{{target}}/{{arch}}/{{current_version}}
```

The fallback only runs when the primary check throws a network error — not when it succeeds with no update available.

## Android update endpoint

Android uses the Odoo endpoint directly (the Tauri updater plugin does not support mobile):

```
GET /pos-api/v1/updates/check/android/aarch64/<current_version>
```

This endpoint is **public** (no JWT auth) because the client may not have valid tokens yet at launch.

| Response             | Meaning             |
| -------------------- | ------------------- |
| `200` with JSON body | Update available    |
| `204` No Content     | No update available |

## Release workflow

### Publishing a new version

1. Bump `version.json` and run `npm run version:sync`
2. Commit and tag: `git tag v1.1.0 && git push origin main --tags`
3. CI builds, signs, and creates a **draft** GitHub Release
4. Review the draft on GitHub, edit release notes, then **Publish**
5. The Worker picks up the new release within 5 minutes (cache TTL)
6. Existing installations detect the update on next launch

See [AUTO\_UPDATE\_GUIDE.md](/docs/AUTO_UPDATE_GUIDE.md) for the full step-by-step release process and checklist.

### CI configuration

The `release.yml` workflow:

* Validates (Ubuntu): lint, typecheck, unit tests
* Builds (Windows): Tauri NSIS installer with `tauri-apps/tauri-action`
* Signs with `TAURI_SIGNING_PRIVATE_KEY` from GitHub secrets
* Uploads installer + signature to GitHub Release

A separate `cache-warm.yml` workflow runs `cargo check` on main branch pushes to warm the Rust build cache for faster release builds.

### Required GitHub secrets

| Secret                               | Purpose                                          |
| ------------------------------------ | ------------------------------------------------ |
| `TAURI_SIGNING_PRIVATE_KEY`          | Ed25519 private key for signing update artifacts |
| `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` | Password for the signing key                     |

### Required Cloudflare secrets

| Secret       | Set via                          | Purpose                                                   |
| ------------ | -------------------------------- | --------------------------------------------------------- |
| `GITHUB_PAT` | `wrangler secret put GITHUB_PAT` | Fine-grained GitHub PAT with `Contents: read` on the repo |

## Signing

Tauri requires Ed25519 signatures to verify update artifacts.

**Generate a signing keypair:**

```bash theme={null}
cd nu_pos_react && npx @tauri-apps/cli signer generate -w ~/.tauri/nupos.key
```

The public key is embedded in `tauri.conf.json` under `plugins.updater.pubkey`. The private key is stored as a GitHub secret for CI builds.

<Warning>
  Never commit the private key (`~/.tauri/nupos.key`) to the repository. Only the public key in `tauri.conf.json` is safe to commit.
</Warning>

<Warning>
  The public key in `tauri.conf.json` **must match** the private key in the `TAURI_SIGNING_PRIVATE_KEY` GitHub secret. If you regenerate keys, update both — a mismatch causes "The signature was created with a different key than the one provided" errors on the client.
</Warning>

<Warning>
  Set signing secrets via CLI (`gh secret set TAURI_SIGNING_PRIVATE_KEY < ~/.tauri/nupos.key`), not the GitHub web UI. The web UI can corrupt base64 values during copy-paste.
</Warning>

### Updater artifacts

`tauri.conf.json` has `"createUpdaterArtifacts": true`, which tells the bundler to generate signed `.exe` installers with companion `.sig` signature files during CI builds.

## Worker proxy

### Source code

The Worker lives in `workers/update-proxy/` in the repo:

```
workers/update-proxy/
├── wrangler.toml       # Worker config (name, repo var)
├── package.json        # Dependencies (wrangler, types)
├── tsconfig.json       # TypeScript config
└── src/
    └── index.ts        # Worker implementation
```

### Endpoints

| Path            | Method | Response                                                                   |
| --------------- | ------ | -------------------------------------------------------------------------- |
| `/update/check` | GET    | Tauri-format JSON with version, platforms, pre-signed URLs, and signatures |
| `/health`       | GET    | `{"ok": true}`                                                             |

### Caching

The Worker returns `Cache-Control: public, max-age=300` (5 minutes). After publishing a new release, it takes up to 5 minutes for the Worker to serve the new version.

### Deploying changes

```bash theme={null}
cd workers/update-proxy
wrangler deploy
```

## Frontend components

### UpdateGate

`src/features/updates/UpdateGate.tsx` wraps the entire app outside `AuthProvider`. It:

* Detects the platform via `getPlatformInfo()` from runtime config flags (`IS_DESKTOP`, `IS_ANDROID`)
* On **web**: passes through immediately (no update check)
* On **desktop**: tries the Tauri updater plugin first (Worker proxy endpoint), falls back to the Odoo endpoint only if the plugin check fails
* On **Android**: calls the Odoo endpoint directly, then prompts to download
* On **error**: shows "Continue Anyway" (the `min_client_version` system parameter acts as a backstop)

### UpdateScreen

`src/features/updates/UpdateScreen.tsx` is a full-screen blocking component with i18n support (`updateScreen` namespace in `en.json`/`es.json`):

| Phase            | What the user sees                                    |
| ---------------- | ----------------------------------------------------- |
| `checking`       | Spinner with "Checking for updates..."                |
| `available`      | Version info + "Install Update" + "Later" buttons     |
| `downloading`    | Progress bar with percentage                          |
| `installing`     | Spinner with "Installing..." + restart message        |
| `android-prompt` | Version info + "Download Update" button               |
| `error`          | Error message + "Retry" and "Continue Anyway" buttons |

The "Later" button defers the update — the overlay will return on next app launch. During download/install, the overlay cannot be dismissed.

### Platform detection

`src/lib/updater.ts` exports `getPlatformInfo()` which reads `window.__POS_RUNTIME_CONFIG__`:

| Flag         | Set by   | Purpose                                  |
| ------------ | -------- | ---------------------------------------- |
| `IS_TAURI`   | `lib.rs` | App is running in Tauri shell            |
| `IS_DESKTOP` | `lib.rs` | Desktop platform (macOS, Windows, Linux) |
| `IS_ANDROID` | `lib.rs` | Android platform                         |

## Testing

### Manual testing checklist

<Steps>
  <Step title="Worker endpoint">
    Verify the Worker returns valid JSON:

    ```bash theme={null}
    curl -s https://pos-update-proxy.jespinal.workers.dev/update/check | python3 -m json.tool
    ```
  </Step>

  <Step title="Desktop">
    Run `npm run desktop:dev`. Verify the update check runs on launch. If a newer version is published, the overlay should appear.
  </Step>

  <Step title="Local test server">
    Build with version `0.0.1`, create a fake update JSON for `0.0.2`, serve with `npx serve`, and temporarily point the endpoint to localhost. See [AUTO\_UPDATE\_GUIDE.md](/docs/AUTO_UPDATE_GUIDE.md#7-testing-the-updater-locally) for details.
  </Step>

  <Step title="Android">
    Run `npm run tauri android dev`. Verify the "Download Update" prompt appears and opens the URL in the browser.
  </Step>

  <Step title="Web">
    Run `npm run dev` and verify the app loads normally with no update screen.
  </Step>

  <Step title="Offline">
    Disconnect from network. Verify the error screen appears with "Continue Anyway", and tapping it proceeds to the app.
  </Step>
</Steps>

### Unit tests

```bash theme={null}
cd nu_pos_react && npm run test:run -- src/lib/__tests__/updater.test.ts
```

Covers semver parsing, version comparison, and platform detection.

## Error handling

The update system is designed to be non-blocking on failure:

* **Network errors** during the update check show an error screen with "Continue Anyway"
* The existing `min_client_version` system parameter in Odoo acts as a backstop — clients below that version get a `CLIENT_OUTDATED` error after login
* **Signature verification failures** on desktop prevent installation (security requirement)
* **Android download failures** are handled by the system browser, not the app

## File map

| File                                                 | Purpose                                                          |
| ---------------------------------------------------- | ---------------------------------------------------------------- |
| `workers/update-proxy/src/index.ts`                  | Cloudflare Worker: proxies GitHub API, returns Tauri-format JSON |
| `workers/update-proxy/wrangler.toml`                 | Worker config (name, repo, GITHUB\_PAT secret)                   |
| `.github/workflows/release.yml`                      | CI: build, sign, create GitHub Release                           |
| `.github/workflows/cache-warm.yml`                   | CI: warm Rust build cache on main pushes                         |
| `nu_pos_react/src-tauri/tauri.conf.json`             | Updater endpoint URL (Worker), pubkey, `createUpdaterArtifacts`  |
| `nu_pos_react/src-tauri/src/lib.rs`                  | Plugin registration, fallback `check_for_update` command         |
| `nu_pos_react/src-tauri/capabilities/default.json`   | Plugin permissions                                               |
| `nu_pos_react/src/lib/updater.ts`                    | Platform detection, version comparison, update checks            |
| `nu_pos_react/src/features/updates/UpdateGate.tsx`   | App-level update gate wrapper                                    |
| `nu_pos_react/src/features/updates/UpdateScreen.tsx` | Full-screen update UI (i18n)                                     |
| `nu_pos_react/src/i18n/locales/{en,es}.json`         | `updateScreen` namespace translations                            |
| `nu_restaurant_pos/models/app_release.py`            | `nu.pos.app.release` model (Android fallback)                    |
| `nu_restaurant_pos/controllers/api_v1.py`            | Odoo update check endpoint (Android + desktop fallback)          |
