The official Relay connects devices signed in to the same GitHub identity. GitHub identity is shared with the marketplaces. Users sign in from OpenBitFun; they do not create a Relay account or deploy a server.
The official endpoint is https://remote.openbitfun.com/v/1.0.2. This release is deployed with
its own process, database, assets, and reverse-proxy location. An existing
/relay deployment remains on its existing binary and data directory.
The Relay forwards opaque encrypted messages. Each device generates an X25519 private key locally and registers its public key after GitHub identity verification. Peers obtain public keys through the authenticated same-account directory and derive an AES-GCM key with X25519 and HKDF-SHA256. The Relay does not receive device private keys or upload copies of settings and sessions. Sessions and files are read from the owning online device on demand.
Selecting Same network starts the same Relay implementation inside the
Desktop host at http://<LAN-IP>:9700. Its SQLite database is local to that
host (<product-home>/relay-v1.0.0/local-server/relay.db), separate from the
official server database. Login, device registration, device discovery,
public-key lookup, RPC and presence all use the selected Relay endpoint.
The two modes differ only in endpoint and host startup; no device traffic is
forwarded from the local Relay to the official Relay.
Both modes verify GitHub identity through auth.openbitfun.com, so signing in
requires internet access. An invitation contains only the selected endpoint
and device id (/#/pair?did=<device-id>); scanning it grants no authority.
The controller must sign in and resolve that id in its same-account directory.
Anonymous room pairing and tunnel-provider startup have been removed.
SSH and Docker workspace connections remain independent of Relay login.
Self-hosting is supported through source and deployment scripts. The public mode uses a fixed official endpoint; it has no deployment wizard or editable Relay URL. A private Relay therefore needs a matching client build.
- Fork this workspace and read
CONTRIBUTING.md. Build the Relay and mobile controller from the same revision. Keep your fork's changes in source control. - Select your HTTPS endpoint in
product-domains/src/account.rs, then align the frontend constants insrc/web-ui/src/infrastructure/remote-connect/remoteConnectionState.tsandsrc/mobile-web/src/services/pairingLink.ts. Native clients have matching constants in KMPcore-transport/AccountDeviceLink.ktand HarmonyOSservices/AccountDeviceLink.ets; update the HarmonyOS account-link parser too. Search forhttps://remote.openbitfun.com/v/1.0.2to verify every runtime reference and corresponding test before building your distribution. - Decide who owns identity. You can retain the official GitHub identity
authority, or run the shared identity service
with your own GitHub OAuth application. For an independent authority, change
IDENTITY_ME_URLinrelay-service/src/identity.rsandDEFAULT_ACCOUNT_API_URLinservices-integrations/src/account_identity/mod.rstogether, and adapt the market sign-in links and callback/completion host.OPENBITFUN_ACCOUNT_API_URLoverrides the desktop/CLI identity API for development; the previousOPENBITFUN_MINIAPP_MARKET_API_URLalias remains readable. Relay never accepts an identity authority from a client request. - Use separate persistent data and asset directories, configure exact browser CORS origins, then put the service behind your own TLS reverse proxy. Build and exercise two devices using the same GitHub identity before distributing your fork. The public web controller must come from that matching build.
The scripts remain in this directory: deploy.sh deploys on the machine where
it runs, common.sh contains Docker/health helpers, and mirror.sh and
release-download.sh support mirrors and published images. Inspect
bash deploy.sh --help first. For fork code use
bash deploy.sh --build-from-source --global-mirror; the default image path
pulls a published upstream release, so it will not include your modifications.
An empty account database is normal: successful GitHub verification creates an
identity. Do not run retired add-user or password-reset commands.
The legacy script uses its own Compose project and defaults. For a fresh versioned deployment, prefer the isolated v1 Compose project and adapt its host paths, bind port, proxy host, and trusted upstream ranges to your infrastructure. Never reuse production data directories or an existing container name for a development deployment. There is no need to restore the removed deployment wizard to operate these scripts.
This directory owns the official service binary and maintenance tools. The
shared HTTP/WebSocket implementation lives in src/crates/services/relay-service.
Set RELAY_DB_PATH to a persistent SQLite database before starting the service.
Startup fails if it is missing; anonymous public relay mode is unsupported.
The service validates OpenBitFun access tokens against the fixed GitHub identity
authority at https://auth.openbitfun.com/api/v1/me.
cargo build --release -p openbitfun-relay-server
RELAY_PORT=9700 RELAY_DB_PATH=/var/lib/openbitfun-relay-v1/relay.db \
RELAY_ASSET_DIR=/var/lib/openbitfun-relay-v1/assets \
./target/release/openbitfun-relay-serverUse the isolated v1 Compose project.
Set RELAY_LISTEN_ADDR=127.0.0.1:19702 with host networking so the service can
verify the immediate loopback proxy peer. Invalid listener values fail startup.
Expose only the TLS reverse proxy. Keep the database and asset paths distinct from older deployments.
relay-admin supports listing and explicitly deleting accounts; GitHub login
creates identities. Password provisioning, password reset, and user-entered
Relay server URLs are retired.
These limits protect the service independently of reverse-proxy configuration. They are implemented in the shared Relay service, not in the agent loop.
| Resource | Limit and overload behavior |
|---|---|
| Authentication request body | 16 KiB; oversized bodies return 413 |
| Buffered HTTP request bodies | 512 MiB total reserved before buffering; overload returns 503 |
| Concurrent HTTP API requests | 2,048; overload returns 503 |
| Body read / API handler | 15 seconds / 130 seconds |
| HTTP request rate | 6,000/minute per source IP, and per account on the device API; overload returns 429 |
| GitHub authorization start / poll | 10 / 120 per minute per IP |
| Identity verification | 10 attempts/minute per IP; 3-second connect and 5-second response timeout; 64 concurrent outbound requests |
| WebSocket connections | 4,096 active sockets per Relay process; a further handshake is rejected with connection capacity exceeded |
| WebSocket connect / heartbeat | Namespace connect must finish within 15 seconds; ping every 15 seconds with a 45-second pong timeout; 30-second acknowledgement lifetime |
| WebSocket frames | 256 KiB per message; larger encrypted bodies use the short-lived HTTP payload lane |
| WebSocket outgoing queue | 128 queued messages per socket, plus 256 MiB of outbound message memory server-wide; a frame that cannot be queued within 2 seconds is dropped |
| RPC memory budgets | 64 MiB server-wide and 16 MiB per account, reserved against the estimated serialized size of in-flight calls; an exhausted budget answers server RPC memory budget busy or account RPC memory budget busy instead of queueing |
| Pending device RPCs | 2,048 globally, 64 per account, with a 256 MiB response budget: legacy HTTP-to-WebSocket bridge limits, kept in the shared crate but not driven by any current server route |
| Registered devices / active credentials | 64 / 256 per account; database-atomic admission |
| Device RPC ciphertext | 48 MiB, with JSON envelope allowance |
| Published Pages | 100 MiB per page, 10 MiB per file, 4,096 files per page; 1 GiB content-addressed asset store and 256 MiB in-memory asset volume by default |
| Page data | 4 MiB per blob, 2,048 blobs and 64 MiB mutable bytes per page, 10,000 blobs and 256 MiB mutable bytes per account; 20 MiB per page database, 1,000 rows and 2 MiB per query |
| Page functions | 64 concurrent workers globally, 16 per user, 8 per page; 3,000 requests/minute per user and 600 per page; 1 MiB request body |
Existing devices can reconnect at the registration limit. Idempotent token replays remain valid at the credential limit. Limits never delete a user's session, device, workspace, or other product data.
Every value in this table is a compile-time constant of the shared service.
Only deployment-level knobs are configurable: the RELAY_* settings below, the
container's CPU, memory, PID and file-descriptor limits, and the reverse proxy's
limit_req, limit_conn, body size and timeouts. A deployment that quotes a
larger limit than its container or proxy allows will fail at the smaller one.
One Relay process owns its sockets, rooms, presence, payload store, asset store and SQLite database in-process, and the Socket.IO build has no external adapter. A second instance cannot route to the first instance's sockets, so horizontal scaling needs per-account stickiness at the proxy; the 4,096-socket ceiling is per process, not per deployment.
Two paths cost work per connection rather than per message. Connect and disconnect pass one global gate that is held across the device row's SQLite round trips, and every open socket re-validates its credential every five seconds. Both are linear in concurrent sockets, and they matter when a whole fleet reconnects at once — a Relay restart is the event that produces that load, so size and test against a mass reconnect, not only steady traffic.
Bearer authentication precedes body buffering on device APIs. Device discovery, public-key lookup, message routing, and RPC correlation all enforce account ownership. Delegated controller credentials cannot register sockets, mint more credentials, or delete devices; revoking their parent device revokes them. Account-enabled services reject the retired anonymous pairing-room endpoints.
The identity HTTP client rejects redirects, bounds response size and duration,
and never accepts a caller-provided identity authority. Browser CORS uses an
explicit origin list; wildcard CORS is rejected by the standalone host when
account APIs are enabled. Published Pages must use an origin separate from the
account sign-in surface. Without both isolated origins, the standalone host
returns 503 for /api/pages, /api/page-auth, and /p routes.
Application limits do not replace network-layer protection. Public deployment also requires bounded proxy connections and request bodies, TLS, upstream DDoS protection, and alerts for saturation, rejected requests, and disk growth. Do not log Authorization headers, OAuth transaction secrets, tokens, request bodies, or URL query strings containing sign-in state.
Strip only the new version prefix when forwarding. Do not replace the existing
/relay location. The proxy must overwrite forwarded IP headers with its own
observed source address, and the upstream port must be unreachable externally.
The Relay trusts forwarded client IPs only from an immediate loopback peer.
location ^~ /v/1.0.2/ {
proxy_pass http://127.0.0.1:19702/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
client_max_body_size 49m;
client_body_timeout 15s;
proxy_read_timeout 140s;
proxy_send_timeout 30s;
}Define the standard $connection_upgrade map and deployment-specific
limit_req/limit_conn zones in the owning Nginx configuration. Tune worker and
file-descriptor limits against measured concurrent sockets; daily active users
alone are not a capacity measurement. Preserve ordinary streaming and attachment
traffic in the load test when tuning rate limits.
Before opening the new location, exercise invalid/expired credentials, cross-account access, concurrent quota exhaustion, oversized/slow bodies, unauthenticated and slow-reader sockets, cancellation, reconnect, and normal streaming. Verify that overload returns promptly and releases memory. Confirm that the old service remains healthy and that rollback only removes the new location and process.
| Endpoint | Purpose |
|---|---|
GET /health, GET /api/info |
Health and service version |
POST /api/auth/github/start, /api/auth/github/poll |
Browser GitHub authorization |
POST /api/auth/login |
Exchange verified identity for a keyed device credential |
POST /api/auth/logout |
Revoke a credential |
POST /api/auth/delegate |
Issue a separately keyed, restricted controller credential |
POST /api/auth/provision-device |
Authorized SSH host bootstrap |
GET /api/devices |
Same-account device directory |
PATCH /api/devices/{id} |
Update an account device alias or self-reported metadata |
GET /api/devices/{id}/key |
Same-account device public key |
DELETE /api/devices/{id} |
Explicit device removal and revocation |
GET /v1/updates |
Authenticated Socket.IO account and machine scopes |
POST /v1/rpc/payloads, GET /v1/rpc/payloads/{id} |
Account-scoped encrypted bulk RPC bodies (short-lived) |
POST /v1/sessions, GET /v1/sessions/{id}, GET/POST /v3/sessions/{id}/messages |
Retired; answer 410 Gone with {"error":"relay_session_history_retired"} |
Realtime clients authenticate the namespace and wait for auth-ok before
registering or calling methods. Machine-owned RPC methods route inside the
same account; only the selected target socket can acknowledge a request.
A lost acknowledgement reports an unknown outcome and never replays a mutation.
Small encrypted messages travel over the live connection; larger RPC bodies use
short-lived HTTP references that expire on their own.
GET /api/devices retains its existing scope: only same-account desktop/session-host
control targets are listed, including offline devices. Registered mobile and watch
devices remain hidden; legacy rows without a device kind remain desktop targets.
The additive nullable fields are:
| Field | Meaning |
|---|---|
device_alias |
Account-owned user alias, persisted independently of the technical name |
device_model |
Host-reported device model |
device_os |
Host-reported operating-system name |
device_os_version |
Host-reported operating-system version |
client_version |
Client build string reported at login/handshake, null when unreported |
client_protocol |
Client protocol number reported at login/handshake, null when unreported |
compatible |
Relay-computed; whether the requesting token's device may remote-control this device |
device_name remains the technical/self-reported name; the Relay never replaces it
with the alias. New clients display alias, then technical name, then device id as
fallback. Older clients continue displaying the technical name and can ignore the
additive response fields. Clients must tolerate absent fields from older Relays.
PATCH /api/devices/{id} requires an authenticated full device bearer token and
returns 204 No Content on success. A device may change the alias of any device
in its own account, but may update model/OS metadata only for its own authenticated
device id. Delegated controller tokens cannot patch devices (403); missing or
other-account targets return 404 without revealing ownership.
{"device_alias":"Build host"}sets the alias;{"device_alias":null}clears it.- Missing fields mean no change, including a missing alias. An empty object is a no-op, not a request to clear fields.
device_model,device_os, anddevice_os_versionaccept strings only when present in a PATCH; explicitnullis rejected rather than clearing metadata.- Each alias or metadata string is limited to 256 UTF-8 bytes, not characters,
and must be nonblank and contain no control characters. Use alias
null, not an empty string, to clear an alias. - PATCH uses strict
deny_unknown_fields: unknown mutation fields, including an arbitrary metadata extension object, are rejected rather than silently ignored and reported as successful. This differs deliberately from extensible response objects, whose unknown fields clients may ignore.
Before sending mutations, clients negotiate capabilities through GET /api/info:
device_alias_v1 enables alias updates and device_metadata_v1 enables metadata
reporting/updates. These are strings in the capabilities array;
protocol_version remains 3. Missing capabilities mean unsupported, regardless
of package version. Future PATCH fields or behaviors require their own capability
negotiation before use; clients must not probe old servers with unknown mutations.
An older Relay returning 404/405 must produce an explicit unsupported state,
not a successful local-only rename.
Newer clients also report their build so the Relay can refuse a remote-control
pair it cannot prove compatible. device_client_build_v1 advertises this: an
optional clientVersion string and clientProtocol number travel with the
realtime handshake and with POST /api/auth/login.
- The device row stores the build from the current connection. Every login
and handshake refreshes both values, including writing
NULLwhen the client reports nothing, so a stale build is never left behind.clientVersionis limited to 64 UTF-8 bytes with no control characters; a malformed or absent value is treated as unreported rather than rejected, so an older client still connects. - Compatibility is decided only from
clientProtocol, and a report is required rather than merely tolerated: control is allowed only when both sides reported a protocol number and the numbers match. Two legacy clients that report nothing, and any pair where either side never reported, are incompatible. GET /api/devicesreports the relay-computedcompatibleflag per target, based on the requesting token's device row. Incompatible devices are still listed, never hidden or deleted.- A
rpc-callbetween incompatible devices is answered with afailureacknowledgement —incompatible client build: remote control requires matching client versions— and is not dispatched.
Presence device-presence entries carry the raw client_version and
client_protocol values only; the relay-computed compatible flag appears
exclusively in GET /api/devices.
Login and provisioning accept optional model/OS metadata. Omission by an older client preserves stored metadata, and neither registration nor reconnect changes the alias. Login (and the realtime handshake) additionally report the client build described above, which is refreshed rather than preserved. Additive SQLite migrations preserve existing device rows; aliases and metadata survive Relay restart when the same persistent database is used. Startup resets stale online presence, not the alias or technical metadata.
Successful patches notify online same-account clients through the existing
device-presence channel, whose device entries also carry the new fields.
Notifications are refresh hints, not durable directory state: re-fetch
GET /api/devices after a patch or notification and on reconnect/normal refresh.
The relay stores no session content. Session transcripts, terminal output and
the workspace/session catalog are host-owned streams
(services-integrations::remote_connect::host_stream): a controller reads
pages on demand with the pairwise-encrypted read_stream device RPC, and the
host pushes an encrypted host-stream-changed device event naming only the
stream id, epoch and newest sequence. Both travel through the same RPC and
ephemeral device-event forwarding as every other command, so the relay never
sees plaintext and keeps nothing after delivery. When the controlled device is
offline there is no history to show, by design.
Compatibility on the same connection:
- An older client that still calls the session history routes or requests the
Socket.IO
sessionscope receives410 Gone/ an explicit auth failure with the samerelay_session_history_retiredreason, never an empty page. - A newer client against an older relay ignores the
updateframes that relay still emits; stream pages and hints do not depend on relay-side state. - Hosts advertise
host_stream_v1in their handshakecapabilities; controllers check it before opening a stream and report an older host as unsupported instead of probing it with unknown commands. - On start-up the service drops the retired
realtime_sessions,realtime_messagesandrealtime_account_sequencetables from an existing database and runsVACUUM, so previously stored ciphertext is removed from disk without an operator step. A version that is still serving clients cannot be patched after the fact: bound its growing log by hand until it reaches the retirement switch, as described in the v1 deployment guide.
The old /ws, HTTP device rpc and messages routes are retired. Deploy the
new client and server together under a separate versioned relay prefix.
RELAY_PORT, RELAY_DB_PATH, RELAY_STATIC_DIR, RELAY_ROOM_WEB_DIR,
RELAY_ASSET_STORE_MAX_BYTES, and RELAY_CORS_ALLOW_ORIGINS are operator
settings. RELAY_PAGE_PUBLIC_BASE_URL and RELAY_PAGE_AUTH_BASE_URL must be set
together and use distinct origins when protected Pages are deployed.
cargo test -p openbitfun-relay-server --bin openbitfun-relay-server
cargo test -p openbitfun-relay-service
cargo check -p openbitfun-relay-server
node scripts/check-core-boundaries.mjsUnit and integration tests are local evidence. Record live remote-control, peer-device, remote-workspace, and detached-dispatch validation separately.