A production-ready Tauri v2 + React 19 desktop app shell with licensing, analytics, auto-updates, and secure storage baked in.
Backstage is a desktop app for AI-powered content creation — carousel generation, image editing, and YouTube analytics. After shipping it with a full licensing system, encrypted storage, SQLite migrations, and license-gated auto-updates, every new Tauri project needed the same plumbing rebuilt from scratch.
Torii is the extracted boilerplate. Everything that was painful to get right the first time — the Polar license flow, device activation, AES-256-GCM key storage, the migration pipeline, the version gate, onboarding — lives here so it never has to be rebuilt again.
- Frontend: React 19, TypeScript, Tailwind CSS v4, Zustand, Vite
- Backend: Tauri v2 (Rust), SQLite via plugin-sql
- Licensing: Polar (license key validation with offline cache)
- Analytics: PostHog + optional Axiom log shipping
- Updates: GitHub Releases auto-updater with license expiry gate
- Storage: AES-256-GCM encrypted key-value store (Rust), Tauri plugin-store (JSON)
- Rust (stable)
- Bun
- Tauri v2 prerequisites for your platform
bun install
cp .env.example .env
# fill in .env values (see Configuration below)
bun run dev| Command | Description |
|---|---|
bun run dev |
Start Tauri dev (hot reload) |
bun run vite:dev |
Frontend only (no Tauri) |
bun run desktop:build |
Production Tauri build |
bun run typecheck |
TypeScript check |
Copy .env.example to .env and fill in:
VITE_POLAR_API_URL # https://api.polar.sh (or sandbox)
VITE_POLAR_ORGANIZATION_ID # from polar.sh dashboard
VITE_POLAR_ORG_SLUG # your org slug
VITE_POLAR_CUSTOMER_PORTAL_URL
VITE_POLAR_PURCHASE_URL # your product page
VITE_POLAR_SERVER_URL # your Hono/API server URL
VITE_AXIOM_TOKEN # optional, for log shipping
VITE_AXIOM_DATASET # optional
VITE_POSTHOG_KEY # optional, for analytics
VITE_POSTHOG_HOST # optional
Three places to update:
src-tauri/Cargo.toml—nameand[lib] namesrc-tauri/tauri.conf.json—productName,identifier, windowtitlepackage.json—name
The updater pulls from a GitHub Releases endpoint. After renaming:
-
Set
endpointsintauri.conf.jsonto your repo's release URL:https://github.com/YOUR_ORG/YOUR_REPO/releases/latest/download/latest.json -
Generate a signing keypair (run once, store the output safely):
bunx tauri signer generate -w ~/.keys/my-app.key -p "" --ci
This writes two files:
~/.keys/my-app.key— private key (never commit this)~/.keys/my-app.key.pub— public key (safe to read, goes in config)
-
Copy the public key into
tauri.conf.json:"plugins": { "updater": { "pubkey": "<contents of my-app.key.pub>" } }
-
Add the private key as a GitHub Actions secret:
gh secret set TAURI_SIGNING_PRIVATE_KEY --repo YOUR_ORG/YOUR_REPO \ --body "$(cat ~/.keys/my-app.key)"
Leave
TAURI_SIGNING_PRIVATE_KEY_PASSWORDunset if you used-p "".
src/
App.tsx # Root: license gate, page routing, shell
components/
TitleBar.tsx # Custom window chrome (draggable, close/min/max)
SettingsPage.tsx # General / License / Storage / Updates / Privacy tabs
LicenseActivation.tsx # Polar license key entry
OnboardingPage.tsx # 4-step onboarding (welcome, appearance, privacy, done)
VersionGateModal.tsx # Blocks launch when running past license expiry
CustomerPortalDialog.tsx
stores/
use-app-settings-store.ts # Theme, autostart, update prefs, analytics toggle
use-license-store.ts # Polar validation, offline cache, device activation
hooks/
use-app-updater.ts # GitHub releases check + license expiry gate
use-window-bounds.ts # Persist/restore window size and position
use-polar-checkout.ts # Embedded Polar checkout
lib/
db.ts # SQLite migration pipeline (add tables here)
polar-config.ts # Polar env var config
posthog.ts # Analytics init
logger.ts # Pino + optional Axiom transport
schema-migration.ts # JSON file versioning pipeline
secure-storage.ts # Wrappers for Rust AES-256-GCM commands
src-tauri/src/
lib.rs # Tauri plugins, window setup, command registration
secure_storage.rs # AES-256-GCM encrypted key-value store
- Extend the
Pagetype inApp.tsx - Add a route in the
Apprender - Add navigation (e.g., a button in TitleBar
actions)
Add a migration in src/lib/db.ts. Bump TARGET_SCHEMA_VERSION and add a migration function. The pipeline runs automatically on startup.
Call the Tauri commands via the wrappers in src/lib/secure-storage.ts (or add wrappers following the same pattern). Data is encrypted with AES-256-GCM keyed to the device.
Add fields to use-app-settings-store.ts. The store auto-persists to Tauri plugin-store on every setter call.