Skip to content

Architecture

Rel has one supported runtime: the installed macOS app owns Chromium and supervises a bundled local agent; the CLI talks only to that agent.

Related references: CLI, Rust SDK, RPC, and Services.

flowchart LR
    CLI["rel CLI"] --> SDK["rel-client"]
    RustClient["Rust programs"] --> SDK
    SDK -->|"HTTP RPC v1<br/>127.0.0.1:17319"| Agent["rel --agent"]
    App["Rel.app"] -->|"supervises"| Agent
    Agent -->|"framed JSON v4<br/>private stdio"| App
    App --> CEF["ClarkBrowserKit / CEF"]
    Agent --> DB[("Current SQLite store")]
    App --> Logs["NDJSON log files"]
    Agent --> Logs
    CEF --> ProfileData["per-session CEF storage"]
    CEF --> SharedCache["partitioned shared assets"]
    CEF -->|"direct session"| Internet["Origin servers"]
    CEF -->|"proxied session<br/>profile loopback port"| ProfileProxy["Agent profile proxy"]
    ProfileProxy --> Upstream["Configured upstream proxy"]
    Upstream --> Internet

The boundaries are intentional:

  1. Rel.app is the only CEF owner.
  2. The app supervises its bundled rel --agent child.
  3. CLI, Rust SDK, and app configuration clients use only /v1 routes.
  4. Agent-to-Chromium work uses private bridge protocol v4.
  5. Proxy credentials remain in Rust/SQLite.
  6. Browser identity is isolated per persistent session.
  7. Logs are shared local files displayed by the app, not RPC, SDK, CLI, or database resources.

There are no alternate browser backends, unversioned route aliases, hidden network bypasses, or compatibility response formats.

Ordinary calls use one success envelope:

{
"status": "ok",
"request_id": "req_...",
"data": {}
}

All failures use one error type:

{
"status": "error",
"request_id": "req_...",
"error": {
"id": "SESSION_NOT_FOUND",
"http_code": 404,
"message": "The requested session was not found.",
"retryable": false,
"details": {}
}
}

Error IDs are stable machine contracts; messages are for people. HTTP status expresses the transport/resource result and always matches error.http_code. The same error object is reused in capture NDJSON and bridge v4.

The complete route and error catalog is in RPC.

One SQLite browser_profiles row, one app tab, one CEF request context, and one profile proxy port represent the same persistent session.

Identity Mutability Purpose
Numeric profile ID Immutable, never reused Database key, storage/log filename, proxy-port offset
session_id Immutable Federated CLI/RPC identity: machine-<UUID>.Session<ID>
Name Editable Human-visible label, default Session<ID>
Page ID Agent-process lifetime Ephemeral automation attachment within a session

The installation UUID is stable in rel_metadata; it avoids hostname leakage while making generated session IDs portable between machines. Clients omit session_id to create a session and retain the returned value. Supplying an unknown session ID returns SESSION_NOT_FOUND and never creates a replacement.

sequenceDiagram
    participant CLI as rel CLI
    participant Agent as rel --agent
    participant App as Rel.app bridge
    participant CEF as Session CEF browser

    CLI->>Agent: POST /v1/captures
    Agent->>Agent: validate, resolve/create session
    Agent-->>CLI: HTTP 200 application/x-ndjson
    Agent-->>CLI: capture.started
    Agent->>App: bridge v4 capture request
    App->>CEF: load, wait, act, read HTML
    CEF-->>App: final URL, HTML, target HTTP status
    App-->>Agent: v4 success or structured error
    Agent->>Agent: persist HTML and capture history
    Agent-->>CLI: capture.completed or capture.failed
    Agent-->>CLI: capture.finished with exit_code

Preflight errors use an ordinary non-200 JSON response. Once streaming begins, each physical line is a complete event object with the request ID. There is no nested stdout/stderr encoding.

A website HTTP status is target data, not RPC transport status. For example, a target 429 is target_http_status:429 with outcome:"target_error"; Rel uses the RATE_LIMITED error only when Rel itself limits the caller.

POST /v1/pages creates a process-local attachment. Subsequent POST /v1/pages/{page_id}/actions calls stay pinned to that page’s persistent session. Agent restart invalidates page IDs but not sessions.

The agent accepts loopback HTTP connections and sends non-health work through a single control worker. /v1/health and /v1/status bypass that queue so the supervisor can diagnose a busy or stalled operation. Health deadlines report a stall; they do not cancel in-flight Chromium work.

Every parsed HTTP request gets an opaque request ID returned in X-Request-Id, ordinary response envelopes, and every capture-stream event.

Each session has an isolated CEF storage directory:

~/Library/Application Support/Rel/Data/Chromium/Tab-<profile_id>

Cookies, site storage, service workers, preferences, and private HTTP cache do not cross session boundaries.

Direct sessions connect from CEF to origins. A proxied session instead receives an agent loopback endpoint at 17400 + profile_id - 1. The agent reloads that session’s current upstream proxy from SQLite for every connection and performs HTTP relay or HTTPS CONNECT. Credentials never enter Swift or CEF.

Main-frame readiness and background network activity are separate state. Pause cancels active work and gates later requests; Play removes the gate and reloads as needed. Downloaded-byte statistics exclude blocked, failed, cancelled, and shared-cache requests.

Adblock uses Brave’s adblock-rust engine and four list sources from Brave’s mirror: EasyList, EasyPrivacy, Brave Specific, and Brave Unbreak. Rel does not maintain a custom URL matcher or custom ruleset API.

Effective adblock is the conjunction of the global UserDefaults switch and the session’s adblock_enabled. Image filtering is separate and has two public modes: all and over_limit, plus image_size_limit_kb.

The wire format uses only image_blocking_mode; block_images is not accepted as an API alias. Blocked CEF requests are appended directly to the session NDJSON log.

flowchart TD
    Request["Eligible CEF request"] --> Shared{"Shared public asset hit?"}
    Shared -->|Yes| Hit["Serve cached body"]
    Shared -->|No| Network["Perform network request"]
    Network --> Private["Session-private Chromium cache/state"]
    Network -->|"safe, public, fresh asset"| Store["Store in shared partition"]

Data/Chromium/SharedAssets reuses safe public images, fonts, stylesheets, scripts, and favicons. Direct sessions share the direct partition. Proxied sessions are partitioned by proxy ID. Authenticated, cookie-bearing, private, stale, ranged, or oversized responses do not qualify.

A session can clear its private browser data without affecting shared assets. Settings can clear every private HTTP cache or the shared cache independently.

flowchart LR
    Agent["Rust agent"] --> DB[("Data/rel-data.sqlite3")]
    Agent --> Global["Data/Logs/global.ndjson"]
    Agent --> SessionLogs["Data/Logs/sessions/id.ndjson"]
    App["Rel.app"] --> SessionLogs
    App --> Defaults["UserDefaults"]
    App --> Rules["Brave list cache"]
    CEF["CEF"] --> Tabs["Data/Chromium/Tab-id"]
    CEF --> Assets["Data/Chromium/SharedAssets"]
Location Data
Data/rel-data.sqlite3 Sessions, proxies/secrets, captures/events, installation ID
Data/Logs/global.ndjson Process/global structured logs
Data/Logs/sessions/<profile_id>.ndjson One session’s structured logs
Data/Chromium/Tab-<id> Session-private browser state
Data/Chromium/SharedAssets Safe partitioned public assets
AdBlock/default-network-filters.txt Combined active Brave network rules
UserDefaults Global adblock and shared-cache settings

Rel creates and validates only the current schema. It does not assign a schema version or run migrations. Rust and Swift append NDJSON; the app’s Logs view tails the selected session file. Clear Logs removes that file and does not require an agent RPC.

Older root-level ~/Library/Application Support/Rel/rel.sqlite3, Logs, and Chromium data is ignored and is not automatically imported into Data.

  1. macOS launches Rel.app and initializes its CEF-capable application object.
  2. The supervisor terminates a stale matching bundled agent.
  3. It launches rel --agent with bridge v4 pipes.
  4. The agent opens the current SQLite store, starts profile listeners and RPC v1, then reports health.
  5. Browser contexts and terminal shells remain lazy until selected.

Closing the browser window leaves the app and agent resident. After repeated health failures, the supervisor replaces the child. Sessions, proxies, captures, browser data, and log files survive; page IDs and active bridge calls do not.

Quit stops the child and permits immediate app termination without waiting for CEF browser-close callbacks. Process teardown owns final Chromium cleanup.

  • RPC is unauthenticated but loopback-only; any local process can currently call it.
  • Bridge pipes are private to the supervised parent and child.
  • Proxy credentials are stored unencrypted in SQLite and consumed only by Rust; API responses expose only password_set.
  • Per-session CEF contexts isolate browser identity.
  • Shared caching excludes personalized responses and partitions proxy identity.
  • Rule sources are fetched over HTTPS from fixed Brave mirror paths and compiled locally.
  • Log files are user-local application-support data, not a remote interface.

make install-app builds and signs /Applications/Rel.app with the Swift host, the Rust CLI/agent (whose CLI uses rel-client), Brave adblock integration, Ghostty runtime, Clark-patched CEF, and helper apps. The installed bundle is the only supported runtime; loose alternate agents or browser backends are not supported.