Architecture Overview

HAProxy UI is a SvelteKit application with an optional Tauri 2 shell. The important structural decision is that all outbound I/O is brokered: in desktop mode the webview never talks to the network, and never touches a secret.

Stack

  • SvelteKit 2 · Svelte 5 (runes) · TypeScript

  • Tailwind CSS 4 · shadcn-svelte / bits-ui components

  • @xyflow/svelte — the graph canvas

  • TanStack Query for server state · Zod for every schema

  • diff for the red/green configuration diff

  • Tauri 2 + Rust (reqwest, git2, keyring) for the desktop build

  • Vitest for unit tests · Playwright for end-to-end

The broker boundary

┌─────────────────┐ invoke() ┌────────────────────┐ outbound ┌──────────────────────┐
│ Webview (UI) │ ─────────▶ │ Rust commands │ ─────────▶ │ Dataplane API │
│ Svelte + flows │ ◀───────── │ (src-tauri/src) │ ◀───────── │ git / OS keychain │
└─────────────────┘ events() └────────────────────┘ └──────────────────────┘

The frontend calls invoke('cmd', args); the backend pushes back with emit('event', payload). The four Rust command modules are:

Module

Responsibility

Backing library

commands/dataplane.rs

All HTTP to the Dataplane API, with the token injected from the keychain

reqwest

commands/secrets.rs

Read/write/delete per-instance API tokens

keyring

commands/instances.rs

Instance list persistence

serde JSON

commands/git.rs

clone, commit_and_push, pull, rebase, force_push, git_status, read_haproxy_cfg, write_text_file

git2, ssh-agent

Because the Dataplane request originates in Rust, the desktop build is not subject to the browser’s CORS constraints. It also means the bearer token is resolved inside Rust on each request and never crosses the webview boundary.

In browser mode the same UI runs against fetch() and localStorage. The client picks its transport at runtime by detecting the Tauri runtime — src/lib/api/client.ts dispatches to the Rust path when present.

Connection modes

Mode

Selected when

CORS

Notes

Mock

Instance has no baseUrl

n/a

In-memory fixture, 150 ms latency. The canvas is fully usable with no cluster.

Vite proxy

baseUrl is the relative path /dataplane

None — same origin

Recommended for browser dev. The dev server forwards /dataplane/* to the Dataplane.

Direct

baseUrl is an absolute URL

Requires cors_allowed_origins on the Dataplane

Production-style. Put both behind one reverse proxy, or configure the Dataplane.

Tauri

Tauri runtime detected

Irrelevant

Request originates in Rust. Any reachable URL works.

Client selection

createClient(instance) returns one of two implementations:

  • mockClient — returns a fixture after a 150 ms delay. No network.

  • realClient — talks to the Dataplane v3 REST surface.

Failures are raised as HaproxyApiError, which carries a kind field so the UI can show the right remediation rather than a generic error:

kind

Meaning

auth

401 / 403 from the Dataplane

conflict

The live configuration moved underneath you. Split out from server so the apply path can offer “Reload & re-diff”.

server

5xx, or another non-OK 4xx

network

Fetch failed or timed out

parse

Response body was not JSON, or did not match the schema

Source layout

  • src/lib/api/ — assemble.ts, normalize.ts, toDataplane.ts, parseHaproxyCfg.ts, diff.ts, apply.ts, client.ts, types.ts (Zod), mocks.ts, v3.ts

  • src/lib/components/ — Visualizer.svelte (canvas), the four inspectors, ImportSheet, DiffPanel, VersionHistorySheet, StatsDashboard, dialogs

  • src/lib/graph/ — xyflow layout and node/edge styling

  • src/lib/nodes/ — FrontendNode, BackendNode, ServerNode renderers

  • src/lib/queries/ — TanStack Query hooks bound to createClient()

  • src/lib/drafts/ — the in-memory working-draft store

  • src-tauri/src/ — Rust commands

Race safety

The node inspector carries a mutex (NodeInspector.mutex.ts). Concurrent edits to the same node queue rather than clobbering each other — which matters because the graph, the inspector and the draft store can all be writing in the same tick during a multi-field edit.

Graph configuration is fixed

Every @xyflow/svelte instance uses the same interaction config, and initial node positions in src/lib/graph/layout.ts are integer multiples of the 16px snap grid. This is not cosmetic — it is the contract that makes the canvas behave identically under finger, trackpad and mouse. See iPad Roadmap before changing it.

panOnDrag: [1, 2],
zoomOnScroll: false,
zoomOnPinch: true,
panOnScroll: false,
selectionOnDrag: false,
selectionKeyCode: 'Meta',
snapGrid: [16, 16]


Was this article helpful?