> ## 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 UPDATE GUIDE

# Auto-Update System Guide

This guide covers the full setup and operation of the NuPOS auto-update mechanism for desktop and Android clients.

## Architecture Overview

```
┌──────────────────────────────────────────────────────────────┐
│                     POS Terminal (Windows)                    │
│                                                              │
│  ┌─────────────┐    check()     ┌──────────────────────┐     │
│  │ React UI    │ ◄────────────► │ tauri-plugin-updater │     │
│  │ (UpdateGate │                │ (Rust side)          │     │
│  │  + Screen)  │                └──────────┬───────────┘     │
│  └─────────────┘                           │                 │
│                                            │ HTTPS GET       │
└────────────────────────────────────────────┼─────────────────┘
                                             │
                                             ▼
                              ┌──────────────────────────┐
                              │  Cloudflare Worker        │
                              │  pos-update-proxy         │
                              │                          │
                              │  GET /update/check       │
                              │  (no auth required)      │
                              └────────────┬─────────────┘
                                           │
                                           │ GitHub API
                                           │ (with PAT)
                                           ▼
                              ┌──────────────────────────┐
                              │  GitHub API               │
                              │                          │
                              │  /repos/.../releases     │
                              │  /repos/.../assets/{id}  │
                              │                          │
                              │  Returns pre-signed      │
                              │  CDN download URLs       │
                              └──────────────────────────┘
                                           ▲
                                           │ upload
                              ┌──────────────────────────┐
                              │  GitHub Actions           │
                              │  (release.yml)            │
                              │                          │
                              │  triggered by tag push    │
                              │  e.g. v1.0.0             │
                              └──────────────────────────┘
```

**Desktop (Windows)**: Tauri updater plugin calls the Cloudflare Worker → Worker fetches release metadata from GitHub API with server-side PAT → returns pre-signed download URLs → Tauri downloads installer directly from CDN (no auth needed) → verifies Ed25519 signature → installs → relaunches. Fully automatic, no manual steps for operators.

**Why a proxy?** The repo is private. GitHub Releases URLs return 404 without auth, and Tauri's HTTP client (`reqwest`) strips `Authorization` headers on cross-origin redirects to GitHub's CDN. The Worker solves this by resolving pre-signed CDN URLs server-side — no secrets in the app binary.

**Android (sideloaded)**: Frontend JS calls the Odoo backend endpoint, then opens the APK URL in the system browser for download and install.

**Web**: No update mechanism — deploy new static files to your server.

***

## 1. Initial Setup (One-Time)

### 1.1 Generate a Signing Keypair

The signing key ensures update integrity — Tauri will not apply unsigned updates. A keypair is already generated (public key is in `tauri.conf.json`). If you need a new one:

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

This produces:

* `~/.tauri/nupos.key` — **private key** (NEVER commit this)
* `~/.tauri/nupos.key.pub` — **public key** (goes into `tauri.conf.json`)

The CLI prompts for a password. Store both the key file and the password securely — if lost, existing installations cannot receive updates.

### 1.2 Configure GitHub Repository Secrets

Set secrets via the CLI (not the web UI, which can corrupt base64 values):

```bash theme={null}
gh secret set TAURI_SIGNING_PRIVATE_KEY < ~/.tauri/nupos.key
gh secret set TAURI_SIGNING_PRIVATE_KEY_PASSWORD
```

| Secret Name                          | Value                                   |
| ------------------------------------ | --------------------------------------- |
| `TAURI_SIGNING_PRIVATE_KEY`          | Full contents of `~/.tauri/nupos.key`   |
| `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` | The password used during key generation |

`GITHUB_TOKEN` is provided automatically by GitHub Actions — do not add it manually.

### 1.3 Configure the Cloudflare Worker

The Worker is already deployed at `https://pos-update-proxy.jespinal.workers.dev`. If you need to redeploy or reconfigure:

```bash theme={null}
cd workers/update-proxy
npm install          # Install wrangler + types
wrangler login       # Authenticate with Cloudflare (opens browser)
wrangler deploy      # Deploy the Worker
wrangler secret put GITHUB_PAT  # Set the GitHub PAT (paste when prompted)
```

The `GITHUB_PAT` must be a fine-grained GitHub PAT with `Contents: read` permission on the repo.

### 1.4 Verify tauri.conf.json Configuration

The following should already be set in `nu_pos_react/src-tauri/tauri.conf.json`:

```jsonc theme={null}
{
  "plugins": {
    "updater": {
      "endpoints": [
        "https://pos-update-proxy.jespinal.workers.dev/update/check"
      ],
      "pubkey": "<your public key>"   // Must match the private key in GitHub secrets
    }
  },
  "bundle": {
    "createUpdaterArtifacts": true,    // Generates .sig files
    "targets": ["nsis", ...]           // NSIS required for Windows auto-update
  }
}
```

**If you regenerated the keypair**, update `plugins.updater.pubkey` with the contents of the new `.pub` file. A mismatch causes "The signature was created with a different key than the one provided" errors.

***

## 2. Release Process

### Step-by-Step

#### 2a. Bump the Version

Edit `version.json` at the repo root:

```json theme={null}
{ "version": "1.1.0" }
```

Or use the bump skill:

```bash theme={null}
# From nu_pos_react/
npm run version:sync   # Stamps package.json, tauri.conf.json, Cargo.toml
```

The sync script runs automatically during `desktop:dev` and `desktop:build`, but run it manually if you want to verify or commit the stamped files.

#### 2b. Commit and Tag

```bash theme={null}
git add version.json nu_pos_react/package.json nu_pos_react/src-tauri/tauri.conf.json nu_pos_react/src-tauri/Cargo.toml
git commit -m "release: v1.1.0"
git tag v1.1.0
git push origin main --tags
```

#### 2c. GitHub Actions Runs Automatically

The `release.yml` workflow triggers on the `v*` tag push. It:

1. **Validates** (Ubuntu): lint, typecheck, unit tests
2. **Builds** (Windows): Compiles the Tauri app with NSIS installer, signs it, creates a **draft** GitHub Release with:
   * `NuPOS-Restaurant_1.1.0_x64-setup.exe` — NSIS installer
   * `NuPOS-Restaurant_1.1.0_x64-setup.exe.sig` — Ed25519 signature
   * `NuPOS-Restaurant_1.1.0_x64_en-US.msi` — MSI installer
   * `NuPOS-Restaurant_1.1.0_x64_en-US.msi.sig` — MSI signature

#### 2d. Review and Publish the Release

1. Go to **GitHub → Releases** — you'll see a new **Draft** release
2. Edit the release notes if needed (add changelog, known issues, etc.)
3. Click **Publish release**

#### 2e. Clients Detect the Update

The Worker caches responses for 5 minutes. After publishing, wait up to 5 minutes for the Worker cache to expire, then existing POS installations will detect the update on next launch.

### Manual Trigger (Testing)

You can also trigger the build manually via GitHub Actions → Release POS → Run workflow, without pushing a tag. This is useful for testing the CI pipeline.

***

## 3. How the Update Flow Works

### On App Launch

```
App starts
    │
    ▼
UpdateGate checks platform
    │
    ├── Web ──────────► Pass through (no update check)
    │
    ├── Desktop ──────► Plugin check() calls Worker /update/check
    │                      │
    │                      ├── Same version ──► Proceed to app
    │                      │
    │                      ├── check() fails ──► Fallback to Odoo endpoint
    │                      │                       (only if POS_API_BASE_URL set)
    │                      │
    │                      └── Update found
    │                            │
    │                            ▼
    │                         Full-screen overlay
    │                         "Version X.Y.Z available"
    │                            │
    │                            ├── "Install Update" ──► Download → Verify sig → Install → Relaunch
    │                            │     (overlay stays, non-dismissable during download)
    │                            │
    │                            └── "Later" ──► Proceed to app
    │                                  (overlay returns on next launch)
    │
    └── Android ──────► JS calls Odoo endpoint
                           │
                           ├── 204 ──► Proceed to app
                           │
                           └── 200 ──► "Download Update" opens APK URL in browser
```

### Download Flow (Private Repo)

The Worker solves the private repo download problem:

1. Worker calls GitHub API `/releases/assets/{id}` with PAT and `Accept: application/octet-stream`, using `redirect: "manual"`
2. GitHub returns 302 with a **pre-signed CDN URL** in the `Location` header
3. Worker returns this pre-signed URL in the Tauri JSON response
4. Tauri downloads directly from the pre-signed CDN URL — no auth headers needed

Pre-signed URLs expire in \~10 minutes. Since the update check and download happen in sequence, this is sufficient.

### Fallback Behavior

If the Worker endpoint is unreachable (no internet, DNS failure, etc.):

* If `POS_API_BASE_URL` is set, falls back to the Odoo update endpoint
* If that also fails, shows a brief error with "Continue Anyway"
* The POS never gets stuck — operators can always reach the order screen
* The `min_client_version` Odoo system parameter serves as a hard backstop at bootstrap time

***

## 4. CI Cost Budget

### Per-Release (Windows Only)

| Runner            | Real Minutes     | Multiplier | Billed Minutes |
| ----------------- | ---------------- | ---------- | -------------- |
| Ubuntu (validate) | \~2 min          | 1x         | \~2            |
| Windows (build)   | \~8 min (cached) | 2x         | \~16           |
| **Total**         |                  |            | **\~18**       |

**First build** may take \~15-20 real minutes (no Rust cache). Subsequent builds benefit from the `cache-warm.yml` workflow that runs `cargo check` on main pushes to warm the Rust cache.

**Budget**: 2,000 free minutes / 18 per release ≈ **\~110 releases/month** — effectively unlimited.

The release workflow only triggers on tag pushes (`v*`), so normal development pushes consume zero release CI minutes.

### Build performance optimizations

* **Rust cache warming**: `cache-warm.yml` runs on main pushes (when Tauri source changes), saving the Rust compilation cache so tag-triggered release builds can restore from it
* **LLD linker**: `RUSTFLAGS: "-C link-arg=-fuse-ld=lld"` for faster linking
* **`cache-on-failure: true`**: Saves partial build cache even on failures

***

## 5. Signing Key Management

| Key         | Location                                     | Purpose                                                         |
| ----------- | -------------------------------------------- | --------------------------------------------------------------- |
| Private key | `~/.tauri/nupos.key` + GitHub secret         | Signs release artifacts during CI build. **Keep secret.**       |
| Public key  | `tauri.conf.json` → `plugins.updater.pubkey` | Embedded in the app binary. Verifies signatures at update time. |

### Key Rotation

If the private key is compromised:

1. Generate a new keypair: `npx @tauri-apps/cli signer generate -w ~/.tauri/nupos-v2.key`
2. Update `tauri.conf.json` with the new public key
3. Update the GitHub secrets via CLI: `gh secret set TAURI_SIGNING_PRIVATE_KEY < ~/.tauri/nupos-v2.key`
4. Build and ship a release signed with the **old** key (so existing clients can verify it)
5. That release now contains the new public key in its binary
6. All subsequent releases are signed with the new private key

***

## 6. Android Update Strategy

The `tauri-plugin-updater` does **not** support Android. Android updates use a separate path:

1. The Odoo backend serves update check responses at `GET /pos-api/v1/updates/check/android/aarch64/{version}`
2. On launch, the frontend calls this endpoint and compares versions
3. If an update is available, the same blocking overlay appears
4. "Download Update" opens the APK URL in the system browser
5. Requires **"Install from unknown sources"** enabled on the tablet (one-time setup)

### Adding Android APK to Releases

To also distribute Android APKs via GitHub Releases:

1. Build the APK: `cd nu_pos_react && npm run tauri android build`
2. Attach the APK to the same GitHub Release as the Windows artifacts
3. Update the Odoo release record to point to the GitHub Release APK URL

***

## 7. Testing the Updater Locally

### Without GitHub Releases

You can test the update flow with a local HTTP server:

1. Build the app with version `0.0.1` (set in `version.json`)
2. Create a `latest.json` for version `0.0.2`:

```json theme={null}
{
  "version": "0.0.2",
  "notes": "Test update",
  "pub_date": "2026-03-31T00:00:00Z",
  "platforms": {
    "windows-x86_64": {
      "signature": "<contents of .sig file>",
      "url": "http://localhost:8080/NuPOS-Restaurant_0.0.2_x64-setup.exe"
    }
  }
}
```

3. Serve the directory: `npx serve -l 8080 ./test-update-server`
4. Temporarily change the endpoint in `tauri.conf.json` to `http://localhost:8080/latest.json`
5. Run the `0.0.1` build — it should detect and offer the `0.0.2` update

### Verifying the Worker

After publishing a release, verify the Worker endpoint:

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

Expected output:

```json theme={null}
{
  "version": "1.1.0",
  "notes": "...",
  "pub_date": "2026-03-31T...",
  "platforms": {
    "windows-x86_64": {
      "url": "https://release-assets.githubusercontent.com/...",
      "signature": "dW50cnVzdGVkIGNvbW1lbnQ6..."
    },
    "windows-x86_64-nsis": { "..." },
    "windows-x86_64-msi": { "..." }
  }
}
```

The `url` fields should be pre-signed CDN URLs (not `api.github.com` URLs).

### Health check

```bash theme={null}
curl -s https://pos-update-proxy.jespinal.workers.dev/health
# {"ok":true}
```

***

## 8. Troubleshooting

### Client doesn't check for updates

* **Web**: Expected — web has no update mechanism.
* **Desktop**: Verify the app is a Tauri build (check window title shows version). The plugin reads endpoints from `tauri.conf.json` which is baked into the binary at build time.
* **Android**: Verify `POS_API_BASE_URL` is set and the Odoo endpoint is reachable from the device.

### Worker returns 204 (no releases)

* Check the GitHub Release is **published** (not draft)
* Check the `GITHUB_PAT` secret is set: `cd workers/update-proxy && wrangler secret list`
* Check the PAT has `Contents: read` permission on the repo

### Worker returns 502

* The GitHub API returned an error. Check the PAT hasn't expired.
* Verify with: `curl -s -H "Authorization: token <PAT>" https://api.github.com/repos/josespinal/rost_pos_restaurant/releases/latest`

### Update found but download fails

* Pre-signed URLs expire in \~10 minutes. If the user waited too long, retry the update check.
* Check the release has both `.exe` and `.sig` files uploaded

### Signature verification fails

* The public key in `tauri.conf.json` must match the private key in the `TAURI_SIGNING_PRIVATE_KEY` GitHub secret
* If you regenerated keys, both must be updated together
* Set secrets via CLI (`gh secret set`), not the web UI (which can corrupt base64)
* Don't manually re-upload a modified binary without re-signing

### CI build fails

* **Missing secrets**: Verify `TAURI_SIGNING_PRIVATE_KEY` and `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` are set in GitHub repo settings
* **Rust compilation errors**: Check Cargo.toml dependencies match the Tauri version
* **Version mismatch**: Ensure `version.json`, `tauri.conf.json`, `Cargo.toml`, and `package.json` all have the same version (run `npm run version:sync`)

### "Later" defers but update appears every launch

This is by design. The operator can defer with "Later", but the update check runs on every app launch. Once an update is available, the overlay will appear each time until the update is installed. During download/install, the overlay cannot be dismissed.

### Android download doesn't trigger install

* Verify `REQUEST_INSTALL_PACKAGES` permission is in `AndroidManifest.xml`
* The user may need to enable "Install from unknown sources" in Android settings
* Some Android versions require: Settings → Apps → Browser → Install unknown apps

***

## 9. Rollback

Tauri does not include a built-in rollback mechanism. If a bad update is deployed:

1. **Fastest**: Publish a new release with the fix (bump patch version)
2. **Manual**: Reinstall the previous version from the GitHub Releases page
3. **Prevention**: Use `releaseDraft: true` in CI (current setting) to review builds before publishing

For critical POS deployments, keep the last 2-3 release installers downloaded locally as a safety net.

***

## 10. Worker Proxy Reference

### Source

`workers/update-proxy/` in the repo root.

### Configuration

`wrangler.toml`:

```toml theme={null}
name = "pos-update-proxy"
main = "src/index.ts"
compatibility_date = "2024-01-01"

# GITHUB_PAT must be set via: wrangler secret put GITHUB_PAT

[vars]
GITHUB_REPO = "josespinal/rost_pos_restaurant"
```

### Platform key mapping

The Worker maps release asset filenames to Tauri platform keys:

| Asset filename pattern | Platform key                            |
| ---------------------- | --------------------------------------- |
| `*_x64-setup.exe`      | `windows-x86_64`, `windows-x86_64-nsis` |
| `*_x64_en-US.msi`      | `windows-x86_64-msi`                    |
| `*_aarch64.AppImage`   | `linux-aarch64`                         |
| `*_amd64.AppImage`     | `linux-x86_64`                          |
| `*.dmg` (aarch64)      | `darwin-aarch64`                        |
| `*.dmg` (x86\_64)      | `darwin-x86_64`                         |

Assets without a matching `.sig` file are excluded from the response (Tauri requires signatures).

### Deploying Worker changes

```bash theme={null}
cd workers/update-proxy
npm install     # If dependencies changed
wrangler deploy
```

***

## 11. File Reference

| File                                                 | Purpose                                                          |
| ---------------------------------------------------- | ---------------------------------------------------------------- |
| `version.json`                                       | Single source of truth for app version                           |
| `scripts/sync-version.mjs`                           | Stamps version into package.json, tauri.conf.json, Cargo.toml    |
| `workers/update-proxy/src/index.ts`                  | Cloudflare Worker: proxies GitHub API, returns Tauri-format JSON |
| `workers/update-proxy/wrangler.toml`                 | Worker config (name, repo var, GITHUB\_PAT secret)               |
| `.github/workflows/release.yml`                      | CI/CD: 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), public key, bundle config         |
| `nu_pos_react/src-tauri/Cargo.toml`                  | Rust deps: updater, process plugins                              |
| `nu_pos_react/src-tauri/src/lib.rs`                  | Plugin registration, fallback `check_for_update` command         |
| `nu_pos_react/src-tauri/capabilities/default.json`   | Tauri permissions for updater/process                            |
| `nu_pos_react/src/lib/updater.ts`                    | Platform detection, version comparison, update check logic       |
| `nu_pos_react/src/features/updates/UpdateGate.tsx`   | App-level gate (wraps all providers)                             |
| `nu_pos_react/src/features/updates/UpdateScreen.tsx` | Full-screen update UI (i18n, progress bar)                       |
| `nu_pos_react/src/i18n/locales/{en,es}.json`         | `updateScreen` namespace translations                            |
| `~/.tauri/nupos.key`                                 | Signing private key (local only, also in GitHub secrets)         |

***

## 12. Release Checklist

* [ ] Bump `version.json` to the new version
* [ ] Run `npm run version:sync` (or let the CI do it from the tag)
* [ ] Commit: `git commit -m "release: vX.Y.Z"`
* [ ] Tag: `git tag vX.Y.Z`
* [ ] Push: `git push origin main --tags`
* [ ] Wait for CI to complete (check GitHub Actions)
* [ ] Review the draft release on GitHub → edit notes → **Publish**
* [ ] Verify Worker: `curl -s https://pos-update-proxy.jespinal.workers.dev/update/check | python3 -m json.tool`
* [ ] (Optional) Update `nu_pos.min_client_version` in Odoo to enforce the minimum version
* [ ] (Android) Create/update Odoo release record with APK URL if shipping Android update
