The Seed desktop app is a browser for the Hypermedia network, an editor, and an archive at once. It runs a full node on your machine, so everything you open is kept locally and served to peers. Everything you write is signed with keys that never leave your device. It runs on macOS, Windows and Linux and is downloadable from seed.hyper.media.
Where the code is
frontend/apps/desktop, package @shm/desktop: Electron 39 with Electron Forge and Vite 6, React 18, tRPC over Electron IPC, React Query, Tailwind. The main process is src/main.ts, the preload is src/preload.ts, and the renderer starts at src/root.tsx and dispatches routes from src/pages/main.tsx. The editor is @shm/editor. The agents interface is the shared @shm/ui agents code. Run it with ./dev run-desktop after ./dev has built the daemon, or pnpm desktop alone.
Processes and ports
The app is four processes. The main process spawns the daemon with -http.port, -grpc.port, -p2p.port and -data-dir, waits for /debug/version, then starts its own HTTP server and finally a local agents server.
Piece | Prod | Dev |
|---|---|---|
Daemon P2P / HTTP / gRPC / metrics | 56000 / 56001 / 56002 / 56003 | 58000 / 58001 / 58002 / 58003 |
Desktop API bridge | 56004 | 58004 |
Agents server | 3050 | 3051 |
Electron user data |
|
|
The renderer and the main process both talk to the daemon over gRPC-web on the HTTP port. TypeScript does not use the native gRPC port. The renderer and main process talk to each other over tRPC. The API bridge on 56004 serves the same typed /api/<Key> protocol as a web site, from the same code. So the CLI, the local agents server and the renderer itself can use the Seed API against the local node. The bridge refuses cross-site browser requests.
Two things run once per app in the main process rather than once per window. The sync service, src/app-sync.ts, keeps a reference-counted list of the resources any window is showing, runs discovery for each of them on a loop, polls the daemon's activity feed, and broadcasts React Query invalidations to every window. Its intervals stretch ten times when no window is focused. The notification store described on Notify lives there too. Windows also share hover state: pointing at a block or a resource broadcasts hypermediaHoverIn over IPC, and every window highlights the elements with the matching data-blockid or data-resourceid.
Where it keeps things
User data is Electron's per-platform application-data folder: ~/Library/Application Support/Seed on macOS, ~/.config/Seed on Linux, %APPDATA%\Seed on Windows. Inside it, daemon/ is the daemon's data directory with the SQLite database, the device key and the encrypted vault. drafts/ holds drafts as files. Drafts and navigation state belong to the app, and the daemon does not store them. So drafts do not sync between devices. Published documents and private documents do sync. In development the folder is Seed-local, so a dev app and a production app can run side by side.
How editing works
Every document goes through the same four states: loading, viewing, editing, publishing.
Loading. The app asks the local daemon. If the document is not there, the daemon discovers it from peers.
Viewing. Read-only. If you have edit access and left a draft, the app reopens it in editing mode.
Editing. Autosave writes a draft about half a second after you stop typing. It creates the draft on the first change and updates it after that. Saves queue, so keystrokes during a save are never lost. Cancel discards the draft.
Switching to an account without edit access saves the draft and leaves editing. If a newer version is published elsewhere while you edit, the app notes it and does not force it on you. The whole flow is one state machine in @shm/shared, shared with the web app.
"New" on a document creates a child draft in place: a card with an editable title appears at the bottom of the parent, and opening it goes to the full editor. Every unpublished child draft of the document you are viewing shows as a card there. The parent is not changed until the child publishes, so there is nothing to discard on it. The write is built by buildInlineDraftWrite in @shm/shared and stored like any other draft.
The editor
@shm/editor (frontend/packages/editor) is a TipTap 2 editor over ProseMirror, derived from BlockNote. Its ProseMirror schema is three layers: blockChildren (a list with a listType of Group, Unordered, Ordered or Blockquote) holds blockNodes, and each blockNode holds one content block (paragraph, heading, image, embed, query and so on) and an optional blockChildren of its own. The rendered DOM carries data-node-type, data-content-type and data-id so styles and tests can target blocks without depending on class names. The block format on the wire is the SDK's HMBlock; the conversion both ways lives in the SDK (hmblock-to-editorblock), and the editor never changes it.
Pasted HTML arrives from ProseMirror's parser as a flat list, so transformPasted in BlockChildren.ts hands it to normalizeFragment.ts, which rebuilds the nested blockNode structure before insertion.
Typography is one class. .hm-prose, defined in @shm/ui (src/hm-prose.css) on top of @tailwindcss/typography, sets the font stacks, heading scale and block rhythm in relative units, keyed off the data-node-type and data-content-type attributes. The same class is applied to the draft editor, the read-only viewer, embeds, comment bodies (.hm-prose.is-comment) and the server-rendered HTML on the web, so a document looks the same in all of them. Size modifiers .hm-prose-sm, -base, -lg and -xl scale a whole surface.
Inspecting the raw data
Two desktop-only surfaces expose the protocol directly.
The metadata editor is a tab on every document. It edits metadata as a form or as JSON and stages patches through the same document machine as the settings panel. Removing a key stages an explicit null, so the published change clears it. Renaming stages a removal and an addition. Lists are disabled because the attribute operation has no list value type yet.
The blob editor, the raw-blob route, opens any ipfs:// CID that decodes as DAG-CBOR and shows it as DAG-JSON. You can edit it and publish it as a new blob with a sha2-256 CID. It reads through the daemon's /ipfs/<cid>.dagjson route, which searches the network if the blob is not local. Links and bytes are converted back to real IPLD values before encoding, so republishing a blob with links does not corrupt them.
Working with it
CLI
seed-cli space dev publishes a folder into the running dev app under a throwaway key and writes edits made in the app straight back to disk. ./dev hm-sync runs that loop for this documentation folder. The CLI can also read the app's vault and keys. See Keys.
SDK
The renderer publishes through @seed-hypermedia/client, the SDK, against the API bridge. It signs with the daemon's SignData RPC, so the private key never enters the renderer.
Web API
http://localhost:56004/api/<Key> is the local Seed API. The bundled agents server uses this endpoint.
Agents
The desktop app starts a local agents server and shows the agents interface. You can configure hosted or self-hosted agent servers in its place. The chat, tools and triggers are described under Seed Agents.
Notifications
The inbox page reads a local copy of the account's notification state that the main process syncs with the notify service, signing requests through the daemon. Marking things read works offline and syncs later. Notify describes the store and the read-state rules.
Development notes
pnpm desktop dev:debug opens Chrome DevTools Protocol on 9222. Automated drivers use it to screenshot and navigate the dev app.
./dev 2-run-desktop starts a second packaged instance on alternative ports with its own user data folder.
Unit tests run with pnpm test:unit. The end-to-end suite packages the app and drives it with Playwright. Auto-updates on Linux are custom. See the release process in Contributing.
See also
Seed Hosting
After the first public publication of a new space, an invitation offers Publish to a Domain, opening the hosting workflow to choose a free Seed address or a custom domain. Not now dismisses the invitation. Desktop and web use the same domain and hosting choices, self-hosting guide, setup-URL form, and completion flow. Self-hosting uses the setup URL without a Seed Hosting login. Space settings has a Web Domain page for publishing and removing a web domain. Seed Hosting sign-in uses a four-digit emailed code, or trusted vault verification for eligible new hosting accounts. Logging out of the desktop app also clears its hosting credentials and cached sites, and revokes the hosting session when the service is reachable.
Change Site Address and Transfer Hosting are available only when the signed-in hosting account owns a site registered to this space and its published URL matches that site's address or custom domain. Self-hosted sites do not expose these controls. A hosting transfer moves management to an existing hosting account; it does not transfer the Seed identity or content permissions. Active paid services must stop before a transfer, and dedicated hosting must stop before a rename.
Renaming preserves the site identity and custom domains. The desktop updates the space's published URL when it points to the old hosting address; a custom primary domain stays unchanged. Pending publication updates are retained so an interrupted move can be completed without moving the hosting address again.
Do you like what you are reading? Subscribe to receive updates.
Unsubscribe anytime