# Services This document describes the hosted services and CI/CD infrastructure that support blit. For the system architecture, see [ARCHITECTURE.md](ARCHITECTURE.md). For development workflow, see [CONTRIBUTING.md](CONTRIBUTING.md). ## Running as a service ### macOS (Homebrew) ```bash brew services start blit server brew services start blit gateway ``` ### Debian / Ubuntu (systemd) ```bash sudo 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=, then: sudo systemctl enable --now blit-share@alice.service ``` All 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/`](crates/sd-notify/) and is a no-op when `NOTIFY_SOCKET` is unset, so the same binaries still work outside systemd. ### Multi-remote gateway (blit gateway) `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 ``` ```bash BLIT_PASSPHRASE=secret blit gateway ``` To avoid storing the gateway passphrase in plaintext, store an argon2id PHC hash instead: ```bash BLIT_PASSPHRASE=$(blit hash-passphrase secret) blit gateway ``` Browsers 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 ``` ```bash BLIT_GATEWAY_WEBRTC=1 BLIT_PASSPHRASE=secret blit gateway ``` The 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/`. 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. ### Nix For nix-darwin and NixOS service modules, see [`nix/README.md`](nix/README.md). ## install.blit.sh `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: 1. **Curl installer** — `curl -sf https://install.blit.sh | sh` fetches the install script (served as `index.html`), which detects OS/arch, downloads the right tarball from `/bin/`, and installs it. 2. **PowerShell installer** — `irm https://install.blit.sh/install.ps1 | iex` installs blit on Windows. Downloads the zip from `/bin/`, extracts `blit.exe` to `%LOCALAPPDATA%\blit\bin`, and adds it to the user `PATH`. 3. **APT repository** — Debian/Ubuntu users add it as a signed APT source for `apt install blit`. 4. **Direct download** — tarballs are available at `/bin/blit___.tar.gz`, Windows zips at `/bin/blit__windows_x86_64.zip`. ### File hierarchy ``` 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 ``` ### How the install script works [`install.sh`](install.sh) is a portable POSIX shell script that: 1. Detects OS (`uname -s`) and architecture (`uname -m`), normalizing to `linux`/`darwin` and `x86_64`/`aarch64`. 2. On Linux, detects the system libc (musl vs glibc) by checking `ldd --version` output and `/lib/ld-musl-*`. Musl systems get `linux-musl` tarballs. 3. Fetches `/latest` from `install.blit.sh` to get the current version. 4. Skips if the installed version already matches. 5. Downloads the tarball from `/bin/blit___.tar.gz`. 6. Extracts the tarball (`bin/blit` single binary) into `$BLIT_PREFIX` (default `/usr/local`), escalating with `sudo`/`doas` if needed. ### Windows installer [`install.ps1`](install.ps1) is the Windows equivalent: 1. Fetches `/latest` to get the current version. 2. Downloads the zip from `/bin/blit__windows_x86_64.zip`. 3. Extracts `blit.exe` to `$BLIT_INSTALL_DIR` (default `%LOCALAPPDATA%\blit\bin`). 4. Adds the install directory to the user `PATH` if not already present. ### `blit upgrade` `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 `exec`s `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`](crates/cli/src/main.rs). ### How the site is built 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: ```mermaid flowchart TD subgraph "Build phase (reusable workflows)" BP_X86[build-packages
linux-x86_64
debs + tarballs] BP_ARM[build-packages
linux-aarch64
debs + tarballs] BP_MAC[build-packages
macos-aarch64
tarballs only] BW[build-windows
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 `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`](js/hub/README.md). ### Architecture - **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 ### Endpoints | Path | Purpose | | ---------------------------------------- | ------------------------------------------------------- | | `/channel//` | 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 `blit.sh` is the public website, deployed to **Vercel**. It serves two purposes: 1. **Landing page** (`/`) — marketing page with install instructions, feature overview, and a join form to connect to a shared terminal. 2. **Terminal viewer** (`/#psk=`) — browser-based terminal that connects to a shared terminal via WebRTC through `hub.blit.sh`. The site is an Astro + Solid app built from [`js/website/`](js/website/). It consumes `@blit-sh/core` and `@blit-sh/solid` via workspace aliases and inlines the browser WASM module at build time. ### Build and deploy The website is built as a Nix derivation (`websiteDist` in [`nix/packages.nix`](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`](nix/tasks.nix) assembles a Vercel prebuilt output directory and deploys via `pnpm dlx vercel deploy --prebuilt`. - **Production deploy** — pushes to `main` that touch `js/website/**`, `js/core/**`, `js/react/**`, or `crates/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. ### Local usage ```sh ./bin/deploy-website # preview deploy ./bin/deploy-website --prod # production deploy ``` ## Release binaries via Nix All 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 with `cargo-zigbuild` targeting glibc 2.31, so the binary runs on Ubuntu 20.04+, Debian 11+, RHEL 8+, etc. `dlopen` works 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 system `ld-musl-.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`](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`](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. ## GitHub Actions workflows 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`](.github/workflows/ci.yml) | Push to `main`, PRs | Lint, test, e2e, coverage, dev-check, verify builds | | [`release.yml`](.github/workflows/release.yml) | `v*` tag push | Verify tag signature, build artifacts, create GitHub Release, publish packages, deploy install site | | [`deploy-hub.yml`](.github/workflows/deploy-hub.yml) | Push to `main` (paths: `js/hub/**`) | Deploy signaling hub to Fly.io | | [`deploy-website.yml`](.github/workflows/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`](.github/workflows/publish-demo-image.yml) | Push to `main`, `v*` tag | Build and push `grab/blit-demo` Docker image | | [`_build-packages.yml`](.github/workflows/_build-packages.yml) | Called by `ci.yml`, `release.yml` | Build debs + tarballs (one job per arch, shares Nix cache) | | [`_build-windows.yml`](.github/workflows/_build-windows.yml) | Called by `ci.yml`, `release.yml` | Build Windows release binary | ### CI (ci.yml) Runs on every push to `main` and on every pull request. Consolidates what was previously separate `test.yml`, `dev-check.yml`, and `coverage.yml` workflows. 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. ```mermaid flowchart LR PR[Push / PR] --> nix[nix-syntax] PR --> lint[lint] PR --> dev[dev-check] PR --> test_linux[test
linux] PR --> test_mac[test
macOS] PR --> e2e[e2e
+ Playwright report] PR --> cov[coverage
+ PR comment] PR --> pkg_x86[build-packages
linux-x86_64] PR --> pkg_arm[build-packages
linux-aarch64] PR --> pkg_mac[build-packages
macos-aarch64] PR --> win[build-windows
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` — `cargo fmt --check` + `prettier --check` + clippy | | `dev-check` | ubuntu (8 vCPU) | Enters the Nix devshell and runs `bin/dev-check` (smoke-tests the full dev stack) | | `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) | ### Release (release.yml) 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. ```mermaid flowchart TD TAG["v* tag push"] --> VER[verify-tag
Check signature via GitHub API] VER --> BP & BW subgraph "Build (parallel, reusable workflows)" BP[build-packages
debs + tarballs per arch] BW[build-windows
x86_64] end BP & BW --> REL[release
Create GitHub Release
with .deb + .tar.gz + .zip] BP & BW --> APT[apt-repo
Assemble APT repo
GPG sign, deploy Pages] REL --> PUB_CRATES[publish-crates
crates.io] REL --> PUB_NPM[publish-npm
npm registry] REL --> BREW[update-homebrew
repository-dispatch to
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 | ### Deploy hub (deploy-hub.yml) Single job, triggered only when files under `js/hub/` change on `main`: ```mermaid flowchart LR PUSH["Push to main
(js/hub/**)"] --> DEPLOY["nix run .#deploy-hub
via flyctl"] ``` ### Deploy website (deploy-website.yml) 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. ```mermaid flowchart LR PUSH["Push to main or PR
(js/website/**, js/core/**,
js/react/**, crates/browser/**)"] --> BUILD["Nix build
websiteDist"] --> DEPLOY["vercel deploy
--prebuilt"] DEPLOY --> COMMENT["Post preview URL
(PRs only)"] ``` ### Publish demo image (publish-demo-image.yml) 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. ```mermaid flowchart TD TRIGGER["Push to main
or v* tag"] --> BUILD_AMD[build-push
amd64] & BUILD_ARM[build-push
arm64] BUILD_AMD & BUILD_ARM --> MANIFEST[manifest
Create multi-arch manifest
and push to Docker Hub] ``` ## Release lifecycle End-to-end flow from version bump to published artifacts: ```mermaid 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
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
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 ``` ## Secrets and authentication | 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 | `publish-crates` 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. `publish-npm` works the same way, using `--provenance` to sign the npm package with the workflow's OIDC identity.