Why a Desktop GUI for Workers KV

Workers KV is Cloudflare’s globally replicated key-value store, designed for low-latency access from Worker scripts no matter where a request lands. It’s a solid product with a generous free tier. But one thing it lacks is local introspection. When your application stores hundreds of thousands of keys, there’s no built-in way to browse, sort, or eyeball what’s actually in there.

That gap inspired Workers KV GUI, a cross-platform desktop application built with Svelte, Redis, and Rust. The goal: give developers a way to connect to a shared external cache, sync keys from the Cloudflare REST API on demand, and browse the data without incurring API costs just to look at it.

Building the SPA with SvelteKit

The front end is a standard single-page application, developed entirely in the browser with the usual web tooling. SvelteKit was the framework of choice because it handles the build system, routing, and developer experience — including hot module replacement — so you can focus on the app itself. If you have a favorite front-end stack, any of them would work here.

Since the target is a pure client-side app, the only real configuration change was opting out of SvelteKit’s default server-side rendering assumption. That means attaching @sveltejs/adapter-static and telling the framework to build an SPA:

// svelte.config.js
import preprocess from 'svelte-preprocess';
import adapter from '@sveltejs/adapter-static';

/** @type {import('@sveltejs/kit').Config} */
const config = {
  preprocess: preprocess(),

  kit: {
    adapter: adapter({
      fallback: 'index.html'
    }),
    files: {
      template: 'src/index.html'
    }
  },
};

export default config;

SvelteKit’s filesystem-based routing neatly maps to the two views needed:

  1. src/routes/index.svelte — the connection setup screen
  2. src/routes/viewer.svelte — the data browsing screen

In a desktop shell, the URL bar isn’t visible, so route naming is just a matter of internal consistency. Initial development ran in a browser with mock data until the views and interactions felt complete — about two days of work.

From Web App to Desktop App

Electron is the best-known way to wrap web assets in a desktop application, but it bundles a full Chromium browser and a Node.js runtime, pushing install sizes past 100 MB and consuming significant system resources at runtime.

The alternatives evaluated were Svelte NodeGui and Tauri. NodeGui relies on Qt for native views but requires adjusting app code to translate components. Tauri takes a different route: it uses the operating system’s native webviewer (WebKit on macOS and Linux, Edge WebView on Windows) and a Rust backend, so your front end stays pure web development with zero framework-specific changes.

The size savings are dramatic:

  • A bare minimum Tauri app: under 4 MB
  • A bare minimum NodeGui app: about 16 MB
  • A bare minimum Electron app: easily 120 MB

Tauri won, and integration was straightforward. After adding @tauri-apps/cli, initialization creates a src-tauri directory for all Tauri-specific files:

yarn add --dev @tauri-apps/cli
yarn tauri init

Most configuration defaults were fine, aside from product name and window title. The build section needs to point at SvelteKit’s output:

// src-tauri/tauri.conf.json
{
  "package": {
    "version": "0.0.0",
    "productName": "Workers KV"
  },
  "build": {
    "distDir": "../build",
    "devPath": "http://localhost:3000",
    "beforeDevCommand": "yarn svelte-kit dev",
    "beforeBuildCommand": "yarn svelte-kit build"
  },
  // ...
}

distDir points to the built production assets, resolved relative to tauri.conf.json, hence the ../ prefix. The devPath is the URL for SvelteKit’s dev server, which runs on port 3000 by default.

Tauri’s own dev and build commands handle the desktop encapsulation. The beforeDevCommand and beforeBuildCommand hooks let you chain SvelteKit into the pipeline, keeping scripts clean at the root level:

{
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "tauri dev",
    "build": "tauri build",
    "prebuild": "premove build",
    "preview": "svelte-kit preview",
    "tauri": "tauri"
  },
  // ...
  "devDependencies": {
    "@sveltejs/adapter-static": "1.0.0-next.9",
    "@sveltejs/kit": "1.0.0-next.109",
    "@tauri-apps/api": "1.0.0-beta.1",
    "@tauri-apps/cli": "1.0.0-beta.2",
    "premove": "3.0.1",
    "svelte": "3.38.2",
    "svelte-preprocess": "4.7.3",
    "tslib": "2.2.0",
    "typescript": "4.2.4"
  }
}

After this setup, the development loop stays as before: run yarn dev, get both SvelteKit’s HMR server and the Tauri application window, with the webview proxying localhost:3000. The production command remains a single yarn build that builds both layers sequentially.

One caveat: Tauri was still in beta during development, though it felt stable and well-designed. The project’s Discord is active, including maintainer responses.

Why Rust for the Backend

The webview could make authenticated REST calls to the Workers KV API and stash results in localStorage or IndexedDB. That works for a single user, but in a team setting everyone would need to pull and store the same data locally on their own machines. With millions of keys, a shared cache makes far more sense for both time and API cost.

Redis is a natural fit because it is also a key-value store. SCAN, sorting, and filtering already exist in Redis, so the app can delegate that work instead of reimplementing it. Standalone or containerized Redis is easy to run locally, and managed services are widely available.

Electron apps get Node.js runtime support out of the box, which conveniently handles things like Redis connections from the client side. Tauri replaces Node.js with Rust, a compiled systems language. Instead of node_modules, you add crates in src-tauri/Cargo.toml, the Rust equivalent of package.json. The redis crate provides the client driver:

# src-tauri/Cargo.toml
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.0.0-beta.1", features = ["api-all", "menu"] }
redis = { version = "0.20", features = ["tokio-native-tls-comp"] }

The "tokio-native-tls-comp" feature is required for TLS support, per the crate documentation.

Bridging Svelte and Rust

Communication between the Svelte front end and the Rust backend flows through Tauri’s command system. The #[command] macro wraps function definitions so that Tauri can handle context and argument parsing. Rust’s type system is strict about argument types, so a command that expects a string and an integer will fail if either is missing:

use tauri::{command};

#[command]
fn greet(name: String, age: u8) {
  println!("Hello {}, {} year-old human!", name, age);
}

Commands must be registered in the tauri::Builder composition within the main function:

use tauri::{command};

#[command]
fn greet(name: String, age: u8) {
  println!("Hello {}, {} year-old human!", name, age);
}

fn main() {
  // start composing a new Builder chain
  tauri::Builder::default()
    // assign our generated "handler" to the chain
    .invoke_handler(
      // piece together application logic
      tauri::generate_handler![
        greet, // attach the command
      ]
    )
    // start/initialize the application
    .run(
      // put it all together
      tauri::generate_context!()
    )
    // print <message> if error while running
    .expect("error while running tauri application");
}

Once registered, the front end can reach the command through the invoke function from the @tauri-apps packages, or via the window.__TAURI__ global. Arguments are passed as an object whose keys must match the Rust function parameter names:

A component diagram of a basic Tauri application.
The developer is responsible for webview contents and may optionally include custom Rust modules and/or define custom commands. Tauri controls the webviewer and the event bridge, including all message serialization and deserialization.
<!-- Greeter.svelte -->
<script>
  function onclick() {
    __TAURI__.invoke('greet', {
      name: 'Alice',
      age: 32
    });
  }
</script>

<button on:click={onclick}>Click Me</button>

Output from println! in Rust appears on the terminal running tauri dev, which is the console used for backend logs:

Hello Alice, 32 year-old human!

Commands can also return values back to the client:

use tauri::{command};

#[command]
fn greet(name: String, age: u8) {
  // implicit return, because no semicolon!
  format!("Hello {}, {} year-old human!", name, age)
}

// OR

#[command]
fn greet(name: String, age: u8) {
  // explicit `return` statement, must have semicolon
  return format!("Hello {}, {} year-old human!", name, age);
}

To avoid repeating invoke boilerplate, a thin client-side helper wraps the calls:

// @types/global.d.ts
/// <reference types="@sveltejs/kit" />

type Dict<T> = Record<string, T>;

declare const __TAURI__: {
  invoke: typeof import('@tauri-apps/api/tauri').invoke;
}

// src/lib/tauri.ts
export function dispatch(command: string, args: Dict<string|number>) {
  return __TAURI__.invoke(command, args);
}

With that in place, Svelte components can make succinct calls to the backend:

<!-- Greeter.svelte -->
<script lang="ts">
  import { dispatch } from '$lib/tauri';

  async function onclick() {
    let output = await dispatch('greet', {
      name: 'Alice',
      age: 32
    });
    console.log('~>', output);
    //=> "~> Hello Alice, 32 year-old human!"
  }
</script>

<button on:click={onclick}>Click Me</button>

Typical Redis Command Flow

After a Redis connection is established via the setup screen, a SYNC button appears. Clicking it is the only time data gets pulled from the Cloudflare API, precisely to keep costs low. A JavaScript function connects to the REST API, retrieves keys, and dispatches a "redis_set" command for each one. All Redis-specific logic lives in the Rust layer.

Reading data back follows an inverted flow. When the viewer component mounts, it dispatches a Tauri command to list available keys. Clicking a key shows its metadata and expiration, but the value is only fetched on demand via a REFRESH button — again, an intentional cost-control measure. That action calls the REST API once more and updates the single key in Redis.

The Payoff

Beyond the rush to finish before the hackathon demo, this project proved a few things. For JavaScript developers, Rust’s ownership rules and borrow-checker are the steepest part of the learning curve, not Tauri itself. Once the bridge pattern was clear, adding new commands felt like exchanging PING/PONG messages.

On macOS, the built Workers KV GUI weighs under 13 MB. SvelteKit earns credit for a fast setup and instant HMR-driven development, and Tauri for keeping the desktop shell lean while still granting low-level system access when the application needs it. The full project is open source on GitHub, with prebuilt binaries on the releases page.