12k
All articles

Building Desktop Apps With deno desktop

Deno desktop compiles TypeScript apps into native desktop binaries, with webview or CEF backends, framework support, cross-compiling, and limits.

OpenReplay Team
OpenReplay Team
Building Desktop Apps With deno desktop

deno desktop compiles a Deno project into a self-contained desktop app: your code, the Deno runtime, and a rendering engine in one binary per platform. It shipped with Deno 2.9. What sets it apart from Electron and Tauri is that the rendering engine is a build-time choice. You pick the operating system’s WebView or a bundled Chromium, and none of Electron, Electrobun, Tauri, or Dioxus offers that switch.

Anyone who has shipped an Electron app knows the 100 MB installer conversation, and anyone who has shipped a Tauri app knows the bug that reproduces only on WebKitGTK. Those two frameworks sit at opposite ends of one axis, and until now choosing a framework meant choosing a rendering engine for good.

This is a first look at the third option, aimed at readers who already know the Electron and Tauri trade-off. It covers what the command produces, the smallest program that shows the model, what each backend costs in megabytes, what framework projects it accepts unmodified, how cross-compilation works, and what is still missing. The Electron versus Tauri argument itself is covered in a separate comparison of Electron and Tauri.

Key Takeaways

  • deno desktop shipped in the stable Deno 2.9.0 release on June 25, 2026, and the release post still labels it experimental with some platform features not yet landed.
  • The default webview backend renders through WebView2 on Windows, WKWebView on macOS, and WebKitGTK on Linux; the optional cef backend bundles Chromium for identical rendering on every platform.
  • Deno’s comparison page puts a WebView-backed app at roughly 40 MB and a CEF-backed app at roughly 150 MB, against ~100 MB+ for Electron and ~2–10 MB for Tauri.
  • A Deno.serve() handler inside a desktop entrypoint takes no port or hostname; the runtime binds it to the address the window loads.
  • --target and --all-targets cross-compile to five platform triples from any host with no Rust toolchain, with the macOS .dmg as the one host-bound exception.

What Is deno desktop?

Point deno desktop at a Deno entrypoint or at a web framework project, and what comes back is a desktop application that draws its interface in a webview and runs its logic in Deno. The 2.9 release post introduces it as the headline feature and, in the same section, marks it experimental with the API surface still stabilizing. It landed in stable 2.9.0, not a canary, as the v2.9.0 release notes record under feat: deno desktop subcommand.

Three things go into the binary for each platform: your code, the Deno runtime, and a rendering backend. Communication between the Deno side and the webview goes through in-process channels rather than socket-based IPC, which is a different model from Electron’s main-and-renderer message passing. Native surfaces such as Deno.BrowserWindow and Deno.Tray are built into the runtime, so window control and system-tray icons need no third-party package.

The Smallest deno desktop Program

The minimum desktop app is a single Deno.serve() handler that returns HTML, plus one command.

// main.ts
Deno.serve(() =>
  new Response("<!DOCTYPE html><h1>Window one</h1>", {
    headers: { "content-type": "text/html" },
  })
);
deno desktop main.ts

Note the missing argument. In a normal Deno server you would pass a port; here you leave it out, because in a desktop entrypoint the runtime hands the handler the address the window is already pointed at. The compiled app opens a native window on that local server. This is the one non-obvious part of the programming model: the HTTP server is the UI transport, and the runtime wires the two ends together for you.

Should You Use the webview or cef Backend?

The rendering engine is a build-time decision, and it is the reason deno desktop occupies a slot neither competitor fills. Electron bundles only Chromium and Tauri uses only the system WebView. deno desktop does either, selected with --backend or the desktop.backend field in deno.json.

deno desktop main.ts                  # webview (default)
deno desktop --backend cef main.ts    # bundled Chromium

The backends page names the engines behind the default: WKWebView on macOS, WebView2 on Windows, WebKitGTK on Linux. It also documents two limits of that backend that matter for anyone evaluating it: DevTools are CEF-only, and WebGPU on Linux requires CEF. The cef backend puts a copy of the Chromium Embedded Framework inside the app bundle, which the same page sizes at ~150 MB for the framework alone.

Deno’s comparison page lists the resulting app sizes as approximations:

ToolEngineApp size (Deno’s figure)
deno desktop, webviewOS WebView~40 MB
deno desktop, cefBundled Chromium~150 MB
ElectronBundled Chromium~100 MB+
TauriOS WebView~2–10 MB

The distribution page gives a separate data point: a hello-world on the WebView backend weighs about 66 MB, and --compress brings that down to 19 MB. Treat all of these as Deno’s own figures rather than measurements of your app.

The release post’s one-line rule is to stay on webview unless you need an identical engine everywhere. Extending that with the documented limits gives a usable decision: default to webview; switch to cef when your UI depends on Chromium-specific rendering, when you cannot test on all three OS engines, when you need DevTools during development, or when you need WebGPU on Linux. The price of switching is the gap between the two rows above.

Which Frameworks Does deno desktop Detect?

Pointed at an existing framework project, deno desktop . works out which framework it is from the config file or package.json, embeds the build output, and runs the framework’s production server as the Deno.serve() handler. For Next.js, Astro, Fresh, and Nuxt, the per-framework notes show nothing more than the framework’s own build command followed by deno desktop ., with no adapter and no extra configuration. SvelteKit is detected too, but only through the Deno Deploy adapter’s or Node adapter’s output; React Router needs a Deno-compatible app/entry.server.tsx.

Run the framework’s build first. The command packages what that build produced; it will not trigger next build or astro build on your behalf.

npx next build        # produce .next/
deno desktop .        # package the built server
deno desktop . --hmr  # development: framework dev server with hot reload

With --hmr, the app window points straight at the framework’s own dev server, so development feels much like working in a browser tab: state survives an edit, fast refresh works, and errors arrive through the usual overlay.

How Do You Cross-Compile deno desktop Apps?

--target <triple> builds for one other platform and --all-targets builds for every supported one, from any host, with no Rust toolchain. The distribution page lists five targets: aarch64-apple-darwin, x86_64-apple-darwin, x86_64-pc-windows-msvc, aarch64-unknown-linux-gnu, and x86_64-unknown-linux-gnu.

deno desktop --target x86_64-pc-windows-msvc main.ts
deno desktop --all-targets main.ts

The mechanism is download, not compilation. For the target you asked for, the CLI pulls down a ready-built denort and a ready-built backend archive, checking both against their SHA-256 hashes before use. That is why it works where Tauri and Dioxus do not. Their Rust toolchains must compile on the target platform, so each OS needs its own build machine. Electron can cross-build through electron-builder, so the contrast is specifically with the Rust-based tools. The one exception on Deno’s side is the macOS .dmg, which shells out to hdiutil and must be produced on a Mac.

Maturity, Honestly

Electron runs Slack, Visual Studio Code, and Notion; Tauri 2 has years of releases and mobile targets behind it; deno desktop arrived with 2.9.0, and every 2.9 patch release since has carried desktop fixes, 2.9.6 included. The docs are candid about what is not there yet.

Output formats are further along than the comparison page’s gap list suggests. The distribution page documents .app and .dmg on macOS, an app directory or .msi on Windows, and an app directory, .AppImage, .deb, or .rpm on Linux, chosen by the --output extension. The .msi, .deb, and .rpm installers shipped in 2.9.0 itself, per the release notes.

The confirmed gaps:

  • Windows auto-update. Only macOS and Linux actually complete the update: they apply the staged patch and, if the new version fails to start, fall back to the old one. Windows downloads and stages a patch but never swaps it in, so nothing changes on the next launch. The auto-update page says to treat Windows auto-update as unsupported for now.
  • Notarization. Signing runs automatically on a macOS host, but the code-signing section points out that you still have to notarize by hand, with xcrun notarytool submit as its own step.
  • Mobile. No iOS or Android targets, where Tauri 2 has both.
  • Secure storage, runtime permission prompts, shared CEF runtime. The comparison page lists all three as absent or on the roadmap; the shared runtime is the item that would shrink CEF apps, and it is a promise, not a release.

Who Should Try deno desktop Now?

Try it now if you already run Deno, your team writes TypeScript and not Rust, and the app is an internal tool or a desktop wrapper around an existing Next.js, Astro, Fresh, or Nuxt codebase where a 40 MB WebView build is acceptable and a macOS or Linux audience covers auto-update. Wait if you ship to Windows users who need in-place updates, need mobile from the same codebase, or need a distribution pipeline with years of signing and installer tooling behind it. The experimental label in the release post is accurate: the model is sound and the commands work as documented, but the surface can still move between patch releases.

Conclusion

deno desktop is a real third option because it separates the rendering-engine decision from the framework decision, and it charges a documented price for each side of that choice. The cheapest way to evaluate it is to run deno desktop . inside a framework project you already have, once on the default backend and once with --backend cef, and compare the two artifacts against the gaps listed above.

FAQs

How does the webview call Deno code in a deno desktop app?

Through bindings. On the Deno side you attach a handler to a window with win.bind(name, handler); page JavaScript then calls bindings.name(args) and gets back a promise carrying whatever the handler returned. Each window keeps its own set of bindings, so a handler registered on one Deno.BrowserWindow is invisible to another. The call travels over in-process channels instead of socket IPC, and when a handler throws, what reaches the webview is a plain object carrying name, message, and stack rather than a real Error.

What is the raw backend in deno desktop and when should you use it?

It runs a desktop app with no web engine at all. You keep windows, input events, and the native API surface, but there is nothing to render HTML into: no webview, no automatic Deno.serve() binding, and no bindings proxy. It suits apps that paint their own interface with WebGPU, Skia, or custom drawing code. The only way to select it is the desktop.backend field in deno.json, since the --backend flag accepts cef and webview only.

Can I switch a deno desktop app between the webview and cef backends without changing code?

Yes. Deno's backends page treats CEF and WebView as interchangeable: windows, bindings, events, navigation, and JS execution all behave the same on either one, and only the raw backend breaks that portability. Building for a backend or target you have not used before pulls down a ready-built archive first, a few hundred megabytes in CEF's case, checksum-verified and kept in the Deno cache directory so later builds skip the download.

Do Deno permissions apply inside a deno desktop app?

Yes, but they come from the permissions compiled into the binary rather than from prompts. A binding executes inside the Deno runtime with whatever the process was granted, so a handler that reads a file needs read access granted at startup, and nothing the webview does can widen that. The docs are clear that no separate permission prompt fires at runtime, so treat anything a binding accepts from the page as untrusted input and validate it.

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.