← 1-bit-bridge

Features

A reference rundown of what the bridge does and the design choices behind each piece. If you're new here, start with the setup walkthrough first — that gets you a working install in ten minutes. This page is for once you know your way around.

Library manifest

SQLite-backed index served in one paginated request — replaces SMB walks.

Pairing

Admin-approval (default) and manual-token flows.

Tailscale

Three modes: cli, tsnet, disabled.

Public deployment

Internet-facing bridges with admin login + Let's Encrypt.

HTTP/3 (QUIC)

Same port as HTTP/2; iOS picks the better transport.

Upscale variants

Optional offline PCM resample for high-end DACs.

Library Inspector

Folder-first admin UI for browsing, searching, and managing variants.

Updates

Opt-in auto-install with quiet hours.

Backups

Dated snapshots, CLI-only restore.

Bonjour discovery

Auto-discovery on the LAN.

Admin console

Loopback-only by default; password-protected in public mode.

Observability

Prometheus /metrics + GET /v1/diagnostics.

Logs

Where they live, what's logged, what's deliberately not.

Container & headless

Env-var overrides for Docker / NAS deployments.

Library manifest

The bridge keeps a SQLite-backed index of your library — every track row carries the file path, sample rate, bits per sample, DSD flag, ReplayGain, and (when enrichment finds them) MusicBrainz IDs for release / artist. The iOS app fetches the whole library in one paginated GET /v1/manifest call, or a ?since=<mtime> delta when it already has a baseline.

This replaces the iOS app's two-phase SMB scan (walk + tag-extract). On a 50,000-track library over Tailscale, the difference is a few hundred milliseconds via the bridge versus several minutes via SMB.

Bytes are never transcoded. FLAC, ALAC, WAV, AIFF, MP3, AAC, DSF, DFF stream as-is. The iOS app still pre-caches the full .dsf / .dff before playback so the DAC's DoP lock isn't at risk.

What's extracted from tags. Beyond the basics, each Track can carry composer, conductor, work, originalYear, and bpm — the iOS app surfaces these as a "Conducted by X" header on conductor-consistent albums and a classical-metadata subtitle line in track rows. Multi-value ARTIST and ALBUMARTIST source tags (FLAC Vorbis, MP4 raw array) are preserved by the extractor and serialised as a ; -joined scalar string on the wire (artist / albumArtist) — the iOS app long-presses on a ; -separated credit to navigate to the specific featured artist. ALAC source bit depth is read from the MP4 alac config atom rather than the decoder default of 32-bit, so the iOS Now Playing chip shows the real depth ("ALAC 44.1 / 16"). DFF files now carry tags from the DIIN chunk; AIFF and WAV walk the container chunks for embedded ID3 or LIST/INFO.

Pairing models

Two flows are supported. They produce identical bearer tokens; the difference is who initiates and who approves.

Admin-approval pairing (recommended)

The iOS app asks the bridge for permission, the operator approves it on the admin console. This is the default in iOS 1-bit 1.2 and lets users add their own devices without the operator having to mint a token in advance.

  1. iOS app submits a request to POST /v1/pairing/requests. The bridge holds it in memory with a 6-digit verification code, the device-chosen display name, and the iOS app's self-reported version.
  2. Admin console shows a Pending pair request banner on every page. Operator clicks in, verifies the 6-digit code matches what the user sees on their phone, gives the device a name, and approves.
  3. Bridge mints a long-lived bearer token, hands it to the iOS app via the polled GET /v1/pairing/{requestId}, and discards the pending-request record.

The verification code defends against a same-network attacker — only the device with the matching code is talking to the bridge.

Manual token (QR / paste)

The legacy flow, still supported. Operator generates a token in the admin console's Devices tab; iOS app scans the resulting QR code or pastes the three fields (URL, token, TLS fingerprint) by hand.

Use this when you're configuring a phone for someone else, or when admin-approval is unavailable (e.g. headless server with no convenient browser, paired iOS device is older than 1.2).

What's stored, what isn't

Per paired device, the bridge stores: a 12-hex ID, a friendly name, the SHA-256 hash of the bearer token (never the raw token), creation and last-used timestamps, and the most recent X-Client-Version header value. TLS fingerprints are pinned by the iOS app, not by the bridge — the bridge has no idea what cert your phone trusts. See privacy for the full list.

Tailscale integration

If you want to reach your library from outside your home network, Tailscale is the supported answer. The bridge offers three modes via tailscale.mode in bridge.yaml.

tsnet — embedded tailnet node (recommended for new installs)
The bridge runs its own tailnet node in-process. No system Tailscale install required, no tailscale CLI on $PATH, and Let's Encrypt cert renewal happens in-process. Authenticate once with ./bridge tsnet auth on first run, or set tailscale.authKey in bridge.yaml for unattended deployments.
cli — delegate to system Tailscale
The bridge shells out to the host's installed tailscale binary for status detection and Let's Encrypt cert provisioning on *.ts.net connections. Use this if you already run Tailscale on the host and want one set of credentials. macOS users: see the sandboxed-CLI gotcha in troubleshooting.
disabled — turn it off
LAN-only deployment. Both the CLI auto-pilot and the embedded node are skipped. The admin tile renders a one-line explanation so anyone who flipped to disabled by accident can recover.

Mode changes require a bridge restart. Switching between modes never invalidates pairings — the iOS app pins the bridge's certificate fingerprint, which doesn't depend on the transport mode.

Cert expiry warning. When the magic-DNS Let's Encrypt cert is within 30 days of expiring, the bridge logs a structured warning at startup so the operator notices before tsnet's in-process renewal next runs. tsnet handles renewal automatically; the warning is a backstop for the rare case where renewal has been failing silently.

Public deployment mode (v0.1.4)

For bridges intentionally exposed to the public internet — a VPS, a port-forwarded home server, or any deployment that lives outside a LAN / Tailscale tunnel — the bridge ships an opt-in public mode with the security pieces a real public host needs.

Choose at install time. bridge init --public selects the public profile: it sets deployment.mode: public, switches the admin listener to a routable interface, generates a 16-character random password for the single admin user, and (if autocert.enabled: true) configures Let's Encrypt for the public TLS certificate. The initial password is printed once to the terminal — copy it before closing the shell, or rotate later with bridge admin reset-password.

Native ACME / Let's Encrypt. Public-mode bridges fetch and renew their TLS certificate via the ACME v2 protocol — no Caddy / nginx / certbot sidecar needed. The cert + account key are cached under <dataDir>/acme/; renewal happens in-process. HTTP-01 needs port 80 reachable to the bridge; TLS-ALPN-01 needs only the public TLS port. Bring-your-own certs are still supported via the existing tls.certFile / tls.keyFile knobs.

Admin login. A single-user form-login page lives at /login. Successful login sets a session cookie carrying a 256-bit token; the server stores only its SHA-256 hash in memory and clears every session on restart. Sessions idle-expire after 24 hours and hard-expire after 7 days. The login handler rate-limits per (client IP, username) tuple — failed attempts and rate-limit refusals are logged with the client IP (see Logs).

Reverse-proxy friendly. If you terminate TLS at a reverse proxy and forward to the bridge over plain HTTP, set deployment.adminTLSTerminatedByProxy: true so the admin login extracts the real client IP from the standard proxy headers instead of the proxy's loopback address.

Loopback-mode deployments are unchanged: no password, no session cookies, no ACME plumbing. The default for fresh installs without --public stays loopback-only.

HTTP/3 (QUIC) (v0.1.4)

The iOS-facing API is served simultaneously over HTTP/2 (TCP) and HTTP/3 (QUIC over UDP) on the same port. The iOS app advertises support for both and the OS picks the better transport per request — typically a clear win on lossy cellular / Tailscale-relayed links where TCP head-of-line blocking compounds with high RTT.

No operator-side configuration is required; HTTP/3 is on by default. To turn it off on a host whose firewall blocks inbound UDP, set disableHttp3: true in bridge.yaml or export BRIDGE_DISABLE_HTTP3=true — the bridge falls back to TCP-only.

The QUIC listener uses the same TLS certificate as HTTP/2 and enforces TLS 1.3. In tsnet Tailscale mode the bridge binds a UDP listener inside the embedded tailnet node alongside the TCP one, so HTTP/3 is reachable over Tailscale too — useful when DERP-relayed connections benefit most from the loss-recovery improvements.

Upscale variants (v1.2, opt-in)

What it does. The bridge can produce upscaled PCM "variants" of your tracks — typically 96 kHz / 24-bit FLAC sidecars derived from a 44.1 kHz / 16-bit source — using the local sox binary. The iOS app surfaces a wand icon next to each eligible track; tap to switch the playback source between the original and the upscaled variant.

Who it's for. Operators with a high-end DAC that responds well to upsampled material, and disk headroom for the additional .flac sidecars. Variants are stored under transcoded/ in the bridge's data directory; figure roughly 3× the source size per variant.

Off by default. Even on v1.2 bridges, no variants exist until the operator opts in by setting upscale.enabled: true in bridge.yaml (or toggling the switch in the admin console's Settings tab) and a paired iOS device explicitly requests an upscale via the wand icon. Nothing happens silently in the background.

How to enable it.

  1. Install sox: brew install sox (macOS), apt install sox libsox-fmt-flac (Debian/Ubuntu), or download the official build from sourceforge. Verify with sox --version.
  2. Open the admin console → SettingsUpscale and flip the toggle. Or set upscale.enabled: true in bridge.yaml.
  3. On the iPhone, navigate to any FLAC / ALAC / WAV track. The wand icon appears on the Now Playing screen.
  4. Long-press the wand → Generate. The bridge enqueues the job; processing is asynchronous (typically a few seconds per track on a modern CPU). Once ready, tap the wand to switch the playback source.

Bit-exact still applies. The bridge never transcodes silently. The original file is always available, untouched, alongside any variant. Switching playback sources is one tap on the wand icon. If you don't trust the upscaling, don't enable it — the rest of the bridge works exactly as before.

Where variants live. By default under transcoded/ inside the bridge's data directory. Set upscale.variantsDir in bridge.yaml (or pass --variants-dir on the CLI) to point at a larger volume, or to lay variants out next to their source files in a source-mirrored tree. The CLI command bridge upscale move migrates an existing variant set without re-generating.

Lifecycle. Delete a single variant from the admin console's Library Inspector (per-track entry has a Delete-variant action), or run ./bridge upscale --gc to remove orphans whose source files are gone. The garbage collector is symmetric — it also reaps track_variants rows whose disk files were deleted out-of-band (a tidy-up that ran a sox rm by hand). External deletions are detected on the next scan and the row drops; the iOS app's wand promotes back to "ready to upscale".

Updates

The bridge polls https://api.github.com/repos/acoseac/1-bit-bridge/releases/latest every six hours and surfaces an Update available banner in the admin console when a newer release exists. The poll is unauthenticated, sends no device identifier, and sees no library statistics — just the standard GitHub API call any browser would make.

Auto-install is opt-in. Enable it in Settings → Updates if you want the bridge to install patches automatically. When enabled:

Manual install runs the same path: download from the admin console's Updates tab, verify, atomic swap, one-tap rollback if the new build doesn't behave.

Backups

The bridge writes timestamped snapshots of its data directory under backups/<timestamp>/. Both manual (Settings → Take snapshot now) and automatic (configurable interval, configurable retention via backup.maxKept). Each snapshot contains the YAML config, the SQLite manifest, and the paired-device list — enough to restore a full working bridge.

Restore is a CLI operation. Stop the bridge via your service manager first (launchctl unload on macOS, systemctl --user stop 1-bit-bridge on Linux, sc.exe stop 1-bit-bridge on Windows; see troubleshooting for the exact commands), then:

./bridge restore --snapshot <timestamp>

Start the bridge again via the same service manager. Kept off the web UI by design — accidentally clicking restore on a running bridge would replace the live library mid-stream. The two-step CLI flow surfaces the implication.

Bonjour discovery

The bridge advertises itself on the LAN as _onebit-bridge._tcp. The iOS app picks it up via the Discover on network entry in the Add Bridge sheet — no manual URL entry, no fingerprint paste.

The TXT record carries:

If multiple bridges run on the same network, each one advertises independently and shows up as a separate entry in the iOS Discover list.

Library Inspector (v0.1.3)

The Inspector is a folder-first browsing surface for the bridge's library — the admin-side counterpart to the iOS app's Sources view. It loads under Library → Inspect in the admin console.

A dedicated Jobs page lists pending / in-flight / failed upscale jobs with their submission time and last-error string so the operator can see what's actually happening rather than guessing from the iOS app's wand state.

Admin console tour

In loopback-mode deployments (the default) the admin console binds http://127.0.0.1:7789/ with no login — anyone with a shell on this machine already has filesystem access to the token store, so adding a password would be theatre. In public-mode deployments the admin console binds a routable HTTPS interface and requires a password login; the rest of the tabs work the same way.

The admin UI ships an indigo palette refresh and a phone-width responsive breakpoint as of v0.1.4 — the previous grid + tables now collapse to a card stack at narrow widths, and the top-nav becomes a hamburger menu.

Admin console tabs, what each manages, and what's reserved for the CLI
TabPurposeWhat's CLI-only
DashboardServer status, library size, paired devices, latest scan stats, certificate expiry, Tailscale state
LibraryAdd / remove root folders, full or delta re-scan, per-folder progress, orphan-variant cleanup. The Inspect button opens the Library Inspector.
DevicesPair (manual flow), approve admin-approval requests, rotate / revoke / rename tokens
JobsPending / in-flight / failed upscale jobs with timestamps and last-error strings
SettingsTailscale mode, upscale toggle, variants directory, auto-update + quiet hours, backup schedule + retention, custom endpointsTLS cert rotation (bridge cert rotate), config edits other than the exposed knobs
UpdatesManual update check, install / rollback, release notes link
LogsLive tail of the service-manager log (last 500 lines)Full log retention via your service manager

Restore-from-snapshot is intentionally CLI-only (see Backups). TLS cert rotation is intentionally CLI-only because the operator-restart step has no safe web equivalent — clicking Rotate cert in a browser would orphan the very session that issued the click.

Logs

The bridge writes structured logs to your platform's service manager:

Per-platform commands for tailing the bridge service log
PlatformHow to view
macOS (launchd)log show --predicate 'subsystem == "com.acoseac.bridge"' --last 1h
Linux (systemd)journalctl --user -u 1-bit-bridge -f
Windows Service%PROGRAMDATA%\1-bit-bridge\bridge.log
Manual runstdout / stderr of the bridge serve process

What's deliberately not logged: bearer tokens (raw or hashed), full HTTP request bodies, and contents of MusicBrainz / Deezer / iTunes lookups.

What is logged in error paths: library-relative paths of files that failed a scan / upscale / read — e.g. music/Diana Krall/Live in Paris/01.flac. These appear only on errors so the operator can act on the failure. The path is library-relative (no /Users/... prefix), but it does contain artist / album / track names by typical folder layout. If you handle these logs, treat them with the same care as the library metadata itself.

Client IP addresses are not logged in loopback-mode deployments. In public-mode deployments, the admin login handler logs the client IP alongside the attempted username on failed login or rate-limit refusal — this is necessary so the operator can investigate brute-force attempts. See privacy → logs for the full breakdown.

Observability (v0.1.4)

Two complementary surfaces for monitoring a running bridge — both counter-only, both safe to scrape periodically.

GET /metrics — Prometheus exposition on the admin listener
Standard Prometheus text format. Counters for HTTP requests, SQLite lock-wait quantiles, upscale pool depth, manifest-builder time-on-disk, log-event counts by level, MusicBrainz cache hit ratio, Tailscale peer counts. Loopback-only in loopback-mode deployments; session-authenticated in public mode (same gate as the rest of the admin console). Point a local Prometheus / Grafana Alloy / VictoriaMetrics at it; no separate sidecar is required.
GET /v1/diagnostics — paired-iOS-visible counter snapshot
JSON payload with the same flavour of counters, served on the iOS-facing API behind the standard bearer-token auth. The iOS app's Diagnostics view consumes this so users on a paired device can see whether the bridge is healthy without needing to open the admin console. Counters and structured state only — no log text, no file paths, no IPs. Including recent log lines would force per-line privacy redaction and risk leaking PII into iOS-side diagnostic snapshots the operator might paste into a public issue.

Neither endpoint is part of the wire protocol the iOS app's core sync flow depends on; both are advertised as feature flags inside the features field of GET /v1/health so older iOS builds simply ignore them.

Container & headless deployments

The bridge runs cleanly under Docker, on a Synology / TrueNAS, or on any headless Linux box. Most config knobs that bridge.yaml exposes have an environment-variable equivalent for cases where editing YAML inside a container is awkward.

Precedence. Env wins over YAML; YAML wins over hardcoded defaults.

Environment-variable overrides for headless / container deployments, mapped to their bridge.yaml keys
Env varYAML keyEffect
BRIDGE_LISTEN_ADDRESSlistenAddressHTTPS listener (default :7788)
BRIDGE_ADMIN_ADDRESSadminAddressAdmin console listener (default 127.0.0.1:7789; bound loopback by default)
BRIDGE_DATA_DIRdataDirWhere config, manifest, tokens, artwork, backups live
BRIDGE_LIBRARY_NAMElibraryNameDisplay name shown in the iOS app's Sources list
BRIDGE_LIBRARY_ROOTSlibraryRootsColon-separated list of music-library folders (e.g. /music:/audiobooks)
TS_AUTHKEYtailscale.authKeyPre-baked Tailscale auth key for unattended tsnet mode boot

For the full Docker recipe — multi-arch builds, docker-compose example, reverse-proxy notes for the admin console — see the Docker deployment guide on GitHub.

Protocol

Every endpoint, request shape, and error code lives in PROTOCOL.md. The wire-protocol version is currently 1; additive fields ship without a version bump, breaking changes don't. The iOS app advertises support for protocol versions in the X-Bridge-Protocol header and refuses to talk to a server it doesn't understand.

v0.1.3 ships several additive fields against protocol v1: composer / conductor / work / originalYear / bpm on Track; multi-value source-tag preservation joined as ; -separated strings in the existing scalar artist / albumArtist fields; the roots structured-reachability array on GET /v1/health; pushEventsSupported and pairingEventsSupported capability flags on GET /v1/health; and a new upscale.complete event on the existing SSE stream. Older iOS app builds simply ignore the new fields; no Mirror-PR break.

v0.1.4 layers more additive fields: GET /v1/diagnostics (counter-only health snapshot), the optimize-* variant class (alongside the existing upscale-* class, routed onto a priority queue that lets CarPlay-Optimize jobs skip a backlog of pre-fetched upscales), and the diagnosticsSummary entry in the features array of GET /v1/health. HTTP/3 (QUIC) is purely transport — the JSON shapes are identical to HTTP/2. Pre-v0.1.4 iOS app builds ignore the new fields; protocol version stays at 1.