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.
System boundaries
Section titled “System boundaries”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:
- Rel.app is the only CEF owner.
- The app supervises its bundled
rel --agentchild. - CLI, Rust SDK, and app configuration clients use only
/v1routes. - Agent-to-Chromium work uses private bridge protocol v4.
- Proxy credentials remain in Rust/SQLite.
- Browser identity is isolated per persistent session.
- 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.
RPC shape
Section titled “RPC shape”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.
Session identity
Section titled “Session identity”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.
Capture flow
Section titled “Capture flow”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.
Control and concurrency
Section titled “Control and concurrency”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.
Browser and network routing
Section titled “Browser and network routing”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.
Filtering
Section titled “Filtering”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.
Cache layers
Section titled “Cache layers”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.
Persistence
Section titled “Persistence”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.
Lifecycle
Section titled “Lifecycle”Startup
Section titled “Startup”- macOS launches Rel.app and initializes its CEF-capable application object.
- The supervisor terminates a stale matching bundled agent.
- It launches
rel --agentwith bridge v4 pipes. - The agent opens the current SQLite store, starts profile listeners and RPC v1, then reports health.
- Browser contexts and terminal shells remain lazy until selected.
Restart and residency
Section titled “Restart and residency”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.
Fast quit
Section titled “Fast quit”Quit stops the child and permits immediate app termination without waiting for CEF browser-close callbacks. Process teardown owns final Chromium cleanup.
Trust boundaries
Section titled “Trust boundaries”- 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.
Build and deployment
Section titled “Build and deployment”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.