Troubleshooting
Symptom → fix. If your problem isn't here, run ./bridge doctor for an environment punch-list, then open an issue on GitHub with the doctor output and your service-manager logs.
Pairing
Bonjour discovery requires both devices on the same Wi-Fi (or Wi-Fi + bridged Ethernet) and Local Network permission granted to the 1-bit app on iOS.
- Confirm the iPhone is on the same Wi-Fi SSID as the bridge host. A "guest" subnet that isolates clients from each other will block discovery.
- Open the iPhone's Settings → Privacy & Security → Local Network → make sure 1-bit is enabled. iOS prompts for this on first launch; if you tapped Don't Allow, you have to flip it back here.
- On the bridge host, confirm the bridge process is running and Bonjour is enabled (default). The admin console's Dashboard shows a Bonjour status line; it should read advertising.
- If you have multiple network interfaces (a VPN, virtual NICs from VMware / Parallels, Tailscale), the bridge advertises on each one — but a misbehaving virtual interface can swallow the announcement. Try toggling the suspect interface off, then run Refresh in the iOS Discover sheet.
- As a fallback, use Method B: manual pair — generate a token in the admin console's Devices tab, scan the QR code from iOS.
The iOS app submitted the request, but the operator hasn't approved it yet on the admin console.
- Open the admin console in any browser on the bridge host (
http://127.0.0.1:7789/). A yellow Pending pair request banner appears at the top of every page when one is waiting. - Click into the banner. The pending-request card shows the device name, iOS app version, and a 6-digit verification code.
- Confirm the code matches what's on the iPhone, then click Approve.
- iOS picks up the approval within a few seconds and finishes pairing.
Pairing requests time out after 10 minutes. If you missed the window, the iOS app will tell you — just tap Try again.
The bridge's TLS certificate fingerprint changed since pairing. This is expected after a cert rotation — by design, the iOS app refuses to silently trust a new cert. The fix is to re-pair the affected device.
- On the iPhone, open 1-bit → Sources → swipe the bridge entry → Delete. Or rename the old bridge and add a fresh entry.
- On the admin console, generate a new pair token (Devices → Pair new device) or wait for the iPhone to submit a new request via Discover.
- Pair as new (see setup step 6).
If you didn't rotate the cert intentionally, something else moved. Check the admin console's Dashboard for the current fingerprint, and check whether the bridge was reinstalled or the data directory was wiped. The cert lives at tls/server.crt in the data directory; if it's missing the bridge generates a new one on startup.
Version skew between the iOS app and the bridge. Each release of either side declares the minimum compatible version of the other.
- Check the iOS app version (Settings → About) and the bridge version (admin console Dashboard, or
./bridge version). - Compare against the compatibility table in the protocol spec.
- Update whichever side is older. The iOS app updates through the App Store; the bridge updates via the admin console's Updates tab or by replacing the binary manually from the releases page.
Library scanning
First scans are slow by design — every track gets MusicBrainz / CAA / Deezer enrichment, and the lookups are rate-limited to be polite to those services.
- 2,000 tracks ≈ 30 seconds.
- 10,000 tracks ≈ 2 minutes.
- 50,000 tracks ≈ 5–15 minutes.
Subsequent re-scans only enrich what's new — typically 30–60 seconds even on a 50k library.
The iOS app can play tracks while the scan is still running — it's not blocking. If you want to verify progress is happening, watch the Library tab's count fields tick upward, or tail the bridge log (see Where do I find logs? below).
Artwork enrichment is best-effort. The bridge tries embedded artwork (APIC frames in ID3v2 tags, covr atoms in MP4), then a sibling folder.jpg / cover.jpg, then MusicBrainz Cover Art Archive, then iTunes Search as a fallback. A miss on all four leaves the placeholder.
- Check the album's tags — is the MusicBrainz Release ID present? If not, CAA can't look up the cover. Re-tag with MusicBrainz Picard for best-quality matches.
- Drop a
folder.jpg(any reasonable resolution, ≤ 2000×2000 px) in the album's folder. The bridge picks it up on the next scan. - For artist photos, the Deezer fallback only fires when the artist has a MusicBrainz ID. Tag the album with the MBID via Picard, re-scan.
Upscale
Three preconditions must all be true:
- Bridge has upscale enabled. Admin console → Settings → Upscale toggle on, OR
upscale.enabled: trueinbridge.yaml. - The track is upscale-eligible. The wand only shows on FLAC / ALAC / WAV / AIFF / MP3 / AAC sources. DSD tracks already exceed PCM — there's nothing to upscale.
- The iOS app is 1.2 or newer. Older versions don't render the wand control.
- Open the admin console's Jobs tab — it lists every pending / in-flight / failed upscale with its submission time and last-error string. A failure stack here usually points straight at the cause (most often
soxmissing, disk full, or an unreadable source file). - Confirm
soxis installed and on the bridge's$PATH:sox --versionon the bridge host. If it's missing, install it (brew install sox,apt install sox libsox-fmt-flac, etc.). - Check free space on the volume where variants live. The Library Inspector header shows the absolute path and current free space; if you've outgrown the default location, set
upscale.variantsDirinbridge.yaml(or use./bridge upscale moveto migrate without re-generating). Each variant is roughly 3× the source size — a stalled job is sometimesENOSPCin disguise. - If many half-finished variants piled up after restarts or crashes, run
./bridge upscale --gcto reap orphans whose source files are gone or whose sidecars are unreferenced.
Tailscale
tailscale cert: exit status 1 (… open /tmp/.../tailscale.crt.tmpNNNN: operation not permitted).
This is a sandboxing collision between the macOS Tailscale GUI app and the bridge's CLI shell-out. The Tailscale.app binary at /Applications/Tailscale.app/Contents/MacOS/Tailscale inherits a sandbox profile that blocks writes to arbitrary paths under /tmp — exactly where tailscale cert wants to write its atomic-rename tempfile.
Two fixes, pick one:
- Install the non-sandboxed Tailscale CLI via Homebrew:
This puts a Tailscale-CLI-only binary atbrew install --cask tailscale/usr/local/bin/tailscale(or/opt/homebrew/bin/tailscaleon Apple Silicon). It runs unsandboxed;tailscale certcan write its tempfile and rename normally. - Switch to embedded mode: in
bridge.yaml, set
This skips the CLI shell-out entirely — the bridge runs its own tailnet node in-process. Authenticate once withtailscale: mode: tsnet./bridge tsnet auth, then restart the bridge.
Either fix is good. tsnet is the more durable option because it removes the dependency on the host CLI altogether — recommended for new installs.
- Confirm Tailscale is running on the bridge host:
tailscale status. The host should appear in the list with a100.x.x.xCGNAT IP. - From the remote device, try
https://<hostname>.tail-XXXX.ts.net:7788/v1/healthin a browser. A 200 response with JSON means Magic-DNS is reachable; the iOS app should also find it. - If you switched
tailscale.moderecently, restart the bridge — mode changes don't take effect until restart. Check the admin console's Dashboard "Tailscale" tile for any error message from the active mode. - iOS 26.4+ ATS rejects self-signed certs at the network layer for non-LAN hosts;
*.ts.nethostnames present a real Let's Encrypt cert (issued by the bridge intsnetmode, or by the system Tailscale CLI inclimode), so this should "just work" — but if you see TLS errors, check that the cert isn't expired.
Updates
- Check that auto-install is actually enabled. Settings → Updates → toggle Auto-install on. (Many operators leave this off intentionally.)
- Check the quiet-hours window. If the candidate fell into quiet hours every day at the polling moment, it'll wait. Either widen the window or click Install now manually.
- Check the compatibility floor. The release's
release-meta.jsondeclares a minimum iOS app version. If any paired device reports an older app version (viaX-Client-Version), the bridge defers the install rather than orphaning it. Update those clients first. - If none of the above apply, look at the bridge log for an updater error. Manual install always works as a fallback: download the archive, extract, replace the binary, restart.
Public mode
bridge init --public install.
The init-time password is printed once and never persisted. Rotate it from a shell on the bridge host:
./bridge admin reset-password
The command prompts for a new password (twice for confirmation) and rewrites <dataDir>/adminauth.json with the new bcrypt hash. Active login sessions are invalidated immediately. No bridge restart required.
bridge update refuses to install with binary path not writable by this user (try sudo bridge update).
This is a public-mode VPS layout where the daemon user owns /usr/local/bin/bridge but the parent directory /usr/local/bin/ is root-owned. The bridge can't atomic-rename a new binary into a directory it can't write — so it surfaces the constraint up-front rather than corrupting the install. Run the update under sudo:
sudo bridge update -config <path/to/bridge.yaml> -yes
If you use a FUSE-mounted library that's only visible to the daemon user (a typical rclone --vfs-cache-mode full setup), root running this command would have historically failed at config-load time with libraryRoots[...]: permission denied. As of v0.1.5 the update path tolerates an inaccessible library root — accessibility is checked at bridge serve startup, not during the update flow, so sudo bridge update works regardless of who can read the library. Post-install, restart the bridge however your platform manages the service (sudo systemctl restart 1-bit-bridge for the canonical systemd layout).
- Confirm the public DNS A/AAAA record for the bridge's domain resolves to the host's public IP. ACME challenge methods require that the LE servers can reach the host on either port 80 (HTTP-01) or the configured TLS port (TLS-ALPN-01).
- Check the host firewall / cloud security group lets inbound traffic on whichever challenge port your config selected. The bridge log surfaces the LE-side error verbatim — typically
connection refusedorno record. - Let's Encrypt enforces aggressive per-domain rate limits. If you've been bouncing the bridge while debugging, switch to the LE staging directory by setting
autocert.useStaging: trueand watch certs flow without burning quota. Switch back when the config is stable. - The cert + account key live in
<dataDir>/acme/. If that directory is somehow corrupt or unwritable, the bridge can't cache renewals; check permissions (the directory should be0700and owned by the bridge user).
HTTP/3 (QUIC)
The quic-go library asks the kernel for a 7 MiB UDP receive + send buffer at bind time. Linux's default net.core.rmem_max / wmem_max caps that at 2 MiB and logs a one-line warning. The bridge keeps running — but HTTP/3 throughput over high-RTT cellular links benefits from the headroom.
Raise the caps via /etc/sysctl.d/999-bridge-quic.conf (the 999- prefix wins over Ubuntu's cloud-image 99-cloudimg-udp.conf drop-in, which would otherwise clobber the setting):
net.core.rmem_max = 7340032
net.core.wmem_max = 7340032
Apply with sudo sysctl --system, verify with sysctl net.core.rmem_max, then systemctl restart 1-bit-bridge so the UDP socket re-reads the cap.
Some carrier middleboxes silently drop UDP on port 443 or strip QUIC. The bridge advertises HTTP/3 alongside HTTP/2 — iOS should fall back to TCP automatically, but a partial failure (handshake succeeds, mid-stream UDP packets dropped) can hang. To rule it out, disable HTTP/3 on the bridge:
disableHttp3: true
in bridge.yaml (or export BRIDGE_DISABLE_HTTP3=true) and restart. If cellular playback recovers, the carrier path is at fault; leave HTTP/3 off for that deployment until the path improves.
Other
bridge serve by hand prints an error and quits.
- Run
./bridge doctor. Most launch failures are environmental — port already in use, library path doesn't exist, data directory permissions wrong. Doctor flags them. - Run
./bridge serve --config /path/to/bridge.yamlby hand and watch stderr — the bridge prints a clear error message before exiting. - If the YAML config is corrupt, restore from a backup snapshot:
./bridge restore --snapshot <timestamp>. Snapshots live underbackups/in the data directory. - If the SQLite manifest is corrupt (rare — only seen after a hard crash mid-write or a hardware fault), delete
bridge.dbfrom the data directory and let the bridge re-scan from scratch. Pairings and config survive.
| Platform | Live tail |
|---|---|
| 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 (open in Notepad++ or tail with PowerShell Get-Content -Wait) |
| Manual run | stdout / stderr of the ./bridge serve process |
| Docker | docker logs -f <container> |
The admin console also has a Logs tab that tails the last 500 lines in the browser. For deeper history, use the platform tools above. Bearer tokens are never logged; library-relative file paths appear only in error lines. Client IP addresses are not logged in loopback-mode deployments — in public-mode deployments the admin login handler logs the client IP on failed logins / rate-limit refusals so the operator can investigate brute-force attempts. See privacy → logs.
- Stop the bridge via your service manager: macOS
launchctl unload ~/Library/LaunchAgents/com.acoseac.1-bit-bridge.plist, Linuxsystemctl --user stop 1-bit-bridge, Windowssc.exe stop 1-bit-bridge. - List available snapshots:
./bridge restore --list. They're datedYYYYMMDD-HHMMSS. - Restore a specific snapshot:
./bridge restore --snapshot 20260508-104500. - Start the bridge again via the same service manager:
launchctl load ~/Library/LaunchAgents/com.acoseac.1-bit-bridge.pliston macOS,systemctl --user start 1-bit-bridgeon Linux,sc.exe start 1-bit-bridgeon Windows. The data directory now matches the snapshot — config, manifest, paired devices.
Snapshots restore the bridge's data directory only — your music files are never touched. iOS pairings continue to work because the token database is part of the snapshot.
Still stuck?
Open an issue with:
- Output of
./bridge doctor - Output of
./bridge version - The relevant section of the bridge log (with bearer tokens already redacted by the bridge — safe to share verbatim)
- Your platform and how you installed (release archive / Docker / built from source)
Issue tracker: github.com/acoseac/1-bit-bridge/issues.