From Web App to Native Desktop: Building ToolKnit with Tauri and a Dual-Source Installer

rust dev.to

The Problem We Wanted to Solve

Every day, developers, designers, and content creators juggle dozens of small tasks — merge a PDF, convert an audio file, compress an image, generate a QR code, polish some text with AI. The typical workflow? Open a browser, search for an online tool, upload your file, wait, download the result. Repeat for the next task. Repeat for the next tool.

The problems with this approach are obvious:

  • Privacy: You're uploading sensitive documents to random websites
  • Speed: Browser-based tools are slower than native processing
  • Fragmentation: Each tool lives on a different site with different UI
  • Offline: No internet, no tools

We wanted to build one desktop application that bundles all these utilities together — running locally, respecting privacy, and feeling like a native app.

This is the story of how ToolKnit went from a web prototype to a polished Windows .exe with a custom installer and dual-source CDN deployment.


Phase 1: The Web Prototype

Before committing to a desktop framework, we built a web-based prototype to validate the concept. The idea was simple: a single-page app with a sidebar navigation, dark theme, and tool cards for each category.

The prototype covered 9 tool categories:

  1. Home — Dashboard with usage stats and navigation
  2. PDF Tools — Merge, split, compress, encrypt/decrypt, rotate, watermark
  3. Image Tools — Format conversion, batch compress, crop, collage, watermark removal
  4. Audio Tools — Format conversion, clipping, BPM detection, channel processing
  5. Video Tools — Format conversion, compression, screenshot extraction
  6. Text Tools — Full/half-width conversion, case conversion, JSON formatting
  7. Calculator Tools — Scientific computation, unit conversion, base conversion
  8. Creative Tools — QR code generation, color picker, password generator
  9. AI Tools — Translation, polishing, document generation, role play

The web prototype proved the concept, but it had limitations:

  • File processing required server-side APIs (or WASM, which was complex for video/audio)
  • Users had to be online
  • No drag-and-drop file system access
  • Browser security sandbox limited what we could do with local files

We needed to go native.


Phase 2: DR41 — The Tauri Migration

We evaluated several options for the desktop migration:

Framework Bundle Size Native Feel Rust Ecosystem Complexity
Electron ~150MB Good No Low
Tauri ~12MB Excellent Yes Medium
Flutter Desktop ~20MB Good No Medium

We chose Tauri 2.x for three reasons:

  1. Tiny bundle size — The final .exe is ~46MB (including FFmpeg), compared to Electron's typical 150MB+
  2. Rust backend — Native performance for file processing, PDF manipulation, and media conversion
  3. Web frontend — We could reuse our existing HTML/CSS/JS frontend almost entirely

Architecture

┌─────────────────────────────────────────┐
│           ToolKnit Desktop              │
├─────────────────────────────────────────┤
│  Frontend (HTML/CSS/JS)                 │
│  ├── Dark theme + WebGL background      │
│  ├── 9 tool category pages              │
│  ├── i18n (zh/en) with nested keys      │
│  └── Spider mascot loading animations   │
├─────────────────────────────────────────┤
│  Tauri Bridge (IPC)                     │
│  ├── invoke() commands                  │
│  ├── Event listeners                    │
│  └── Drag & drop file handling          │
├─────────────────────────────────────────┤
│  Rust Backend                            │
│  ├── PDF: pdf-lib-plus-encrypt          │
│  ├── Audio/Video: FFmpeg (bundled)      │
│  ├── Image: image crate                 │
│  ├── AI: DeepSeek/OpenAI/Qwen APIs      │
│  └── File system access                 │
├─────────────────────────────────────────┤
│  Native Window                          │
│  ├── Custom titlebar (no decorations)   │
│  ├── System tray with i18n menu         │
│  └── 1100x800 fixed size                │
└─────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Key Technical Decisions

Custom window decorations: We disabled native window decorations (decorations: false) and built a custom titlebar with minimize/maximize/close buttons, plus a "always on top" toggle. This gave us full control over the visual design.

WebGL dynamic background: The home page features a custom WebGL shader that renders a flowing dark fluid animation. We optimized it by disabling anti-aliasing and alpha channels for performance:

const gl = canvas.getContext('webgl', {
  antialias: false,
  alpha: false,
  premultipliedAlpha: false
});
Enter fullscreen mode Exit fullscreen mode

Bundled FFmpeg: Audio and video tools require FFmpeg. Instead of asking users to install it separately, we bundle a minimal FFmpeg binary and download it on first launch if needed. The download source is selected based on the user's language (more on this below).

i18n with nested keys: We use a nested JSON structure for translations, supporting both Chinese and English:

{"home":{"pdfDecrypt":{"title":"PDF Decrypt","dropHere":"Drop your PDF here..."}}}
Enter fullscreen mode Exit fullscreen mode

The language is determined at startup by reading an install_lang.txt file that the installer writes during setup. This file contains the NSIS language ID (1033 for English, 2052 for Simplified Chinese).


Phase 3: The Installer

Distributing a Tauri app is straightforward — npm run tauri build produces an .exe. But we wanted more control over the installation experience.

Custom NSIS Installer

We built a separate Tauri-based installer (toolknit-installer) that:

  1. Presents a visual wizard with language selection (Chinese/English)
  2. Downloads the main program zip from CDN
  3. Extracts it to the user's chosen path
  4. Creates desktop shortcuts and start menu entries
  5. Writes install_config.json and install_lang.txt for the main app to read

The installer itself is a Tauri app too — same tech stack, different purpose. It's only ~11.6MB.

Dual-Source CDN Deployment

Here's where it gets interesting. We serve users in both China and internationally. Network latency between China and overseas CDNs can be significant.

We deployed two object storage sources:

Source Provider Region Use Case
R2 Cloudflare R2 Global (CDN) International/English users
RainS3 RainS3 China (cn-nb1) Chinese users

The download URL is selected based on the user's language preference:

let base_url = if language == "zh" {
    "https://toolknit.cn-nb1.rains3.com/toolknit-desktop.zip"
} else {
    "https://cdn.24picture.com/toolknit-desktop.zip"
};
Enter fullscreen mode Exit fullscreen mode

Both sources are kept in sync using a Node.js upload script that pushes to both simultaneously:

// Upload zip (main program)
await uploadToClient(r2Client, r2Bucket, 'toolknit-desktop.zip', zipPath, 'application/zip', 'R2');
await uploadToClient(rainClient, rainBucket, 'toolknit-desktop.zip', zipPath, 'application/zip', 'RainS3');

// Upload installer exe
await uploadToClient(r2Client, r2Bucket, 'toolknit-installer.exe', installerPath, 'application/octet-stream', 'R2');
await uploadToClient(rainClient, rainBucket, 'toolknit-installer.exe', installerPath, 'application/octet-stream', 'RainS3');
Enter fullscreen mode Exit fullscreen mode

The installer also has a fallback mechanism — if the primary source fails, it automatically tries the other source:

let urls = [
    format!("{}?_t={}", base_url, ts),
    format!("{}?_t={}", fallback_url, ts),
];
Enter fullscreen mode Exit fullscreen mode

We append a timestamp parameter to bust CDN cache, ensuring users always get the latest version.


Phase 4: The Download Page

The final piece was a beautiful download page at toolknit.com. We wanted it to showcase the app's features with smooth parallax scrolling and full bilingual support.

Parallax Scrolling with Layered Effects

Each of the 9 tool categories gets its own scrolling module with alternating image/text layouts. We implemented 4 layers of parallax:

  1. Hero section — Title, subtitle, and description each move at different speeds
  2. Module images — Images translate at a different rate than the scroll
  3. Module content — Text content moves at its own pace, with sub-elements (tag, title, description, features, stats) each having individual data-speed values
  4. Image scaling — Images subtly scale based on scroll position for depth
// Each element has a data-speed attribute
const offset = (elementCenter - viewportCenter) * speed;
el.style.transform = `translateY(${offset}px)`;
Enter fullscreen mode Exit fullscreen mode

Language-Aware Screenshots

The download page supports both English and Chinese. When the user switches languages, not only does the text change, but the screenshots change too — English screenshots show the English UI, Chinese screenshots show the Chinese UI:

const prefix = lang === 'en' ? '1-' : '';
document.querySelectorAll('.module-image img').forEach((img, i) => {
    img.src = `https://24picture.com/${prefix}${i + 1}.webp`;
});
Enter fullscreen mode Exit fullscreen mode

The language preference is saved to localStorage, so returning users see their preferred language automatically.

Download Logic

The download buttons on the page follow the same dual-source logic as the installer:

const DOWNLOAD_URLS = {
    en: 'https://cdn.24picture.com/toolknit-installer.exe',
    zh: 'https://toolknit.cn-nb1.rains3.com/toolknit-installer.exe'
};

function handleDownload(e) {
    e.preventDefault();
    const lang = localStorage.getItem('toolknit-lang') || 'en';
    window.open(DOWNLOAD_URLS[lang], '_blank');
}
Enter fullscreen mode Exit fullscreen mode

Lessons Learned

1. Tauri's CSP is strict but worth it

Tauri enforces a strict Content Security Policy. Every external domain your app connects to must be whitelisted in tauri.conf.json. This forced us to be explicit about our API endpoints and CDN URLs — which is actually a good security practice.

2. File locks on Windows are painful

During development, the Rust compiler would sometimes fail with os error 32 (file in use) because the previous build's .exe was still running. Our solution: a kill script that runs before every build:

taskkill/F/IMtoolknit-desktop.exe2>$nulltaskkill/F/IMcargo.exe2>$null
Enter fullscreen mode Exit fullscreen mode

For first-time full builds, we also set CARGO_BUILD_JOBS=1 to avoid concurrent file access.

3. Dual-source deployment is simpler than it sounds

Using S3-compatible APIs for both Cloudflare R2 and RainS3 means we can use the same AWS SDK code for both. The only difference is the endpoint and credentials. One script, two uploads, done.

4. The installer is part of the product

A great app with a terrible install experience still feels bad. Building a custom Tauri-based installer let us control the entire user journey — from download to first launch — with proper language detection and a clean wizard UI.


Tech Stack Summary

Layer Technology
Desktop Framework Tauri 2.x
Frontend Vanilla HTML/CSS/JS
Backend Rust
PDF Processing pdf-lib-plus-encrypt
Audio/Video FFmpeg (bundled)
Image Processing image crate
AI Integration DeepSeek, OpenAI, Qwen APIs
Icons Lucide Icons
Object Storage Cloudflare R2 + RainS3
Upload Script Node.js + AWS SDK v3
Smooth Scroll Lenis
Background Custom WebGL shader

What's Next

ToolKnit is currently in v1.0 Beta for Windows. We're planning:

  • macOS and Linux support
  • Plugin system for community-contributed tools
  • More AI-powered features
  • Auto-update mechanism

Try It Out

ToolKnit is free and open for everyone. Download the installer for your platform:

Visit toolknit.com to see the full feature showcase, or check out our blog for more development stories.


If you found this article helpful, consider giving it a reaction or following for more posts about desktop development with Tauri, Rust, and modern web technologies.

Source: dev.to

arrow_back Back to Tutorials