Sync & access
How the library is indexed, how you pair, and how you reach it from anywhere.
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.
Atlas premium metadata (v0.1.7, opt-in)
Standard enrichment (MusicBrainz / Cover Art Archive / Deezer / iTunes) gets you correct titles and a 500 px cover. Atlas is an optional layer on top, for when you want more: high-resolution album art (up to ~1080 px), artist biographies, and album descriptions, cached on the bridge and served back to every paired device.
The bridge never holds an Atlas credential of its own. By default the iOS app — which does hold the credential — fetches the metadata and pushes it to the bridge (POST /v1/atlas-ingest), which caches it in bridge.db and the artwork cache; on that path the bridge makes no outbound Atlas request. For large libraries you can additionally opt into bulk harvest (atlas.harvestEnabled): the app provisions a short-lived, device-attested credential and the bridge then fetches in bulk directly from the Atlas server — by default the reference instance at atlas.ars.md, or a self-hosted one you point it at — sending only your library's distinct artist and release MusicBrainz IDs. Covers carry a content version (artworkVersion) so the app refreshes a changed cover in place. Off by default; see privacy → Atlas metadata for the exact data flow.
PDF album booklets (v0.1.8, opt-in)
When a release shipped with a digital booklet — the liner-notes PDF with sleeve essays, personnel, credits, and lyrics — the bridge caches it locally and serves it to your paired devices, so the artwork you already see gains the words that came with the record. Booklets can be large (10–64 MB); the iOS app downloads each one once and caches it.
Booklets ride the Atlas path, and inherit its posture. They're sourced through the same Atlas bulk-harvest the bridge uses for bios and descriptions, so the feature only lights up when Atlas harvest is enabled (atlas.enabled + atlas.harvestEnabled) — off by default, like the rest of Atlas. The bridge asks the Atlas service whether a given release has a booklet, keyed only by that release's MusicBrainz ID; it sends no file paths and no library inventory. A bridge without the feature simply advertises no booklets and older apps move on.
On the wire it's additive against protocol v1: a bookletTag content marker on a manifest Track (a cache-bust hint that changes when the booklet's bytes change) advertises that GET /v1/booklet/{musicBrainzAlbumID} will serve the PDF, and the booklets flag in GET /v1/health tells the app the surface exists. A request for a booklet the bridge knows about but hasn't finished downloading returns 202 and prioritises that release in the queue, so a retry lands it. See privacy → Atlas metadata for the exact data flow.
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.
- 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. - 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.
- 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
tailscaleCLI on$PATH, and Let's Encrypt cert renewal happens in-process. Authenticate once with./bridge tsnet authon first run, or settailscale.authKeyinbridge.yamlfor unattended deployments. cli— delegate to system Tailscale- The bridge shells out to the host's installed
tailscalebinary for status detection and Let's Encrypt cert provisioning on*.ts.netconnections. 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
disabledby 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.
Audio & playback
Optional PCM upscaling, serving your library to LAN renderers, and pulling another LAN server's library in — always bit-exact.
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.
- Install
sox:brew install sox(macOS),apt install sox libsox-fmt-flac(Debian/Ubuntu), or download the official build from sourceforge. Verify withsox --version. - Open the admin console → Settings → Upscale and flip the toggle. Or set
upscale.enabled: trueinbridge.yaml. - On the iPhone, navigate to any FLAC / ALAC / WAV track. The wand icon appears on the Now Playing screen.
- 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. External deletions are detected on the next scan and the row drops; the iOS app's wand promotes back to "ready to upscale".
Audio analysis (v0.1.7, opt-in)
What it does. With analysis.enabled: true, the bridge decodes each track locally and computes three things: a peak waveform for the iOS scrubber, R128 loudness (a ReplayGain value when the file doesn't carry one), and an estimated musical key and tempo. Everything runs on the bridge host — no track audio is ever uploaded.
What it needs. The sox binary (with FLAC support), plus ffmpeg / ffprobe as a fallback decoder for AAC / m4a sources sox can't read. The bridge probes for these at startup; if they're missing it simply leaves analysis off and logs a one-liner — nothing else is affected. Results are cached in a track_analysis table, with peak-envelope sidecars under <dataDir>/waveforms/.
What it feeds. The waveform drives the scrubber; loudness sharpens the iOS quality chips; and key + tempo are what the harmonic Smart Mixes (Auto Mix, Wind Down, Lift Off) are built from. Like upscaling, it's entirely opt-in and off by default.
DLNA / UPnP MediaServer (v0.1.5, opt-in)
The bridge can present your library to DLNA / UPnP renderers on your LAN — the Chord 2Go is the headline target, but generic UPnP players (mconnect, BubbleUPnP, Linn Kazoo) work too. It exposes a standard UPnP ContentDirectory: an All Tracks flat list and a Folders hierarchy mirroring your on-disk Artist → Album layout, plus a free-text Search. Audio is served bit-exact — no transcoding — and any cached upscale variants surface as alternate streams the renderer can pick.
Off by default. Enable with dlna.enabled: true in bridge.yaml (or the admin Settings toggle), then restart. Defaults: friendly name "1-bit Bridge", TCP port :7790, SSDP advertised on every LAN-eligible interface.
Security model — read this before enabling. UPnP has no authentication; renderers can't speak bearer tokens. So while DLNA is on, any device on your local network can browse and stream your library over the DLNA port with no token. The LAN bind is the gate, not a password. Two structural guards apply: the listener binds only private / link-local interfaces — never loopback or a routable public interface, and never the Tailscale interface unless you explicitly opt in with dlna.allowTsnet: true (SSDP discovery never crosses Tailscale regardless, since multicast doesn't traverse it) — and a bridge in public mode refuses to start the MediaServer at all. Don't enable DLNA on an untrusted shared network.
Renderer discovery. A separate opt-in, dlna.discovery.enabled: true (default off), has the bridge scan the LAN (SSDP M-SEARCH) for renderers and expose the result on the authenticated GET /v1/renderers endpoint, so the iOS app can offer "play to" targets. The discovered list stays on your network — see privacy → DLNA for the full data posture.
UPnP / DLNA upstream sources (v0.1.6, opt-in)
The mirror image of the MediaServer above: instead of serving your library to a UPnP device, the bridge can pull another UPnP / DLNA MediaServer's library in — folding its tracks into your 1-bit library without copying a single file. The headline target is a Chord 2Go serving its own SD-card library, but any MiniDLNA-class server (a NAS media server, MinimServer, and the like) works.
How it works. The bridge runs an SSDP discovery client, walks the upstream server's folder view into its own SQLite manifest, and — crucially — proxies the audio at play time rather than copying it. When the iOS app asks for one of those tracks, the bridge fetches it from the upstream on your LAN and streams it onward over the same authenticated, fingerprint-pinned channel as your local files. Still bit-exact — the bytes pass through untouched, no transcoding — and nothing is duplicated to the bridge's disk, so the upstream stays the single copy. The bridge tags each upstream's tracks as their own sub-source under this bridge, so in the app you can see which library a track came from.
Setup. Open the admin console's new UPnP page. Servers discovered on your LAN show up automatically — pick one and add it — or, if SSDP multicast doesn't reach the bridge, click Add manually and paste the server's rootDesc.xml URL (copied from its own admin page). Each upstream gets a friendly name and a path prefix (so two upstreams can't collide); advanced options let you point at a non-default browse root or skip noise folders like System Volume Information. You can also configure it in bridge.yaml under upnpUpstream.servers.
Stable identity. The bridge keys each track on its reconstructed folder path (<prefix>/Artist/Album/NN - Title.ext), not the upstream's volatile streaming URL — so tracks keep their identity, and your playlists keep working, across the upstream server's restarts and DHCP address changes.
LAN-only, off by default, refused in public mode. Like the MediaServer, this is an opt-in LAN feature: SSDP multicast can't cross Tailscale, and the upstream's private (e.g. 192.168.x.x) byte URLs aren't reachable from a public VPS — so a public-mode bridge refuses to enable it. The upstream MediaServer has to sit on the same network as the bridge; your phone, though, reaches those tracks over any transport — LAN, Tailscale, or public — because the bridge is the one doing the proxying.
Your data
Cross-device playlist backup and opt-in listening history, stored only on your host.
Playlist backup (v0.1.5)
The bridge can back up the playlists you build in the iOS app so they survive a reinstall or a re-pair. Backups ride a durable per-device identity (a random token the iOS app keeps in its Keychain and sends as X-Device-Token), not the bearer token — so rotating or re-pairing a device doesn't orphan anything.
Shared across your devices, as of v0.1.6. Every paired device belongs to you, so the backups are pooled across all of them: any device can list and restore any playlist you've backed up, not just the one that created it. Switch phones, or restore onto a second device, and your playlists come along. (Earlier v0.1.5 bridges kept each device's backups to itself — the bridge advertises the newer behaviour with a playlistsCrossDevice flag so the app knows which model it's talking to.) The bridge still records which device last wrote each playlist, shown in the admin console.
A single playlist can mix tracks from this bridge with tracks that live on a different bridge or on local / SMB storage. Items belonging to another source are stored only as opaque references (an origin fingerprint plus a path the bridge never resolves, opens, or serves) — your iOS app re-resolves them locally on restore. Conflicting edits from two devices are reconciled last-writer-wins by the playlist's modification timestamp. The bridge advertises the base capability as the playlistBackup flag in GET /v1/health; older iOS builds that don't recognise the flags simply don't use them.
Listening history (v0.1.5, opt-in)
If you turn it on (per-bridge, in the iOS app — off by default), the app reports each play to the bridge: the track, when it started, how long you actually listened, the codec, and — where iOS exposes it — the audio output route and device (e.g. "CarPlay", "AirPods Max") and the sample rate delivered to the DAC. Events queue locally on the phone and drain in batches, so flights and dropped Wi-Fi don't lose data.
This is your data, for your eyes only. It is stored on the bridge host and surfaced in the admin console's Data page (codec / output histograms, a most-played list, a paginated event log). As of v0.1.6 the history is combined across all your paired devices — the admin console and a new authenticated feed both show the full picture, and any of your devices can read it back, so "most played" reflects everything you listen on rather than one phone. The bridge never sends it off the host on its own — no telemetry, no analytics, nothing sent to ars.md. The only way it leaves your machine is if you, the operator, explicitly export it (JSON / CSV) from that page. See privacy → per-device data for the exact fields stored.
Smart Mixes (v0.1.7, opt-in)
Smart Mixes are rotating, server-generated playlists built from what you actually listen to — the bridge's answer to "shuffle, but smarter." They're computed once a day on the bridge host from your listening history and audio analysis, served at GET /v1/smart-playlists, and never leave your machine. Enable them with smartPlaylists.enabled: true (off by default; the analysis-based families also need analysis.enabled).
Twelve families ship today:
- Heavy Rotation — what you've played most lately.
- On Repeat — the tracks you keep coming back to, with hysteresis so the list doesn't thrash.
- Recently Played and Forgotten Favorites — newest plays, and loved tracks you haven't touched in a while.
- Drive Mix — what you specifically play over CarPlay.
- Time of Day — your habit for the current hour (the app sends its local hour so the title is right).
- Finish Line — sized to your typical listening-session length.
- Artist Deep Cuts — unplayed tracks from artists you currently love, rotated weekly.
- Auto Mix — a harmonically-sequenced set that flows by musical key (Camelot-wheel adjacency).
- Daily Mix — mostly familiar, with a discovery seasoning.
- Wind Down and Lift Off — low-BPM-and-quiet, or high-BPM-and-loud, mood bands from the loudness + tempo analysis.
You can give any mix a custom cover image from the admin console. See privacy → Smart Mixes & analysis for the data posture.
Operations
Updates, snapshots, discovery, and the folder-first Library Inspector.
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:
- Quiet hours. Set a daily window (e.g. 02:00–06:00) during which the bridge will refuse to restart, even if an update is ready. Live playback is never interrupted.
- Compatibility floor. Each release ships a
release-meta.jsonsidecar declaring the minimum iOS app version it expects. The bridge refuses installs that would orphan a paired client on an older app, unless the operator forces it manually. - Apple notarization (macOS) and SHA-256 verification. The auto-installer verifies code signature, Team ID, and asset checksum before swapping the binary. Atomic rename on success; rollback on failure.
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:
host— the bridge's hostname (e.g.media-server.local)port— the HTTPS port (default7788)name— the operator-set library nameversion— the bridge's protocol version (currently1)ips— the bridge's routable LAN IP literals (added in v0.1.6), so the iOS app can connect even when the.localhostname doesn't resolve
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 the Library → Inspector sub-tab in the admin console.
- Folder navigation. Drill into a root, then into subfolders; each row carries the track count, total size, and how many variants are stored alongside. Long names truncate with an ellipsis (full name reveals on hover) so the right-side stats drawer stays readable.
- FTS5 search. A SQLite full-text index over title / artist / album returns flat results in milliseconds across libraries with tens of thousands of tracks. Search results render as a flat list while keeping the folder structure available a click away.
- Cursor pagination + virtualized scroll. The page streams 200 rows at a time and only paints the rows currently on screen, so scrolling stays smooth even at 50,000+ entries.
- Per-track actions. Inline-edit a track's display title; queue an Upscale variant; delete a generated variant; jump into the folder. The right-side stats drawer summarises the selected scope (root, folder, or single track) — track counts, source-size totals, projected upscaled size, and free-space headroom.
- Variants storage display. The header strip shows the absolute path where variants live (configurable via
upscale.variantsDir) and the free space available on that volume. - Artwork & context (v0.1.8). The same enrichment your phone sees is now visible admin-side: a tile's info panel carries album / artist artwork, biography, and description, and any release with a cached PDF booklet gets a booklet chip.
- Eligible-based coverage bars (v0.1.8). Each tile carries Upscaled and CarPlay-optimized bars that read as done-of-eligible (e.g.
62 / 62) plus an explicit "N need nothing," rather than a raw fraction of every track. An album already at the CarPlay floor reads as complete, so a genuine gap stands out instead of drowning in tracks that need nothing doing. - Harmonic filter & skip reasons (v0.1.7). An interactive Camelot wheel filters the view by musical key; per-track skip-reason badges say why a track isn't upscale-eligible — DSD (already beyond PCM), a lossy source, or an unknown format.
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 & internals
The admin console, metrics, logs, container config, and the wire protocol.
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.
Rebuilt in v0.1.8. The console is a ground-up, modern-minimal redesign with a light / dark / system theme toggle and a mobile-first layout — the earlier v0.1.4 phone-width breakpoint became a genuinely phone-first console, which matters because a phone is usually where you are when you notice something's off with the library. Server-Sent-Events tiles stream live updates in place of polling across the dashboard and settings surfaces.
Two surfaces are new this release. A Sources breakdown on the Dashboard (and the Library → Roots view) separates filesystem roots from UPnP upstreams; and a new Enrichment tab under Settings — mirrored by an Enrichment panel on the Dashboard — surfaces the active metadata source, live coverage telemetry, and a per-folder retry missing that re-enriches a stalled album without a full re-crawl. The Library Inspector also grows album and artist artwork, bios, booklet chips, and eligible-based coverage bars (detailed below). The full tab map:
| Tab | What it manages |
|---|---|
| Dashboard | Status tiles (original tracks, paired devices, uptime, DB size), the master-quality composition breakdown (PCM sampling density, DSD streams, codec layout), a Sources split (filesystem roots vs. UPnP upstreams), and the Enrichment panel |
| Library | Sub-tabs — Roots (add / remove folders, per-root variant counts, the transcoded-cache location), Inspector (the folder-first browser: coverage bars, booklet chips, artwork, per-tile upscale / optimize, full-text search), Jobs (pending / in-flight / failed upscale + analysis jobs with last-error strings) |
| Devices | Approve admin-approval pair requests, mint manual / QR tokens, rotate / revoke / rename per-device tokens |
| Listening | Opt-in listening history and playlist backups pooled across your paired devices — view, filter by device, export (JSON / CSV / M3U8) |
| Settings | Sub-tabs — General, Enrichment, Networking (Tailscale, HTTP/3, DLNA, public mode), Server endpoints, Updates (auto-install + quiet hours), Audio (upscale + analysis toggles), Backups (snapshot schedule + retention) |
A few things stay CLI-only by design: restore-from-snapshot (see Backups) and TLS cert rotation, 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:
| Platform | How 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 run | stdout / 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 / Unraid box, or on any headless Linux host. As of v0.1.8 it's a one-command container: the image bundles the sox / ffmpeg audio toolchain and self-initialises on first boot (serve --init-if-missing writes a default config if none exists), so docker run or docker compose up gets you a scanning bridge with no separate bridge init step. Most bridge.yaml knobs also 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.
| Env var | YAML key | Effect |
|---|---|---|
BRIDGE_LISTEN_ADDRESS | listenAddress | HTTPS listener (default :7788) |
BRIDGE_ADMIN_ADDRESS | adminAddress | Admin console listener (default 127.0.0.1:7789) — must stay loopback in loopback mode; only public mode allows a routable bind |
BRIDGE_DATA_DIR | dataDir | Where config, manifest, tokens, artwork, backups live |
BRIDGE_LIBRARY_NAME | libraryName | Display name shown in the iOS app's Sources list |
BRIDGE_LIBRARY_ROOTS | libraryRoots | Colon-separated list of music-library folders (e.g. /music:/audiobooks) |
BRIDGE_UPSCALE_ENABLED | upscale.enabled | Turn on offline PCM upscaling / CarPlay-optimize (uses the bundled toolchain). Off by default (v0.1.8) |
BRIDGE_ANALYSIS_ENABLED | analysis.enabled | Turn on waveform / loudness / key-tempo analysis. Off by default (v0.1.8) |
BRIDGE_DISABLE_HTTP3 | disableHttp3 | Set true to bypass the UDP (HTTP/3) listeners on a host whose firewall blocks inbound UDP |
TS_AUTHKEY | tailscale.authKey | Pre-baked Tailscale auth key for unattended tsnet mode boot |
For the full container recipe — the one-command quickstart, docker-compose examples, the bundled audio toolchain, in-container Tailscale (tsnet), and reverse-proxy notes for the admin console — see the Docker guide.
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.
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.
v0.1.5 is again additive against protocol v1: a new optional X-Device-Token request header carries a durable per-device identity; new authenticated endpoints GET /v1/playlists and GET/PUT/DELETE /v1/playlists/{id} (per-device playlist backup), POST /v1/history/batch (opt-in listening history), and GET /v1/renderers (DLNA renderer discovery); and the capability flags playlistBackup, playbackHistory, dlnaServer, and rendererDiscovery in the features array of GET /v1/health, each advertised only when the corresponding feature is wired so older iOS builds ignore what they don't recognise. The DLNA / UPnP MediaServer is a separate LAN-facing surface, not part of the iOS wire protocol. Protocol version stays at 1.
v0.1.6 stays additive against protocol v1: the bridge can ingest an upstream UPnP / DLNA MediaServer and advertises each one as a sub-source row in the app's Library view (via fields on GET /v1/health); its tracks ride the normal /v1/manifest + /v1/download path, proxied bit-exact, so no new client-facing endpoint is required. Playlist backup and listening history become user-wide — any of your paired devices can list, restore, update, or delete any playlist and read the combined history — advertised via the new playlistsCrossDevice and playbackHistoryRead flags, with a new cursor-paged GET /v1/history feed. The Bonjour TXT record gains an ips= field carrying the bridge's routable LAN addresses. Pre-v0.1.6 iOS app builds ignore every one of these; protocol version stays at 1.
v0.1.7 continues the additive line against protocol v1. New authenticated endpoints: GET /v1/smart-playlists and GET /v1/smart-playlist-image/{slug} for Smart Mixes; GET /v1/waveform and GET /v1/analysis/stats for audio analysis; and POST /v1/atlas-ingest, GET /v1/atlas-meta/{…}, and POST /v1/atlas-harvest/credential for Atlas metadata. Track gains an artworkVersion content marker so the app can refresh a changed cover in place. New features-array flags advertise each capability — smartPlaylists, waveform / loudness / keyTempo, and atlasEnrichment — so older iOS builds ignore what they don't recognise. Protocol version stays at 1.
v0.1.8 stays additive against protocol v1. PDF album booklets add a bookletTag content marker on Track (a cache-bust hint) and a bearer-authenticated GET /v1/booklet/{musicBrainzAlbumID} route, advertised by the booklets flag in the features array of GET /v1/health — present only when Atlas harvest is wired. Everything else this release is server-side hardening, not wire-visible. No re-pairing and no iOS app update are required — older builds ignore the new field, flag, and route. Protocol version stays at 1.