Architecture

A technical overview of how Roost works, from the app runtime to the gateway infrastructure. This page is aimed at developers who want to understand the system before building on it or contributing.

Roost is a self-hosted app platform. You install it on your own hardware, optionally connect it to the internet through a gateway, and run apps that you and your team can use from any browser or the native iOS and desktop clients. Apps are zipped packages containing a JavaScript server (running in V8 isolates), SQLite migrations, and a React client.

Traffic flow

When accessed remotely, traffic flows through several layers before reaching your Roost server:

Browser / Native App
        |
        v
   HAProxy  (SNI routing, TLS termination)
        |
        v
   Unix Socket  (reverse SSH tunnel)
        |
        v
   Roost Server  (your machine)
        |
   +----+----+-------+
   |         |       |
V8 Isolates  SQLite  Media
(app servers) (per-app DBs) (FFmpeg/libvips)

Your Roost server establishes a reverse SSH tunnel to the gateway host, forwarding a Unix socket. The gateway's HAProxy routes incoming HTTPS traffic through this tunnel to your machine. For local access, clients connect directly.

App runtime

Each app runs in its own V8 isolate with a fresh V8 context per connected client. This provides strong isolation between apps—they cannot access each other's data or the host filesystem.

Apps expose a Server class with three lifecycle methods:

class Server {
  // Platform services injected here
  constructor({ db, events, media, system, users }) { ... }

  // Called when a client connects via WebSocket
  open(userId) { ... }

  // Called when the client sends a message
  process(message) { ... }

  // Called when the client disconnects
  close() { ... }
}

Clients connect via WebSocket. Messages sent from the client are routed to process(), and events emitted server-side fan out to all connected clients. Server code is TypeScript compiled to an IIFE via esbuild; client code is React + Vite + TailwindCSS.

V8 function template pattern

Go functions exposed to JavaScript in the executor follow a consistent pattern: create a v8.PromiseResolver for error handling (reject on failure), return the raw *v8.Value directly on success (JS await on a non-promise passes it through), or return resolver.GetPromise().Value after rejecting on error.

Platform services

The app constructor receives platform services that provide access to databases, file storage, media processing, and more. These are defined in roost/api.json and exposed to each isolate.

Service Description
db SQLite database per app. WAL mode, serialized writes. Migrations run automatically at startup.
events Real-time event bus. Emit events server-side and they fan out to connected WebSocket clients.
media Audio/video transcoding (FFmpeg), image resizing (libvips), metadata extraction, and MusicBrainz lookups.
system System-level operations including file uploads, search indexing, and platform configuration.
users User management and authentication. Query user profiles, roles, and online status.
http Make outbound HTTP requests from app server code.

Media service methods

The media service provides several specialized methods:

  • extractAudio(fileID) — FFprobe metadata (duration, bitrate, codec, etc.)
  • extractTags(fileIDs) — Embedded tag data from audio files (artist, album, year, genre, art) using majority voting across files
  • lookupAlbum(fileIDs) — Like extractTags but falls back to MusicBrainz for cover art
  • searchAlbum(artist, album) — Search MusicBrainz releases
  • searchRecording(artist, title) — Search MusicBrainz recordings for untagged tracks
  • fetchAlbumArt(mbid) — Fetch cover art by MusicBrainz release ID
  • transcodeVideo(fileID, options?) — Transcode video via FFmpeg
  • resizeImageWidth(fileID, width) — Resize image via libvips

Database architecture

Roost uses SQLite for all local data. Databases run in WAL mode with writes serialized via mutex. Each app gets its own database file, and there are system-level databases for core platform state:

Database Purpose
system.sqlite Users, settings, app configuration
events.sqlite Event log and subscriptions
uploads.sqlite File metadata, references, and transforms
<app>.sqlite Per-app data (one database per installed app)

Migrations use golang-migrate format (NNN_description.up.sql) and are embedded in the binary via //go:embed. They run automatically at startup.

The query layer is generated by sqlc—you write SQL queries in sql/query.sql, run sqlc generate, and get type-safe Go code in a db/ package.

Upload database design

The uploads database uses a reference-counted design. Files are split into two tables: file_ref (references from apps) and file (actual file data). Transforms (audio/video transcoding, image resizing) are tracked in a unified transforms table. SQLite triggers handle reference counting so file data is cleaned up when all references are removed.

Upload lifecycle

File uploads are encrypted and chunked. The three-step process ensures data integrity and supports resumable uploads:

  1. Start — Insert a file row with an empty digest (X'')
  2. AddPart — Append encrypted chunks to the file
  3. Complete — Set the final digest, marking the file as ready
Important: Queries that list files should filter length(digest) > 0 to exclude incomplete or abandoned uploads.

Apps can express disinterest in files they no longer need. This is buffered as a side effect and processed after the current operation completes, ensuring safe cleanup without race conditions.

Gateway infrastructure

The gateway is the bridge between your local Roost server and the public internet. It handles DNS, TLS certificates, and traffic routing.

Gateway API

A Go server built with chi router and PostgreSQL (via pgx). It manages user registrations, DNS (Route53), email (SES), storage (B2), and TLS certificates via ACME DNS-01 challenges. The API runs behind an ALB on port 8080.

Gateway Worker

Runs on each gateway host. Subscribes to Redis pub/sub for real-time events. When a Roost server connects, the worker provisions a Unix user, manages SSH authorized keys, and regenerates HAProxy config. When servers disconnect, HAProxy config is updated accordingly.

TLS certificates

Certificates are obtained automatically via ACME DNS-01 challenge. The gateway API creates Route53 TXT records for domain validation. certmagic handles the certificate lifecycle— obtaining, renewing, and storing certificates.

Infrastructure

Gateway instances run in an AWS fleet. Traffic arrives through two load balancers:

  • NLB (TCP:443) — Routes HTTPS traffic to HAProxy, which uses SNI routing to direct requests to the correct Roost server via Unix socket
  • ALB (HTTPS:8080) — Routes API requests to the gateway API

Push notifications

Roost supports end-to-end encrypted push notifications via APNs for iOS. The flow works as follows:

  1. The iOS app registers its device token and a public key with the gateway
  2. The Roost server encrypts the notification payload with the device's public key
  3. The encrypted payload is sent through the gateway's APNs connection
  4. The iOS Notification Service Extension decrypts the payload on-device

When push delivery fails, the system falls back to email notifications. Device tokens and encryption keys are stored in the gateway's push_devices table.

Native clients

Roost runs in any modern browser, but also has native clients that add platform-specific features.

iOS

A native Swift/UIKit app with WKWebView that loads the Roost web UI. Key features:

  • Push notifications with end-to-end encryption via Notification Service Extension
  • Multi-server management via a bundled server picker web UI (React/Vite, built to a single HTML file)
  • Continuous photo backup to the Photos app
  • Native audio playback bridge with Now Playing integration
  • mDNS discovery for finding local Roost servers on the network

The native↔web bridge uses window.__roostNative = true to signal the native environment, and WKWebView message handlers (roostAudioBridge, serverPicker) for communication.

Desktop

A Wails-based app (Go + WebView) for macOS. Features:

  • Multi-server picker
  • Proxy configuration and URL rewriting
  • Signed builds with install scripts
  • File logging for diagnostics

Code generation

Roost uses two code generation tools to keep type definitions and database queries in sync across the stack.

sqlc

Query-first SQL development: write SQL queries in sql/query.sql, run sqlc generate, and get type-safe Go code. Used for both Roost (SQLite, with 5 database configs in roost/sqlc.yml) and Gateway (PostgreSQL).

Cameo

A custom code generator that reads JSON schema definitions (roost.json) and produces TypeScript interfaces and SQL migrations for apps. Platform service interfaces are generated from roost/api.json, which defines the db, events, media, system, users, and http services.

To add a new platform method:

  1. Add the method signature to roost/api.json
  2. Implement the Go function in roost/executor/executor.go
  3. Run scripts/regenerate-cameo to rebuild cameo and regenerate all app interfaces
  4. Update app server code to use the new method

Git structure

Each top-level directory (roost/, gateway/, common/, etc.) and each app directory (apps/musicbox/, apps/chat/, etc.) is its own git repository. There is no monorepo root—commits must be made in each individual repo.