Skip to content

feat(desktop): adapt Temlet to a Tauri desktop app (macOS/Windows)#35

Merged
rumitvn merged 8 commits into
masterfrom
feat/tauri-desktop
Jun 10, 2026
Merged

feat(desktop): adapt Temlet to a Tauri desktop app (macOS/Windows)#35
rumitvn merged 8 commits into
masterfrom
feat/tauri-desktop

Conversation

@rumitvn

@rumitvn rumitvn commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Summary

Adapts Temlet from a server-heavy Next.js web app into a self-contained Tauri v2 desktop app (macOS/Windows). The React UI runs in the native webview; the existing Next.js backend (all API routes + services — Puppeteer, ffmpeg, Prisma) runs as an embedded local sidecar server. Data moves from PostgreSQL to a local SQLite file, and the packaged app bundles its own Node runtime, Chromium, and ffmpeg, so it runs on a clean machine with nothing pre-installed.

This is the working-POC milestone plus self-contained-runtime hardening. Installers/signing/CI are tracked as follow-ups in DESKTOP_ROADMAP.md.

Architecture

  • Dev (npm run tauri:dev): native window wraps next dev.
  • Release: Rust shell (src-tauri/) boots the bundled standalone server on 127.0.0.1, seeds/migrates SQLite, injects secrets, waits for the port, then navigates the window off a loading splash. Child process killed on exit.

What's included

  • DB: Prisma datasource PostgreSQL → SQLite (@prisma/adapter-better-sqlite3); fresh migration baseline; migrations applied in-place on app update (Prisma-_prisma_migrations-compatible).
  • Bundled runtimes: Node (fetch:node), Chromium for Puppeteer (fetch:chromiumPUPPETEER_EXECUTABLE_PATH), ffmpeg via ffmpeg-static (FFMPEG_PATH).
  • Secrets: stored in the OS keychain (keyring) with an in-app Settings screen; per-install CRON_SECRET; temlet.env legacy fallback.
  • Native UX: folder picker, reveal-rendered-file in Finder/Explorer, notification helper.
  • In-process render monitor via instrumentation.ts.
  • Docs: DESKTOP.md, DESKTOP_ROADMAP.md; CLAUDE.md updated.

Notable fix

Removed process.cwd()-rooted dynamic fs reads in migrate.ts that made Next's file tracer bundle the entire project tree (multi-GB src-tauri/target) into the standalone output.

Verification done (headless)

  • Embedded server boots + serves /api/health, dashboard, and DB-backed routes under the bundled Node with an empty environment (no system Node/PATH).
  • SQLite round-trip (JSON fields, cuid, dates, case-insensitive search); migrations-on-update applies only the new migration and is idempotent.
  • Bundled ffmpeg runs (6.0); Puppeteer launches the bundled Chromium.
  • tsc 0 errors, eslint clean (new/changed files), cargo check debug+release clean, production build green and lean (136 MB standalone).

Test plan (needs a GUI / built app)

  • npm run tauri:dev opens the desktop window; dashboard loads.
  • npm run tauri:build produces a self-contained app; launching it boots the backend, creates SQLite in app-data, and round-trips a render.
  • Settings → enter an API key → Save & Restart → value persists (keychain).
  • Crawler job runs (bundled Chromium); YouTube upload generates a thumbnail (bundled ffmpeg).
  • "Reveal" opens the rendered file in Finder/Explorer.

Not in scope (follow-ups in DESKTOP_ROADMAP.md)

Code signing / notarization, installers, auto-update, desktop OAuth deep-links, CI build matrix, first-run wizard.

v-rumnv added 8 commits June 10, 2026 09:58
Switch the database from a remote/external PostgreSQL server to a local
SQLite file so the app is self-contained for desktop use.

- schema.prisma: provider postgresql -> sqlite (models unchanged; Json
  persists as TEXT, all field types are SQLite-compatible)
- lib/prisma.ts: swap @prisma/adapter-pg for @prisma/adapter-better-sqlite3;
  resolve the DB path from DATABASE_URL with a project-local fallback
- replace the 13 Postgres migrations with a fresh init_sqlite baseline
- drop `mode: 'insensitive'` from contains queries (SQLite LIKE is ASCII
  case-insensitive by default and rejects the mode arg)
- prisma.config.ts / env.example / DATABASE_SETUP.md: SQLite file: URL docs
- deps: add better-sqlite3, remove pg/@prisma/adapter-pg/@types/pg
Package Temlet as a Windows/macOS desktop app (Tauri v2). The React UI runs
in the native webview; the existing Next.js backend runs as an embedded
local server, keeping all API routes and services intact.

- src-tauri: Rust shell. Dev wraps `next dev` (devUrl); release spawns the
  bundled standalone server on 127.0.0.1, seeds the SQLite DB on first run,
  injects secrets from <app-config>/temlet.env, polls the port, then
  navigates the window off a loading splash. Child process killed on exit.
- next.config.ts: output "standalone"; pin outputFileTracingRoot (was
  nesting the bundle with no root server.js), exclude src-tauri (2.2GB ->
  135MB), force-include ffmpeg packages
- scripts/prepare-sidecar.mjs: stage standalone build + static + public +
  seed DB into src-tauri/resources for bundling
- instrumentation.ts: run the render monitor in-process when
  TEMLET_RUN_MONITOR is set (replaces scripts/monitor.js in the packaged app)
- loading/index.html: branded startup splash
- DESKTOP.md, temlet.env.example: desktop build + config docs
- gitignore generated artifacts (target, resources, *.db)
…roadmap

- CLAUDE.md: flag the in-progress Tauri desktop migration up front; correct the
  stack (Next 16, Prisma 7 + SQLite, Tauri v2), commands (tauri:dev/build,
  prepare:sidecar, SQLite DATABASE_URL), layout (src-tauri, instrumentation.ts,
  loading/, prepare-sidecar), the desktop sidecar concept, and secrets/setup docs
- DESKTOP_ROADMAP.md: phased, prioritized checklist for hardening the desktop
  build (bundling runtimes, migrations-on-update, native UX, secret storage,
  desktop OAuth, signing/installers/auto-update, CI)
The packaged app no longer depends on a system Node or an inherited PATH.

- scripts/fetch-node-runtime.mjs: download the official Node binary for the
  target (defaults to host platform/version, ABI-matched to the native modules)
  into src-tauri/resources/runtime/; idempotent, cross-build via TARGET_* env
- lib.rs: resolve_node() prefers the bundled runtime, then TEMLET_NODE_PATH,
  then common locations, then PATH
- tauri.conf.json: bundle resources/runtime; chain fetch:node into the build
- verified: server boots and better-sqlite3 loads under the bundled node with an
  empty environment (no PATH)
The packaged app seeds the SQLite DB only on first run; a later version with a
schema change must migrate the user's existing DB. The Prisma CLI isn't bundled,
so add a minimal migrate-deploy equivalent.

- app/lib/migrate.ts: apply migrations not yet recorded in Prisma's
  _prisma_migrations ledger, via better-sqlite3, in a transaction per migration
  (seeded baseline is recognized and skipped)
- instrumentation.ts: run it before serving when TEMLET_APPLY_MIGRATIONS=1
- lib.rs: set TEMLET_APPLY_MIGRATIONS for the embedded server
- prepare-sidecar.mjs: stage prisma/migrations into the bundle
- deps: add @types/better-sqlite3
- verified: baseline DB applies only the new migration, idempotent on re-boot
…tions

Phase C native UX, plus a build-correctness fix uncovered while verifying it.

Native UX (Tauri plugins dialog/opener/notification):
- app/lib/desktop.ts: isDesktop() + pickDirectory/revealPath/openPath/notify,
  dynamically importing the plugin packages so the web/SSR build is unaffected
- app/hooks/useIsDesktop.ts: useSyncExternalStore-based gate (no hydration mismatch)
- RenderCard: desktop-only "Reveal" button opens the rendered file in Finder/Explorer
- OutputFolderManagerDialog: "Add Folder" via native picker (desktop) / prompt (web)
- register plugins in lib.rs; grant dialog/opener/notification permissions

Fix — Next standalone was bundling the entire project tree (incl. src-tauri/target,
multi-GB) which caused ENOSPC during build:
- root cause: migrate.ts read process.cwd()-rooted paths, making @vercel/nft
  conservatively trace the whole cwd. Source the migrations dir from
  TEMLET_MIGRATIONS_DIR (set by the shell) and stop resolving the DB path via cwd
- prepare-sidecar.mjs: defensively prune any over-copied project files from the bundle
- result: standalone back to ~136MB; verified the env-based migrator + server boot
Replace hand-editing the plaintext temlet.env with secure, in-app config.

- src-tauri/src/secrets.rs: keyring-backed get/set + get_managed_config command;
  per-install CRON_SECRET generated & persisted (no more hardcoded value)
- lib.rs: register commands + restart_app; load keychain config into the server
  env on startup (temlet.env kept as a legacy fallback, keychain takes precedence)
- app/components/SettingsDialog.tsx: desktop-only Settings dialog (API keys,
  Nexrender, YouTube/TikTok, working dir via native picker) with Save & Restart
- RenderHeader: gear button shown on desktop; hosted from the dashboard
- app/lib/desktop.ts: getManagedConfig/setSecret/restartApp bridges
- deps: keyring (apple-native, windows-native), uuid
- verified: tsc, eslint (new files), cargo check (debug+release), production build
…offline

The last pieces for "runs on a clean machine":

- youtube-upload: use the bundled ffmpeg-static binary (FFMPEG_PATH override)
  instead of system ffmpeg; keep the generated-thumbnail fallback
- crawlerService: honor PUPPETEER_EXECUTABLE_PATH (set by the shell)
- scripts/fetch-chromium.mjs: install Chrome-for-Testing into resources/chromium
  via @puppeteer/browsers; record the relative executable path; idempotent;
  cross-build via TARGET_BROWSER_PLATFORM
- lib.rs: set PUPPETEER_EXECUTABLE_PATH (from the chromium marker) and FFMPEG_PATH
- tauri.conf: bundle resources/chromium; chain fetch:chromium into the build
- verified: bundled ffmpeg runs (6.0); Puppeteer launches the bundled Chromium
@rumitvn rumitvn merged commit 78dbd89 into master Jun 10, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants