# We ran our desktop app in a browser tab, then took it out

> For two weeks, Cydonia's home page ran the real app, a GPUI workspace compiled to WebAssembly over projects held in memory. It worked. It was also far too heavy for a landing page. What it took, and what we kept.

From 0.1.17 on 26 September until 10 October, the hero of cydonia.sh was not a video. It was Cydonia: the same window the desktop app opens, compiled to WebAssembly, running over two projects held in memory, with a stand-in agent answering in the session. You could open the tour's article, its board and the session behind the cards. It worked, and then we took it out, because it was too heavy for a page whose job is to load fast. This is how it was built, and what outlived it.

## One store, two backends

Cydonia keeps everything as files in the project: articles as Markdown, boards and sessions beside them. A browser tab has no files. So the first step had nothing to do with the web: every read and write of boards, sessions, articles and entry numbers went through one `Project` backend, a closed enum over the filesystem and a new in-memory backend seeded from a `.cydonia` layout.

The right panel's file list got the same treatment, as a `Files` trait with a `Disk` and a `Memory` implementation and a type alias naming this build's:

```rust
#[cfg(feature = "desktop")]
pub type Here = Disk;
#[cfg(not(feature = "desktop"))]
pub type Here = Memory;
```

## What a browser cannot have

Everything that needs a native host went behind one `desktop` feature: agents over ACP, the MCP server, the terminal, global hotkeys, the updater, SQLite and live syntax grammars. With the feature off, the GUI crate builds for `wasm32-unknown-unknown`. The demo crate was what remained: the welcome and tour projects as bytes, a stand-in agent, and a call to start the app on GPUI's web platform.

Three things needed working around rather than switching off.

**The run loop is the browser's.** On the web, GPUI's `run_embedded` returns at once, so the demo hands back the one owner of everything it built, and that owner is what keeps the app alive.

**No grammars.** The desktop app installs tree-sitter grammars on demand and runs them in wasmtime. A browser build cannot do either. So the demo's build script highlighted every code fence in the demo projects on the host, at compile time, and baked the spans in as a table. The fences you saw were painted from precomputed spans.

**No system fonts.** The browser hands a canvas no fonts at all, so Inter and Lilex were bundled into the binary, five TTFs. GPUI's web platform maps its interface and monospace families to fonts we did not ship, so anything drawn outside the theme, a drag preview for one, fell back.

## Shipping it

The site built the demo on every deploy: `cargo build --release` for wasm32, `wasm-bindgen --target web`, then `wasm-opt -Oz`. Rebuilt from that commit today, the result is 14.4 MB of wasm after `wasm-opt`, plus 160 KB of glue JS. Compressed, it is 5.7 MB with gzip at its highest setting, and 3.9 MB with Brotli at its highest.

Two more pieces made it survive the real web:

- **iOS.** wasm-bindgen's loader falls back from streaming compilation only when the `Content-Type` is wrong. WebKit, which is every browser on iOS, can fail streaming compilation of a compressed response, and Cloudflare compresses the wasm with Brotli. That failure was re-thrown, not fallen back from, so the build patched the loader to skip streaming on iOS, and failed loudly if wasm-bindgen's template ever changed.
- **Cache pairing.** The glue JS and the wasm were stamped with a hash of both, so a browser could never pair one build's JS with another's wasm. The wasm's size was stamped in too, for the loading bar.

The page fetched the wasm only once the hero scrolled into view, or when a reader pressed run over the recording, and laid the app out at 1,080 pixels wide, scaled down in a narrower box rather than reflowed.

## Why it came out

A landing page has one job, and a download of that size before anything moves is not it. Until it arrived, the hero was a loading bar. The demo proved the point it was built to prove: Cydonia's workspace is not tied to a disk, or even to a desktop. It did not need to keep proving it on every visit. The hero is a recording now.

## What stayed

The demo crate is gone, but almost nothing it needed was demo-specific:

- The one-backend store is how Cydonia reads and writes projects today, and the in-memory backend is still there.
- The `desktop` feature still draws the line around everything that needs a native host, and the GUI crate still builds without it.
- `bezel-browser`, the webview behind Cydonia's browser tabs, still has a web build: an `<iframe>` above the canvas.
