This document describes the hosted services and CI/CD infrastructure that support blit. For the system architecture, see ARCHITECTURE.md. editor:local:/src/blit/SERVICES.mdFor development workflow, see CONTRIBUTING.md.
brew services start blit server
brew services start blit gatewaysudo systemctl enable --now blit-server@alice.socket
# Share via WebRTC: create /etc/blit/share-alice.env with
# BLIT_SOCK=/run/blit/alice.sock and BLIT_PASSPHRASE=<secret>, then:
sudo systemctl enable --now blit-share@alice.serviceAll three blit daemons (server, gateway, share) send READY=1
over $NOTIFY_SOCKET once they're actually serving — server after the
IPC listener is bound, gateway after the TCP listener is bound, share
after the signaling hub registers the producer. Units in this repo
ship with Type=notify so systemctl start returns only after the
service is genuinely consumable. The notify implementation is in
crates/sd-notify/ and is a no-op when
NOTIFY_SOCKET is unset, so the same binaries still work outside
systemd.
blit gateway can front multiple remote hosts in a single browser UI.
Configure remotes in ~/.config/blit/blit.remotes (same file used by blit open):
rabbit = ssh:rabbit
hound = ssh:alice@hound
BLIT_PASSPHRASE=secret blit gatewayTo avoid storing the gateway passphrase in plaintext, store an argon2id PHC hash instead:
BLIT_PASSPHRASE=$(blit hash-passphrase secret) blit gatewayBrowsers still enter secret; the hash embeds its random salt and argon2 parameters.
SSH remotes are connected via the embedded SSH client (russh) with ssh-agent authentication. blit is auto-installed on remote hosts if missing.
To proxy share: (WebRTC) remotes through the gateway instead of having the
browser connect to the hub directly, set BLIT_GATEWAY_WEBRTC=1:
hound = share:mysecret
rabbit = share:anothersecret?hub=wss://custom.hub
BLIT_GATEWAY_WEBRTC=1 BLIT_PASSPHRASE=secret blit gatewayThe gateway connects as a WebRTC consumer (using the passphrase-derived channel
identity; the hub assigns each connection a unique sessionId so multiple
consumers can connect to the same producer concurrently) and re-exposes each
destination at WS /d/<name>. The browser never touches WebRTC
or the hub. Without BLIT_GATEWAY_WEBRTC=1, share: entries are ignored by
the gateway and the browser connects to the hub directly.
For nix-darwin and NixOS service modules, see nix/README.md.
install.blit.sh is an APT repository and binary download site hosted on GitHub Pages. It is rebuilt and deployed on every tagged release (v*). Users interact with it in three ways:
- Curl installer —
curl -sf https://install.blit.sh | shfetches the install script (served asindex.html), which detects OS/arch, downloads the right tarball from/bin/, and installs it. - PowerShell installer —
irm https://install.blit.sh/install.ps1 | iexinstalls blit on Windows. Downloads the zip from/bin/, extractsblit.exeto%LOCALAPPDATA%\blit\bin, and adds it to the userPATH. - APT repository — Debian/Ubuntu users add it as a signed APT source for
apt install blit. - Direct download — tarballs are available at
/bin/blit_<version>_<os>_<arch>.tar.gz, Windows zips at/bin/blit_<version>_windows_x86_64.zip.
install.blit.sh/
index.html # install.sh (curl installer served as the landing page)
install.ps1 # PowerShell installer for Windows
latest # plain-text file containing the current version (e.g. "0.12.0")
blit.gpg # GPG public key for APT signature verification
bin/
blit_0.12.0_linux_x86_64.tar.gz
blit_0.12.0_linux-musl_x86_64.tar.gz
blit_0.12.0_linux_aarch64.tar.gz
blit_0.12.0_linux-musl_aarch64.tar.gz
blit_0.12.0_darwin_aarch64.tar.gz
blit_0.12.0_windows_x86_64.zip
pool/
blit_0.12.0_amd64.deb
blit_0.12.0_arm64.deb
dists/stable/
Release # APT release metadata
Release.gpg # detached GPG signature
InRelease # clearsigned release
main/binary-amd64/
Packages # APT package index
Packages.gz
main/binary-arm64/
Packages
Packages.gz
install.sh is a portable POSIX shell script that:
- Detects OS (
uname -s) and architecture (uname -m), normalizing tolinux/darwinandx86_64/aarch64. - On Linux, detects the system libc (musl vs glibc) by checking
ldd --versionoutput and/lib/ld-musl-*. Musl systems getlinux-musltarballs. - Fetches
/latestfrominstall.blit.shto get the current version. - Skips if the installed version already matches.
- Downloads the tarball from
/bin/blit_<version>_<os>_<arch>.tar.gz. - Extracts the tarball (
bin/blitsingle binary) into$BLIT_PREFIX(default/usr/local), escalating withsudo/doasif needed.
install.ps1 is the Windows equivalent:
- Fetches
/latestto get the current version. - Downloads the zip from
/bin/blit_<version>_windows_x86_64.zip. - Extracts
blit.exeto$BLIT_INSTALL_DIR(default%LOCALAPPDATA%\blit\bin). - Adds the install directory to the user
PATHif not already present.
blit upgrade is the in-place self-update command. On Unix it fetches install.sh from https://install.blit.sh, writes it to a temp file, and execs sh with BLIT_PREFIX set to the install prefix of the currently running binary. On Windows it fetches install.ps1 and runs it via PowerShell. See crates/cli/src/main.rs.
The apt-repo job in the release workflow assembles the entire site from build artifacts, signs the APT metadata with GPG, and deploys via GitHub Pages:
flowchart TD
subgraph "Build phase (reusable workflows)"
BP_X86[build-packages<br>linux-x86_64<br>debs + tarballs]
BP_ARM[build-packages<br>linux-aarch64<br>debs + tarballs]
BP_MAC[build-packages<br>macos-aarch64<br>tarballs only]
BW[build-windows<br>x86_64]
end
subgraph "apt-repo job"
DL[Download all deb + tarball + windows artifacts]
REPO[Assemble repo/ directory]
SIGN[GPG-sign Release metadata]
PAGES[Deploy to GitHub Pages]
DL --> REPO --> SIGN --> PAGES
end
BP_X86 --> DL
BP_ARM --> DL
BP_MAC --> DL
BW --> DL
hub.blit.sh is the WebRTC signaling relay that enables blit share. It runs on Fly.io and is deployed automatically when code under js/hub/ changes on main.
The hub routes WebRTC signaling messages (offers, answers, ICE candidates) between peers over WebSocket. Channels are identified by ed25519 public keys, and the server verifies NaCl crypto_sign envelopes before relaying — no server-side accounts needed.
For protocol details, deployment instructions, and configuration, see js/hub/README.md.
- Bun runtime with
Bun.serve()for HTTP and WebSocket - Redis for cross-instance pub/sub and session tracking (sets with TTL)
- tweetnacl for ed25519 signature verification
- Stateless — all session state lives in Redis, so instances scale horizontally
| Path | Purpose |
|---|---|
/channel/<pubkey>/<producer|consumer> |
WebSocket upgrade for signaling |
/ice |
STUN/TURN server config (Cloudflare TURN if configured) |
/message |
Session URL template for client display |
/health |
Liveness check (pings Redis) |
blit.sh is the public website, deployed to Vercel. It serves two purposes:
- Landing page (
/) — marketing page with install instructions, feature overview, and a join form to connect to a shared terminal. - Terminal viewer (
/#psk=<secret>) — browser-based terminal that connects to a shared terminal via WebRTC throughhub.blit.sh.
The site is an Astro + Solid app built from js/website/. It consumes @blit-sh/core and @blit-sh/solid via workspace aliases and inlines the browser WASM module at build time.
The website is built as a Nix derivation (websiteDist in nix/packages.nix) which compiles the WASM crate, installs pnpm deps via fetchPnpmDeps, and runs astro build. The deploy-website task in nix/tasks.nix assembles a Vercel prebuilt output directory and deploys via pnpm dlx vercel deploy --prebuilt.
- Production deploy — pushes to
mainthat touchjs/website/**,js/core/**,js/react/**, orcrates/browser/**trigger./bin/deploy-website --prod. - Preview deploy — PRs with the same path changes get a preview deploy with the URL posted as a PR comment.
./bin/deploy-website # preview deploy
./bin/deploy-website --prod # production deployAll release binaries are built with Nix, which makes the entire toolchain reproducible and keeps the build definitions small.
On Linux, two variants are shipped:
- glibc (
blit-gnu) — libopus is statically linked; only glibc itself is dynamic. Built withcargo-zigbuildtargeting glibc 2.31, so the binary runs on Ubuntu 20.04+, Debian 11+, RHEL 8+, etc.dlopenworks natively for GPU acceleration (VA-API, NVENC, Vulkan). This is the default for most Linux systems. - musl (
blit-musl) — built with the LLVM musl cross toolchain. All dependencies except musl libc are statically linked, producing a single binary. The interpreter is set to the systemld-musl-<arch>.so.1. For Alpine and other musl-based systems.
Both release tarballs contain a single binary at bin/blit.
The blit-gnu derivation in nix/packages.nix builds the glibc binary with static dep overrides and cargo-zigbuild. The blit-release-gnu derivation patches the interpreter to the standard system path via patchelf. The blit-release-musl derivation does the same for musl. The blit-musl build verifies its only NEEDED library is libc.so. install.sh auto-detects the system libc (musl vs glibc) and downloads the right tarball.
On macOS, true static linking isn't practical (Apple doesn't ship static system libraries). Instead, postFixup rewrites any nix-store dylib references to their /usr/lib/ equivalents (libSystem, libc++, libresolv, etc.) using install_name_tool, so the binary runs on stock macOS without Nix installed.
The Rust toolchain is configured with musl targets (x86_64-unknown-linux-musl, aarch64-unknown-linux-musl) in nix/common.nix. The same toolchain also includes wasm32-unknown-unknown for the browser WASM build.
The glibc tarballs on install.blit.sh/bin/ and .deb packages require only system glibc ≥ 2.31. The musl tarballs require only system musl libc. All are single-binary downloads with no external library dependencies (GPU drivers are loaded via dlopen at runtime on glibc systems). macOS and Windows binaries have no required dependencies.
On Windows, Nix isn't available, so the _build-windows.yml reusable workflow uses cargo build --release directly on a Windows runner with the MSVC toolchain. The resulting .exe files link against standard Windows system DLLs (kernel32, ws2_32, etc.) that are always present.
Seven workflow files live in .github/workflows/. The three _-prefixed files are reusable workflows called by ci.yml and release.yml:
| Workflow | Trigger | Purpose |
|---|---|---|
ci.yml |
Push to main, PRs |
Lint, test, e2e, coverage, verify builds |
release.yml |
v* tag push |
Verify tag signature, build artifacts, create GitHub Release, publish packages, deploy install site |
deploy-hub.yml |
Push to main (paths: js/hub/**) |
Deploy signaling hub to Fly.io |
deploy-website.yml |
Push to main (paths: js/website/**, js/core/**, js/react/**, crates/browser/**), PRs |
Build website via Nix, deploy to Vercel (prod on main, preview on PRs) |
publish-demo-image.yml |
Push to main, v* tag |
Build and push grab/blit-demo Docker image |
_build-packages.yml |
Called by ci.yml, release.yml |
Build debs + tarballs (one job per arch, shares Nix cache) |
_build-windows.yml |
Called by ci.yml, release.yml |
Build Windows release binary |
Runs on every push to main and on every pull request. Build jobs are delegated to reusable workflows (_build-packages.yml, _build-windows.yml) shared with the release pipeline. On Linux, each architecture builds both debs and tarballs in a single job so they share the Nix store cache.
flowchart LR
PR[Push / PR] --> nix[nix-syntax]
PR --> lint[lint]
PR --> test_linux[test<br>linux]
PR --> test_mac[test<br>macOS]
PR --> e2e[e2e<br>+ Playwright report]
PR --> cov[coverage<br>+ PR comment]
PR --> pkg_x86[build-packages<br>linux-x86_64]
PR --> pkg_arm[build-packages<br>linux-aarch64]
PR --> pkg_mac[build-packages<br>macos-aarch64]
PR --> win[build-windows<br>x86_64]
| Job | Runner | What it does |
|---|---|---|
nix-syntax |
ubuntu (4 vCPU) | nix-instantiate --parse on all .nix files — catches syntax errors in modules that aren't evaluated by nix flake check |
lint |
ubuntu (4 vCPU) | ./bin/lint --check — cargo fmt --check + prettier --check + clippy |
test |
ubuntu (8 vCPU), macOS | ./bin/tests — cargo test --workspace |
e2e |
ubuntu (8 vCPU) | ./bin/e2e — Playwright against the full stack; uploads report artifact |
coverage |
ubuntu (8 vCPU) | ./bin/coverage — runs tests with coverage; uploads HTML report and posts summary on PRs |
build-packages |
ubuntu (8 vCPU) ×2, macOS | Verify debs + tarballs build (debs on Linux only, tarballs on all 3 platforms) |
build-windows |
windows (8 vCPU) | Verify Windows release build compiles (x86_64) |
Triggered by pushing a v* tag. A verify-tag job checks the tag signature via the GitHub API before any builds start — unsigned or unverified tags fail the workflow immediately.
flowchart TD
TAG["v* tag push"] --> VER[verify-tag<br>Check signature via GitHub API]
VER --> BP & BW
subgraph "Build (parallel, reusable workflows)"
BP[build-packages<br>debs + tarballs per arch]
BW[build-windows<br>x86_64]
end
BP & BW --> REL[release<br>Create GitHub Release<br>with .deb + .tar.gz + .zip]
BP & BW --> APT[apt-repo<br>Assemble APT repo<br>GPG sign, deploy Pages]
REL --> PUB_CRATES[publish-crates<br>crates.io]
REL --> PUB_NPM[publish-npm<br>npm registry]
REL --> BREW[update-homebrew<br>repository-dispatch to<br>indent-com/homebrew-tap]
| Job | Depends on | What it does |
|---|---|---|
verify-tag |
— | Checks the tag signature via the GitHub API; fails if unsigned or unverified |
build-packages |
verify-tag | Reusable workflow: Nix-builds debs + tarballs per architecture (3 runners, shares Nix cache within each) |
build-windows |
verify-tag | Reusable workflow: cargo build --release on Windows, packages .exe files into zips |
release |
build-packages, build-windows | Downloads all artifacts, creates a GitHub Release with auto-generated notes |
publish-crates |
release | ./bin/publish-crates — publishes workspace crates to crates.io |
publish-npm |
release | ./bin/publish-npm-packages — publishes @blit-sh/browser, @blit-sh/core, @blit-sh/react, @blit-sh/solid to npm |
update-homebrew |
release | Sends a repository-dispatch event to indent-com/homebrew-tap with the new version |
apt-repo |
build-packages, build-windows | Assembles the APT repo directory, GPG-signs metadata, deploys to GitHub Pages |
Single job, triggered only when files under js/hub/ change on main:
flowchart LR
PUSH["Push to main<br>(js/hub/**)"] --> DEPLOY["nix run .#deploy-hub<br>via flyctl"]
Builds the website via Nix and deploys to Vercel. Runs on pushes to main and on PRs when relevant paths change. PR deploys post a preview URL as a comment.
flowchart LR
PUSH["Push to main or PR<br>(js/website/**, js/core/**,<br>js/react/**, crates/browser/**)"] --> BUILD["Nix build<br>websiteDist"] --> DEPLOY["vercel deploy<br>--prebuilt"]
DEPLOY --> COMMENT["Post preview URL<br>(PRs only)"]
Builds and pushes grab/blit-demo to Docker Hub. Runs on pushes to main and on v* tags. Tagged releases get an additional version tag.
flowchart TD
TRIGGER["Push to main<br>or v* tag"] --> BUILD_AMD[build-push<br>amd64] & BUILD_ARM[build-push<br>arm64]
BUILD_AMD & BUILD_ARM --> MANIFEST[manifest<br>Create multi-arch manifest<br>and push to Docker Hub]
End-to-end flow from version bump to published artifacts:
sequenceDiagram
participant Dev as Developer
participant Rel as bin/prepare-release
participant Git as Git / GitHub
participant CI as GitHub Actions
Dev->>Dev: ./bin/release-prepare 0.12.0
Dev->>Rel: ./bin/prepare-release 0.12.0
Rel->>Rel: Validate version consistency across<br>Cargo.toml, package.json, nix/common.nix
Rel->>Rel: Bump all version files
Rel->>Rel: cargo test -p blit-server
Rel->>Git: git commit "release 0.12.0"
Dev->>Git: Push release/0.12.0 branch<br>Open PR against main
Dev->>Git: Review and merge PR
Dev->>Dev: ./bin/release-tag 0.12.0
Dev->>Git: Signed tag v0.12.0 pushed
Git->>CI: v* tag triggers release.yml + publish-demo-image.yml
CI->>CI: verify-tag: check signature via GitHub API
CI->>CI: Build .deb (amd64, arm64)
CI->>CI: Build tarballs (linux-x86_64, linux-aarch64, macos-aarch64)
CI->>CI: Build Windows zips (x86_64)
CI->>Git: Create GitHub Release with all artifacts
CI->>CI: Publish crates to crates.io
CI->>CI: Publish packages to npm
CI->>CI: Dispatch to indent-com/homebrew-tap
CI->>CI: Assemble + GPG-sign APT repo
CI->>CI: Deploy install.blit.sh via GitHub Pages
CI->>CI: Build + push grab/blit-demo to Docker Hub
| Secret | Used by | Purpose |
|---|---|---|
GPG_PRIVATE_KEY |
apt-repo | Signs APT Release metadata |
HOMEBREW_TAP_TOKEN |
update-homebrew | PAT for cross-repo dispatch to homebrew-tap |
FLY_API_TOKEN |
deploy-hub | Fly.io deploy token for blit-hub |
DOCKERHUB_USERNAME |
publish-demo-image | Docker Hub credentials |
DOCKERHUB_TOKEN |
publish-demo-image | Docker Hub credentials |
VERCEL_TOKEN |
deploy-website | Vercel API token |
VERCEL_ORG_ID |
deploy-website | Vercel organization ID |
VERCEL_PROJECT_ID |
deploy-website | Vercel project ID |
CARGO_REGISTRY_TOKEN |
publish-crates | Temporary bootstrap token for new crates |
publish-crates normally uses no stored secret. It authenticates to crates.io via OIDC trusted publishing — GitHub mints a short-lived ID token (enabled by the id-token: write permission on the release workflow) and exchanges it for a crates.io upload token. A crate name must exist before its trusted publisher can be configured, so set CARGO_REGISTRY_TOKEN for the first release that creates a crate, configure that crate's trusted publisher for release.yml, then remove the secret. publish-npm uses OIDC as well, with --provenance signing the package for npm.