Development
How the pieces fit is in architecture.md; this page is for building and testing them.
Requirements
| Tool | Version | Needed for |
|---|---|---|
| Node | ≥ 22 (24 recommended) | the portal, the desktop window's UI, the plugin SDK |
| pnpm | 10 (pinned via packageManager) |
the workspace |
| Rust | ≥ 1.88 | the agent, the desktop app, the cloud relay |
| JDK | 17 | the Android app and the Kotlin suites (Gradle itself comes from the wrapper) |
| Android SDK | platform 34, build-tools 34 | verify:android only |
git clone https://github.com/santoshpanna/FlexDeck.git flexdeck
cd flexdeck
pnpm install
pnpm dev:desktop # the desktop app, with its window
Commands
pnpm start # build the portal, then run the cloud service
pnpm verify # every gate: Node, Rust, Android
pnpm verify:node # typecheck + tests for the TypeScript packages
pnpm verify:rust # fmt, clippy, tests for the Rust workspace
pnpm verify:android # the app plus the pure-JVM Kotlin tests
pnpm dev:web # the portal UI AND the cloud API behind it
pnpm dev:server # just the cloud API, on 127.0.0.1:8787
pnpm portal:dev # just the portal UI, on 127.0.0.1:5173
pnpm dev:desktop # the desktop app (opens the window)
pnpm dev:agent # the headless agent, no window
pnpm desktop:dev # just the window's UI, on 127.0.0.1:5174
pnpm desktop:app # just the window (UI must already be running)
pnpm desktop:build # build the window's UI bundle
pnpm brand:icons # regenerate every icon and favicon from the [f] mark
pnpm verify:plugins # the plugin SDK, template, OBS and the host
pnpm create-plugin <id> "<Name>" # start a plugin from the template
pnpm plugins:package # build every plugin into a .fdpkg
Run pnpm install once from the root. Skipping it is not obvious: the TypeScript
tests pass without it, because Node strips types natively — only typecheck
needs the real tsc, so the failure looks like a missing gate rather than a
missing install.
A browser's first connect to the agent on this PC is approved in the desktop
window. The headless agent (pnpm dev:agent) has no window, so a portal page
cannot connect to it that way; run the desktop app, or call the API directly
with the admin token that --show-admin-token prints.
verify:android needs ANDROID_HOME, because the SDK is deliberately not
committed. Fetch it once with:
sdkmanager --sdk_root=.tools/android-sdk "platform-tools" "platforms;android-34" "build-tools;34.0.0"
The core/grid and core/client modules are pure Kotlin/JVM precisely so the
logic they hold stays testable without the SDK installed.
Verifying
Requirements: Node ≥ 22 (24 recommended), Rust ≥ 1.88. A JDK 17 is needed only for the Kotlin suite — the Gradle wrapper fetches Gradle itself.
# TypeScript — correctness + parity
node --test packages/grid/test/grid.test.ts packages/grid/test/parity.test.ts
# Rust — format, lints, tests
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
# Kotlin
cd android && ./gradlew :core:grid:test
Changing the layout algorithm
If you change the grid algorithm on purpose:
pnpm gen:expected # regenerate the golden fixtures
git diff contract/fixtures/grid
The diff is the point — it shows exactly which rectangles moved, so an accidental parity break becomes a visible, reviewable change instead of silent one-pixel drift between the preview and the tablet.
Never hand-edit an *.expected.json.
The desktop window
The agent still runs, exactly as before — the window is a view of it, not a
second program. flexdeck-desktop assembles the same agent (the sequence lives
in flexdeck-agent, so the two cannot drift), serves it, and adds a small window
and a tray icon. Closing the window hides it; the agent keeps serving tablets.
It shows four things: whether this computer is reachable (the server, web access, and the failures that only appear from a tablet), a pairing code with a countdown, the paired tablets with a live connected/not-connected state, and the actions this build can run.
The window reads state over Tauri's own IPC rather than the agent's HTTPS API. That is deliberate: the API needs a bearer token and presents a self-signed certificate, and neither belongs in a webview — the window is the machine's own interface, so it holds the agent handle directly. It is also why the window can report enabled but not connected honestly, which the agent's own code already insists on.
pnpm dev:desktop is the one command: it starts the window's UI, waits for it to
answer, opens the window, and stops the dev server when you quit. The UI is on
port 5174, not the portal's 5173, so both can run at once.
The Tauri CLI is not required. A debug build loads devUrl, so cargo run is
enough; importing @tauri-apps/cli would only rename these steps to tauri dev.
Dev builds run beside an installed release. A debug desktop build is "flexdeck
dev" (identifier dev.flexdeck.desktop.dev, its own WebView profile) and
listens on 23541, keeping 23539 for the installed app and 23540 for its
same-PC door; a debug APK is package dev.flexdeck.dev, labelled "flexdeck
dev". Pair a dev tablet by QR, or type the address with :23541.
One agent per port: dev:desktop and dev:agent both serve the tablets on
23541, so run one or the other. dev:desktop checks the port first and says so
rather than opening a window that cannot reach anything.
One window per installation: opening flexdeck while it already sits in the tray brings that window forward rather than starting a second copy. The listener keeps trying a taken port every few seconds, and tries at once whenever the window comes forward, so closing whatever held the port is enough — no restart.
Two checks the ordinary suites cannot make:
- The portal and the agent are only joined by hand.
apps/portal/scripts/roundtrip.check.tsdrives a running agent, applies edits through the portal's editing model, and asserts the widget, its label, its position and the revision all survive — plus undo and redo. Run it after changing either side:pnpm --filter @flexdeck/portal check:roundtrip <base-url> <identity> <desktop-id>. It needs a live cloud, desktop and tunnel. - The agent binary needs rebuilding before a manual test.
cargo testdoes not refreshtarget/debug, and a failed build leaves the previous binary in place, so a smoke test can silently exercise stale code.
Startup is: load Settings from the store, then call
Agent::startup_warnings(settings.bind()). Between them they cover the failures
that all look fine from the agent's own machine — a QR containing a loopback
address, an agent advertising an address it is not listening on, and presses that
would do nothing because no live executor is installed. They are warnings rather
than errors, because same-machine testing is legitimate, so the caller decides
what to show.
Releasing and deploying
Everything after a push to main is one workflow,
.github/workflows/pipeline.yml, which runs the changelog first and then calls
one workflow per target. Each target workflow can also be run by hand from the
Actions tab.
| Stage | Workflow | Runs | Goes to |
|---|---|---|---|
| Changelog | pipeline.yml |
every push | CHANGELOG.md + releases.json, committed back; release tags |
| Web | web.yml |
every push | Vercel: landing, docs, changelog, download at flex-deck.com |
| Server | server.yml |
when cloud or portal code changed, and on releases | Docker Hub image → Easypanel: editor + API at app.flex-deck.com |
| Desktop | desktop.yml |
release commits | Windows installer (NSIS) |
| Android | android.yml |
release commits | signed release APK |
| Publish | pipeline.yml |
release commits | GitHub release with both files, SHA256SUMS.txt and notes |
| Plugin SDK | sdk.yml |
SDK release commits | npm, crates.io or PyPI, then a <sdk>-v… tag |
The public pages and the editor are deployed apart because the editor and the
cloud API must share an origin (see crates/flexdeck-cloud/src/portal.rs). The
Vercel build is told the editor's address (VITE_EDITOR_URL), and
flex-deck.com/config redirects there. The cloud image is built with
VITE_EDITOR_ONLY=true, so app.flex-deck.com/ is the editor itself.
Releases
Releases are cut by commit lines: release: v0.4.0 for the apps,
release(js-sdk): v0.2.0 and the like for the plugin SDKs. Every kind, what it
builds, and how to retry one is in release.md.
One-time setup
| Where | Name | What |
|---|---|---|
| Repository secret | VERCEL_TOKEN, VERCEL_ORG_ID, VERCEL_PROJECT_ID |
A Vercel token and the project's ids (vercel link writes them to .vercel/project.json) |
| Repository variable | EDITOR_URL |
The cloud's origin; defaults to https://app.flex-deck.com |
| Repository variable | DOCKERHUB_USERNAME |
The Docker Hub account the image is pushed to |
| Repository secret | DOCKERHUB_TOKEN |
A Docker Hub access token with read and write access |
| Repository secret | EASYPANEL_DEPLOY_WEBHOOK |
The Easypanel service's deploy webhook URL |
| Repository secret | ANDROID_KEYSTORE_BASE64 |
The release keystore, base64 -w0 release.jks |
| Repository secret | ANDROID_KEYSTORE_PASSWORD, ANDROID_KEY_ALIAS, ANDROID_KEY_PASSWORD |
Its passwords and alias |
The Easypanel service uses the Docker image <DOCKERHUB_USERNAME>/flexdeck-cloud:latest
on port 8787, with a volume at /data and the domain app.flex-deck.com. The
Docker Hub repository can be public (the source is public anyway). If you make
it private, give Easypanel a read-only Docker Hub access token.
Keep the Android keystore safe forever. Every update must be signed with the same key, or Android refuses to install it over the old version.
Build status
Phase C complete — the agent builds, runs, pairs, and serves.
Built and verified:
| Piece | State |
|---|---|
| Monorepo (pnpm workspaces + Cargo workspace + Gradle wrapper) | ✅ |
proto3 contract for config, events and pairing |
✅ |
| Proto codegen → Rust types + proto3 JSON mapping | ✅ 8 tests |
| Shared grid algorithm — TypeScript reference | ✅ 10 unit tests |
| Shared grid algorithm — Rust | ✅ 9 unit tests |
| Shared grid algorithm — Kotlin | ✅ 2 tests |
| Golden fixtures + cross-renderer parity tests | ✅ 5 fixtures, 3 languages |
| Agent config store (SQLite: revision log, undo/redo, JSON-Patch diff) | ✅ 10 tests |
| Action engine (hotkey parsing + desktop execution) | ✅ 37 tests |
| Pairing (self-signed identity, PIN/QR windows, device tokens) | ✅ 28 tests |
| Agent sync core (catch-up policy + patch wire round trip) | ✅ 11 tests |
| Agent REST API (config, undo/redo, devices, pairing) | ✅ 15 tests |
| Agent WebSocket hub + end-to-end socket tests | ✅ 23 tests |
| LAN address discovery + startup checks | ✅ 24 tests |
| Agent settings persistence (port, bind address, host override) | ✅ 10 tests |
Agent binary (flexdeck) — runs, seeds, pairs, persists its identity |
✅ 7 tests |
| REST authentication (admin token + device tokens, per route) | ✅ 5 tests |
| Portal static serving (SPA fallback + an explanatory page when unbuilt) | ✅ 5 tests |
| Portal app (React: same-PC connect, code pairing, status, devices, grid preview) | ✅ |
| Portal editor (move, resize, add, delete, label, undo/redo) | ✅ 20 tests |
| Action catalogue, served by the agent and driving the portal's form | ✅ 8 tests |
| Portal action editing (attach / change / clear a press action) | ✅ 6 tests |
| Android client core (JSON Patch, wire frames, session, reconnect) | ✅ 51 tests |
| Android app (pairing, canvas deck renderer, TLS pinning) | ✅ builds, 1.6 MB APK |
| Desktop window (Tauri: status, pairing, live device presence, plugins) | ✅ builds, 131 + 12 Rust, 26 TS tests |
| Plugin registry + built-ins (core, system, media, launcher, notifications) | ✅ 21 tests, live Windows audio/CPU backend |
Plugin contract (ABI 2: manifest, JSON-RPC messages, plugin.json) |
✅ 15 tests, pinned to the SDK by shared fixtures |
| Plugin host (sidecar processes: deadlines, crash restarts, namespace enforcement) | ✅ 18 tests |
| Install / update / uninstall / hot reload, without restarting the agent | ✅ 13 end-to-end tests, incl. real Node |
| Plugin repositories (official, vetted, custom; pinned checksums, offline cache) | ✅ |
Plugin SDK + template (pnpm create-plugin) |
✅ 9 + 3 tests |
| OBS Studio plugin (scenes, record, stream, mute, sources; live state) | ✅ 14 tests against an in-memory OBS |
| Plugin-event and schedule triggers; buttons bound to live values | ✅ agent, portal and tablet |
MCP endpoint for coding agents (loopback /mcp, widget tools, JSON Patch, revision-checked writes) |
✅ 23 tests, editor parity fixtures |
| CI (format, lint, tests, fixture-drift check) | ✅ |
The plugin system is built — see plugins.md for using, writing and publishing plugins. The desktop window is built — see The desktop window below.