← 1-bit-bridge

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

The bridge isn't appearing in iOS "Discover on network". The iPhone shows an empty list or "No bridges found".

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.

  1. 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.
  2. Open the iPhone's SettingsPrivacy & SecurityLocal 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.
  3. 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.
  4. 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.
  5. As a fallback, use Method B: manual pair — generate a token in the admin console's Devices tab, scan the QR code from iOS.
Pairing seems stuck on iOS — the app shows "Waiting for approval…" and nothing happens.

The iOS app submitted the request, but the operator hasn't approved it yet on the admin console.

  1. 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.
  2. Click into the banner. The pending-request card shows the device name, iOS app version, and a 6-digit verification code.
  3. Confirm the code matches what's on the iPhone, then click Approve.
  4. 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.

iOS shows "Server unreachable" or "Pin mismatch" on a previously-working bridge.

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.

  1. On the iPhone, open 1-bit → Sources → swipe the bridge entry → Delete. Or rename the old bridge and add a fresh entry.
  2. 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.
  3. 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.

iOS app says "protocol mismatch" or "this server requires a newer iOS app".

Version skew between the iOS app and the bridge. Each release of either side declares the minimum compatible version of the other.

  1. Check the iOS app version (Settings → About) and the bridge version (admin console Dashboard, or ./bridge version).
  2. Compare against the compatibility table in the protocol spec.
  3. 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 scan is taking forever. Library tab shows "Walking · NN files" for what feels like an unreasonably long time.

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.

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).

Tracks show up in the iOS app but artwork is missing. Some albums show a cover, others a generic placeholder.

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.

  1. 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.
  2. Drop a folder.jpg (any reasonable resolution, ≤ 2000×2000 px) in the album's folder. The bridge picks it up on the next scan.
  3. 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

The wand icon doesn't appear in iOS.

Three preconditions must all be true:

  1. Bridge has upscale enabled. Admin console → Settings → Upscale toggle on, OR upscale.enabled: true in bridge.yaml.
  2. 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.
  3. The iOS app is 1.2 or newer. Older versions don't render the wand control.
Upscale jobs are stuck — the wand stays on the spinner forever.
  1. 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 sox missing, disk full, or an unreadable source file).
  2. Confirm sox is installed and on the bridge's $PATH: sox --version on the bridge host. If it's missing, install it (brew install sox, apt install sox libsox-fmt-flac, etc.).
  3. 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.variantsDir in bridge.yaml (or use ./bridge upscale move to migrate without re-generating). Each variant is roughly 3× the source size — a stalled job is sometimes ENOSPC in disguise.
  4. If many half-finished variants piled up after restarts or crashes, run ./bridge upscale --gc to reap orphans whose source files are gone or whose sidecars are unreferenced.

Tailscale

macOS only: "Re-mint cert" in admin console fails with 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:

  1. Install the non-sandboxed Tailscale CLI via Homebrew:
    brew install --cask tailscale
    This puts a Tailscale-CLI-only binary at /usr/local/bin/tailscale (or /opt/homebrew/bin/tailscale on Apple Silicon). It runs unsandboxed; tailscale cert can write its tempfile and rename normally.
  2. Switch to embedded mode: in bridge.yaml, set
    tailscale:
      mode: tsnet
    This skips the CLI shell-out entirely — the bridge runs its own tailnet node in-process. Authenticate once with ./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.

Bridge serves on the LAN but not over Tailscale. Local pairing works; pairing from a remote tailnet device fails to find the bridge.
  1. Confirm Tailscale is running on the bridge host: tailscale status. The host should appear in the list with a 100.x.x.x CGNAT IP.
  2. From the remote device, try https://<hostname>.tail-XXXX.ts.net:7788/v1/health in a browser. A 200 response with JSON means Magic-DNS is reachable; the iOS app should also find it.
  3. If you switched tailscale.mode recently, 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.
  4. iOS 26.4+ ATS rejects self-signed certs at the network layer for non-LAN hosts; *.ts.net hostnames present a real Let's Encrypt cert (issued by the bridge in tsnet mode, or by the system Tailscale CLI in cli mode), so this should "just work" — but if you see TLS errors, check that the cert isn't expired.

Updates

Auto-update didn't install — the admin console says "Update available" for days but nothing changes.
  1. Check that auto-install is actually enabled. Settings → Updates → toggle Auto-install on. (Many operators leave this off intentionally.)
  2. 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.
  3. Check the compatibility floor. The release's release-meta.json declares a minimum iOS app version. If any paired device reports an older app version (via X-Client-Version), the bridge defers the install rather than orphaning it. Update those clients first.
  4. 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

Lost the admin password from a 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).

Let's Encrypt cert provisioning is failing — admin console shows a TLS error or repeated ACME retries in the logs.
  1. 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).
  2. 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 refused or no record.
  3. 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: true and watch certs flow without burning quota. Switch back when the config is stable.
  4. 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 be 0700 and owned by the bridge user).

HTTP/3 (QUIC)

"failed to increase receive buffer size" warning in the logs at startup on Linux.

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.

HTTP/3 is unreachable — iOS app works fine over Wi-Fi but stalls on cellular.

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 crashes on launch. The service-manager status shows the process exited; bridge serve by hand prints an error and quits.
  1. Run ./bridge doctor. Most launch failures are environmental — port already in use, library path doesn't exist, data directory permissions wrong. Doctor flags them.
  2. Run ./bridge serve --config /path/to/bridge.yaml by hand and watch stderr — the bridge prints a clear error message before exiting.
  3. If the YAML config is corrupt, restore from a backup snapshot: ./bridge restore --snapshot <timestamp>. Snapshots live under backups/ in the data directory.
  4. If the SQLite manifest is corrupt (rare — only seen after a hard crash mid-write or a hardware fault), delete bridge.db from the data directory and let the bridge re-scan from scratch. Pairings and config survive.
Where do I find logs?
Per-platform commands for live-tailing the bridge service log
PlatformLive 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 runstdout / stderr of the ./bridge serve process
Dockerdocker 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.

Restoring from a backup snapshot.
  1. Stop the bridge via your service manager: macOS launchctl unload ~/Library/LaunchAgents/com.acoseac.1-bit-bridge.plist, Linux systemctl --user stop 1-bit-bridge, Windows sc.exe stop 1-bit-bridge.
  2. List available snapshots: ./bridge restore --list. They're dated YYYYMMDD-HHMMSS.
  3. Restore a specific snapshot: ./bridge restore --snapshot 20260508-104500.
  4. Start the bridge again via the same service manager: launchctl load ~/Library/LaunchAgents/com.acoseac.1-bit-bridge.plist on macOS, systemctl --user start 1-bit-bridge on Linux, sc.exe start 1-bit-bridge on 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:

Issue tracker: github.com/acoseac/1-bit-bridge/issues.