1-bit

Bridge · Privacy

Bridge privacy policy

The 1-bit bridge is a companion server you install on your own machine and pair with the 1-bit iOS player. It does not phone home. It collects no analytics. It has no user account. This page explains what it stores on your machine, what it sends out, and what it doesn't.

Last updated: 29 September 2026

This policy covers the bridge, not the iOS app. Looking for the 1-bit app's policy? That's on the app privacy page. The bridge's source is public; if anything here doesn't match what the binary does, that's a bug — please open an issue.

At a glance

  • Runs on your machine — no ars.md server, no telemetry, no analytics, no account. (A 1-bit Cloud library is this same software run by us; that deployment is covered by the app privacy policy, not this page.)
  • Stores config, a library manifest, hashed device tokens, artwork, and (opt-in) per-device backups on your host — all detailed below.
  • Sends out only: the artist and album names from your files to public metadata APIs (MusicBrainz, Cover Art Archive, Deezer — if enrichment is on), a GitHub release check, in public mode Let's Encrypt for TLS, and — each only if you turn it on — your library's artist/release IDs to the Atlas premium-metadata service your app points it at, or an acoustic fingerprint of a track to AcoustID.
  • Playlist backup, favorites, and (opt-in) listening history stay on your host, shared only across your own paired devices. DLNA — serving to renderers or pulling in a UPnP server — is off by default and refused in public mode; while serving is on, its listener needs no login and answers on every network interface unless you name one (details).
  • The source is public — every claim here can be checked against the code.

Regulatory note

Scope. This page describes a bridge you run yourself. From app v2.0, 1-bit Cloud runs the same bridge software on our infrastructure as a hosted library: that deployment is ours to operate, it has an account (Sign in with Apple), and what it holds and sends is disclosed in the 1-bit Cloud section of the app privacy policy, which governs it. Everything below is about your own bridge.

By default, the 1-bit bridge has no ars.md-operated backend: in normal operation it runs no server we control, transmits nothing to us, and has no account system. There is one opt-in exception — the optional Atlas premium-metadata harvest described under Atlas metadata below. If you turn it on and leave it pointed at our reference instance (atlas.ars.md), the bridge sends your library's artist and release identifiers there to fetch bios and descriptions. That instance is self-hostable, so you can keep even that traffic on your own infrastructure.

The bridge does process some data that can identify a person — a paired device's name, an opt-in listening history, the durable device token, and IP-bearing log lines (all detailed below) — but every byte of it stays on the machine you run the bridge on, under your control.

The bridge's own traffic carries no personal data to ars.md — the optional Atlas lookup sends only public catalog identifiers (artist and release IDs), never your files, device tokens, or listening history — so for your library and usage there is no data-controller relationship with us. The one thing the developer-operated Atlas server does retain, when you enable Atlas, is a minimal anti-abuse record (your install's attestation key and the IP it was last seen from) — not linked to your identity, never used for tracking, and deletable on revocation. That is disclosed in full under Atlas metadata → "What the Atlas service stores" below; for that narrow data the developer is the controller, and a GDPR (EU), Swiss FADP (Switzerland), or CCPA (California) deletion request can be directed to the contact address at the foot of this page.

If you run a bridge that other people use, you are the controller of the data on your own host, and these disclosures are written to help you understand exactly what that is.

What the bridge is and where it runs

The bridge is a Go binary that runs as a background service on your machine — under launchd on macOS, systemd on Linux, or as a Windows Service. It indexes a music library you point it at, and serves it to a paired 1-bit iOS device over HTTPS. Everything happens between your machine and your phone; no ars.md-controlled server is involved.

Deployment modes. The bridge runs in one of two modes set by deployment.mode in bridge.yaml.

Loopback mode (the default) binds the admin console to 127.0.0.1 — anyone on the host machine implicitly has filesystem access to the data directory, so the admin console runs without authentication. Only the console is confined to the machine. The iOS-facing API listens on listenAddress, :7788 by default, which names no interface, so it answers on every network interface the machine has (your LAN, and any VPN or Tailscale interface) unless you put one address there. It is served over HTTPS and needs a paired device's token on every request, apart from the health check and the pairing handshake the app makes before it has one.

Public mode (operator opt-in) is for bridges intentionally exposed to the public internet: the admin console binds a routable interface, gains a single-user password login, and the iOS-facing TLS certificate switches to a Let's Encrypt cert provisioned via the ACME protocol.

Both modes preserve the same paired-token-authenticated wire protocol toward the iOS app.

Data stored on your machine

By default under ~/Library/Application Support/1-bit-bridge/ (macOS), ~/.config/1-bit-bridge/ (Linux), or %LOCALAPPDATA%\1-bit-bridge\ (Windows):

  • bridge.yaml — configuration: library paths, listen addresses, scan interval, backup schedule, update-check toggle.
  • bridge.db — SQLite manifest of your library: file paths, sizes, mtimes, parsed tag metadata (title, artist, album, year, sample rate, MusicBrainz IDs when enrichment finds them). Used to serve the manifest to the iOS app in a single request instead of re-scanning every connect. When the optional upscale feature is enabled, an additional track_variants table records the on-disk path, format, and source signature of each generated upscaled audio file so they survive restarts and re-pairs. The same database also holds a few small tables for per-device features, described in detail under Per-device data below: device_registrations (a durable per-device identity used to anchor backups), playlists / playlist_items (optional playlist backups), and playback_history (optional listening history). These are never sent off your machine by the bridge on its own — only synced back to your own paired devices, or exported by you from the admin console — and are visible to you in the admin console; the playlist and history tables are user-wide, readable by any of your paired devices rather than only the one that wrote each row. When the optional UPnP upstream feature is enabled, an additional upnp_track_routing table records the identity and current stream URL of each upstream MediaServer on your LAN so the bridge can re-fetch its tracks — that is your own LAN gear's address, and it never leaves your host. It also holds a track_analysis table (locally-computed waveform / loudness / key / tempo — see Smart Mixes & analysis), the definitions of your generated Smart Mixes, and, when the optional Atlas feature is on, a metadata cache (artist_atlas / release_atlas — artist bios and album descriptions). It additionally holds your favorites backup (favorite_tracks / favorite_albums), duplicate-group bookkeeping recorded against each track row (which copies the bridge considers duplicates of each other, and which one it serves — see duplicate suppression), and, when acoustic fingerprinting is enabled, a per-track note of whether a fingerprint match was accepted, so the result can be audited or undone. The track_analysis table also gained the locally-measured track-quality scalars and frequency spectrum described under audio analysis.
  • tokens.json — paired-device tokens. Raw tokens are never stored; only their SHA-256 hashes, plus a device name, creation date, last-used timestamp, and last-reported client version. File permissions 0600 (owner-only).
  • server.crt / server.key — self-signed TLS keypair generated on first run. The certificate's SHA-256 fingerprint is what the iOS app pins.
  • artwork/ — JPEG cache for album covers and artist photos fetched during enrichment. High-resolution covers fetched via the optional Atlas path land here too.
  • waveforms/ (optional) — peak-envelope sidecar files generated by the local audio analysis for the iOS scrubber. Created only when analysis.enabled: true. Their names mirror your library's folders and file names.
  • covers/smartmix/ (optional) — custom cover images you upload for a Smart Mix from the admin console.
  • booklets/ (optional) — PDF album booklets fetched via the optional Atlas path, cached so the iOS app can open them without re-fetching. Created only when Atlas is enabled and a release actually has one; a booklets table in bridge.db tracks which releases have been checked so the bridge doesn't ask twice.
  • transcoded/ (optional; upscale.variantsDir can name another directory) — generated .flac renditions of your tracks. Created only when the operator opts into upscale.enabled: true, and then only by a rendition the bridge is asked for: one a paired iOS device requests (POST /v1/upscale), a batch started from the admin console, the optional auto-optimize sweep, or the bridge upscale / optimize / render commands. Their names mirror your library's folders and file names. The sidecars contain audio bytes derived from your own source files via a local sox resample — they never leave the bridge except over the same paired-token-authenticated channel as the originals. Removed when the source track is deleted, or in bulk via bridge upscale --gc.
  • adminauth.json (public-mode deployments only) — single-user admin credential store. Only a bcrypt hash of the password is persisted; the plaintext password is shown once at bridge init --public time and never written to disk. The console's login sessions are persisted in the same file, so a restart does not sign you out: for each session, the SHA-256 of its cookie token (never the token itself) and the times it was issued and last used. Signing out deletes that session's record. File mode 0600.
  • acme/ (public-mode deployments only) — autocert cache directory used by Go's golang.org/x/crypto/acme/autocert. Holds the Let's Encrypt account key, the issued certificate, and renewal state. The operator-supplied contact email is registered with Let's Encrypt as part of the ACME account. Directory mode 0700.
  • adminauth-ticket-<hash>.json (public-mode deployments only — beside adminauth.json) — a login link minted from the command line (bridge admin login-link) is redeemed by the serving bridge, a separate process, so its record has to cross the disk. From bridge version 0.2.1 each live ticket has a 0600 file of its own, named for the ticket's SHA-256 and holding only the account it signs in and when it expires. A link lives 60 seconds by default, at most 10 minutes when it is minted with a longer --ttl. Its file is deleted when the ticket is presented; an expired one that was never presented is deleted the next time a link is minted. Earlier versions kept the SHA-256 of every live ticket in one adminauth-tickets.json file, which 0.2.1 deletes the first time it mints a link. The ticket itself is never written to disk or logged.
  • backups/<timestamp>/ — optional dated snapshots of the above. Operator-tunable in bridge.yaml; can be set to 0 to disable.

Logs

The bridge writes structured logs to your platform's service manager — journalctl / log show / %PROGRAMDATA%\1-bit-bridge\bridge.log on Windows.

Bearer tokens, full HTTP request bodies, and contents of MusicBrainz / Deezer / Cover Art Archive lookup queries are not logged.

Client IP addresses are not logged for the iOS-facing /v1/* API in loopback-mode deployments. That includes the web server's own lines about a connection that failed, such as a TLS handshake a client broke off: from bridge version 0.2.1 they leave the client's address out, and earlier versions logged it. There are two deliberate exceptions.

(1) In public-mode deployments, the admin login handler logs the client IP alongside the attempted username when a login fails or hits the per-(IP, username) rate-limit threshold — these lines are necessary so the operator can investigate brute-force attempts and tune their reverse-proxy / firewall accordingly.

(2) When the optional DLNA / UPnP MediaServer is enabled (default off; refused in public mode), the addresses of the devices that talk to its listener appear in the bridge's log and in an in-memory buffer. In the log, at INFO, each browse or search request carries the address and port it came from, the requesting app's User-Agent, and what it asked for — a folder, or the search text. At WARN, an event subscription is logged with the address it came from and the callback address it named, when that callback is another private address, or this machine or a link-local address that is not the subscriber's own; an ordinary control point calls back on its own address and logs nothing. Each pair of addresses is logged once per run of the bridge, up to 64 pairs of each kind. If you set the BRIDGE_DLNA_TRACE environment variable, every file request is also logged at INFO with its address. The in-memory buffer records every request the listener answers and keeps the last 1,000 (time, method, path, a few of the request's headers, status, size, duration and address) unless dlna.telemetryEnabled is false; nothing writes it to disk or shows it in the console, and a restart clears it. On a home network these are the addresses of your own devices, but they are whatever reaches the DLNA port: DLNA below says what answers where.

If you ship bridge logs to a third-party log aggregator, treat these lines as PII.

Absolute filesystem paths appear in the log in these places. The startup banner names your library roots. Some scanner and tag-reader warning and error lines name the file they concern by its absolute path, such as a file the tag reader could not parse or one whose embedded artwork it skipped. The bridge names its own files by their absolute paths when it cannot read or write them: the credential and token stores, the login-ticket files, the TLS certificate and the SQLite database. From bridge version 0.2.1 a login ticket's file is named for the ticket's SHA-256, so the line logged when a link cannot be redeemed for such a reason can carry that hash (never the ticket itself); earlier versions named the one shared ticket file. The bridge also names rendition and waveform sidecars, whose names mirror your library's folders and files, when a background check adopts, moves or removes one; the optional integrity.orphanSidecarSweepIntervalSec sweep (default disabled) logs each orphan sidecar it deletes. None of these reaches the iOS app: from bridge version 0.2.1, an error message the app can receive, such as a failed upscale's, names files library-relative.

Exporting a log. The admin console can download the bridge's log — filtered to a level and a time window — and can package it into a bug-report bundle together with the version, platform, configuration shape, and preflight results. Neither is sent anywhere by the bridge: both are downloads you initiate, and it's your choice whether to share them. Because a bundle exists to be sent somewhere, it is redacted by default — absolute filesystem paths are replaced throughout, in every section rather than only in the log lines. The plain log export is unredacted by default, since you're reading your own machine's paths on your own loopback console; both defaults can be overridden per download. Before attaching either to a public issue, read it — a log's warning and error lines can still carry the library-relative paths described below.

Library-relative file paths (e.g. music/Diana Krall/Live in Paris/01.flac) appear in warning and error lines — when a scan, upscale, or read operation fails or runs into trouble on a specific file, the path is included so the operator has something to act on — and in a few informational lines, such as a track the metadata lookup could not match. These paths don't carry your home directory or username, but they do contain artist / album / track names by typical folder layout. If you ship bridge logs to a third-party log aggregator, treat them with the same care as the library metadata itself.

Smart Mixes & audio analysis

Two optional features, both off by default and both computed entirely on your machine — neither makes any network call of its own.

Audio analysis (analysis.enabled) decodes your own tracks locally with sox (and ffmpeg / ffprobe as a fallback for AAC / m4a) to compute a peak waveform, R128 loudness, and an estimated musical key and tempo. The results live in the track_analysis table in bridge.db, with the waveforms written as sidecar files under <dataDir>/waveforms/. The decode and the signal processing both run on the bridge host; nothing about your audio is uploaded anywhere.

The same decode measures more, and all of it stays local too: a whole-track frequency spectrum (which is how the app can tell you a "hi-res" file was upsampled from a CD), a true-peak level, a DR score, and — for FLAC — a verification of the audio against the checksum the encoder stored inside the file. These are stored in the same track_analysis table and served only to your own paired devices. See file provenance for what the numbers mean and where the bridge deliberately declines to answer.

Smart Mixes (smartPlaylists.enabled) are rotating, server-generated playlists — Heavy Rotation, Drive Mix, Wind Down, an Auto Mix that sequences by musical key, a time-of-day mix, and others — rebuilt once a day from data that is already on your host: your opt-in listening history and the audio-analysis results above. They are served only to your own paired devices, over the same bearer-token-authenticated channel as everything else. The single input the iOS app contributes is the current local hour (an optional ?local_hour= query) so the time-of-day mix can be titled correctly; that value is used on-host and not stored. Any custom cover image you upload for a mix is kept under <dataDir>/covers/smartmix/.

Acoustic fingerprinting (optional)

Some files carry tags no lookup can use — album: CD 01, artist: An Unknown Artist. For those, the bridge can identify the recording from the audio instead. It is off by default and stays off unless you set fingerprint.enabled: true and supply your own free AcoustID application key — the bridge ships no built-in key, so there is no way for this to run without a deliberate act by you.

What is computed locally. The fpcalc tool (Chromaprint) reads the audio and produces a compact acoustic fingerprint: a compressed summary of how the sound's frequency content changes over time. It is not the audio, and it is not reversible — you cannot reconstruct or listen to the recording from it.

What is sent, and to whom. When a track needs identifying, the bridge makes one HTTPS request to the AcoustID API (https://api.acoustid.org/v2/lookup) carrying exactly three things: the fingerprint, the track's duration in seconds, and your own AcoustID application key. It sends no filename, no file path, no tags, no library inventory, no device token, and no listening history — and the audio itself never leaves your machine. AcoustID is an independent, open-data project run by the MusicBrainz community; see their web service documentation. Requests are paced to their published rate limit, and results are cached so the same file is never looked up twice.

What comes back, and what the bridge does with it. AcoustID answers with candidate recording identifiers. The bridge deliberately will not write an album identity or a cover from that answer — a fingerprint identifies a recording, and the same recording appears on many releases. It uses the recovered artist name to re-run the ordinary metadata search instead, and if your file's own artist tag contradicts the answer, your tag wins and the match is discarded. Each accepted match is recorded against the track in bridge.db so you can audit or reverse it later.

Atlas premium metadata (optional)

Atlas is an optional metadata service that supplies richer-than-public album art (high-resolution covers, up to ~1080 px), artist biographies, album descriptions, and PDF album booklets, the liner notes that shipped with the record. It is off by default (atlas.enabled). When it is on, the metadata reaches the bridge by one of two paths — and only the second involves the bridge making any outbound connection of its own.

  1. App-mediated ingest (the default path). Your iOS app — which holds the Atlas read credential, not the bridge — fetches the metadata from Atlas and hands it to the bridge over the authenticated app↔bridge channel (POST /v1/atlas-ingest). The bridge caches it (in the artist_atlas / release_atlas tables, and the artwork/ directory for covers) and serves it back to all of your paired devices. On this path the bridge makes no outbound Atlas connection. Where the app itself fetches that metadata from is covered by the app's privacy policy.
  2. Bridge-direct bulk harvest (opt-in, atlas.harvestEnabled). For libraries large enough that per-album fetching is impractical, you can let the bridge harvest in bulk. Your iOS app provisions a limited-scope, device-attested credential to the bridge (POST /v1/atlas-harvest/credential), which expires on its own and which the app renews while the harvest stays on; the bridge then connects directly to the Atlas server the app named — by default our reference instance at atlas.ars.md, or a self-hosted instance you point it at — and submits your library's distinct artist and release MusicBrainz identifiers to receive bios and descriptions in return. The same path also asks whether a release has a PDF booklet and, when one exists, downloads it — still keyed only by that release identifier. Those identifiers are public catalog IDs; the bridge sends no file paths, no device tokens, and no listening history — though taken together the identifiers do describe which artists and albums your library holds, which is why the harvest stays off unless you enable it. Turning the harvest off in the app stops it renewing the credential and has it ask the bridge to delete the copy it holds (DELETE /v1/atlas-harvest/credential, from bridge version 0.2.1); a bridge older than that, or one the app cannot reach at that moment, keeps the copy until it expires, so to be sure the harvest stops at once, turn atlas.harvestEnabled off on the bridge. The base URL must be HTTPS. This is the one exception to the bridge's otherwise no-outbound-to-ars.md posture, it stays off unless you enable it, and self-hosting Atlas keeps even this traffic on your own infrastructure.

Lyrics (opt-in, atlas.lyricsEnabled, default off). Rides the bulk-harvest credential, so it is inert unless the harvest above is on. For a track that carries no lyrics of its own, the bridge sends the Atlas instance the harvest is pointed at the release identifier plus the track's title, number and duration — what it takes to resolve one recording — and stores the plain or synced lyrics document Atlas relays from LRCLIB in a track_lyrics table in bridge.db. No file paths and no listening history go with it. Lyrics found in your own files or .lrc / .txt sidecars are read locally and never sent anywhere.

Whichever path supplies it, the fetched metadata is cached locally and re-fetched no more often than the configured time-to-live (atlas.metaTtlHours, default 30 days). The outbound case is summarised under what the bridge sends to the internet.

What the Atlas service stores (server side)

Atlas is the one developer-operated server in the 1-bit system. Unlike the rest of this page — which describes software running on your machine — this part describes data held on infrastructure the developer runs, so it is set out here in full. It exists only to stop abuse and enforce rate limits; it is not linked to your identity, never used for tracking, and never assembled into a record of what you look up.

  • An attestation record per install. The first time an app install (or a bridge doing the bulk harvest) attests, Atlas stores that install's Apple App Attest key — its key identifier (a SHA-256 of the public key), the public key, a signature counter, the attestation receipt, and first/last-seen timestamps. This is a persistent per-install identifier, but by Apple's design it is not tied to your Apple ID, your name, or any advertising identifier, and reinstalling produces a fresh, unrelated record.
  • A last-seen IP and app version per credential. On each authenticated request Atlas overwrites the IP address, user agent, and app version most recently seen for that credential. An IP address is personal data, so it is disclosed here — but it is kept as a most-recent value, overwritten on the next request, not accumulated into a history or a location trail.
  • No per-lookup log. Atlas keeps no record of which artists or albums any install looked up. The MusicBrainz identifiers it receives are used to serve from — and fill — a shared public-metadata cache keyed by identifier ("what Atlas has fetched from public sources"), not a per-user access log. There is no request / lookup / browsing-history table.

All of the above is deleted when the device's access is revoked or expires. Because Atlas is self-hostable, you can point the app and bridge at your own instance and keep even this anti-abuse data on your own infrastructure.

App Store privacy label. Because Atlas can be enabled by the user, 1-bit's App Store privacy details (from app v1.7) declare an Identifiers item — the App Attest device key and the IP address — used only for App Functionality and fraud prevention, not linked to your identity and not used for tracking. No "Usage Data" or "Browsing History" is declared, because no per-lookup record exists. With Atlas off (the default) nothing is collected at all.

What the bridge sends to your iOS app

Every /v1/* endpoint requires the bearer token issued at pairing, served over TLS pinned to the server's certificate fingerprint. The one exception is /v1/health, which is unauthenticated so the iOS app can probe it during pairing. In loopback-mode deployments the admin web console at http://127.0.0.1:7789/ binds loopback-only — it is not reachable from the network. In public-mode deployments the admin console binds a routable interface and is gated by the single-user password login described above.

The iOS-facing API is served over HTTP/2 (TCP) and, when enabled, HTTP/3 (QUIC over UDP) on the same port — the client picks one transport per request. No additional data is exposed by the HTTP/3 path; the privacy posture is identical to HTTP/2.

On the wire to the iOS app: directory listings, file metadata (size, mtime), byte ranges of music files, the full manifest, cached artwork, and GET /v1/diagnostics (authenticated, counter-only — uptime, queue depths, MusicBrainz cache hit ratio, lock-wait quantiles; no log text, no paths, no IPs). No transcoding — the bridge delivers bytes exactly as they sit on your disk.

The admin listener additionally exposes a Prometheus-format /metrics endpoint for local observability tools. It is bound by the same gate as the rest of the admin console: loopback-only in loopback mode, session-authenticated in public mode. Like /v1/diagnostics, the metrics are integer counters and histograms only.

DLNA / UPnP MediaServer (optional)

The bridge can optionally present your library to DLNA / UPnP renderers on your local network — devices like the Chord 2Go, or generic UPnP players (mconnect, BubbleUPnP, etc.). It is off by default; you enable it with dlna.enabled: true in bridge.yaml or the admin console's Settings toggle.

UPnP has no authentication mechanism — renderers cannot present a bearer token — so when DLNA is enabled, anything that can open a connection to the DLNA port can browse the library and stream audio without a token. This is inherent to the DLNA protocol: the only boundary is the network the bridge machine is on and the interfaces its listener answers on, not a password. Be aware of this before enabling it on an untrusted network.

What it opens. With DLNA off, the default, the bridge opens none of the following. With it on (a loopback-mode bridge only; see below) it opens:

  • A TCP listener on dlna.listenAddress, :7790 by default. That address names no interface, so the listener answers on every network interface of the machine — loopback, your LAN, and any VPN or Tailscale interface the machine has — and to every client: the bridge filters neither. To bind one interface only, put its address in dlna.listenAddress (for example 192.168.1.20:7790).
  • UDP port 1900 (SSDP), IPv4, on every interface. It answers a search that reaches it on any of the machine's addresses, and naming an address in dlna.listenAddress does not narrow it.

So who can reach the bridge's DLNA port is decided by your network, not by the bridge: your LAN, any VPN or tailnet the machine is on, and the internet only if the machine has a public address or the port is forwarded to it, in each case unless a firewall says otherwise. Two structural safeguards apply:

  • Announced on the LAN only. The bridge announces itself, and joins the SSDP multicast group (239.255.255.250:1900) to hear renderers' searches, only on private / link-local interfaces, and never on Tailscale or a public one — multicast doesn't traverse a point-to-point tunnel. That decides where the bridge advertises itself, not who can connect to the two ports above.
  • Refused in public mode. A bridge running in deployment.mode: public will not start the DLNA MediaServer at all, regardless of the dlna.enabled setting — an internet-facing host must never expose an unauthenticated library browser.

Over DLNA the bridge exposes the same kind of data it serves the iOS app — folder/track listings, file metadata (title, artist, album, duration, sample rate), and the audio bytes themselves (bit-exact, no transcoding). The same token-less listener also serves album covers (/dlna/artwork/{key}, keyed by the app's own cover key, never a library path), so a renderer can show the cover beside the track. It does not expose bearer tokens, admin credentials, or any of the per-device data described below. When DLNA is enabled the addresses of the devices that connect appear in the bridge log and an in-memory buffer, as described under Logs.

A related opt-in, dlna.discovery.enabled: true (default off), lets the bridge scan the LAN for renderers (SSDP M-SEARCH) so the iOS app can offer them as playback targets via the authenticated GET /v1/renderers endpoint. The discovered list is renderer names + addresses on your own network; nothing is sent off-host. The scan opens no fixed port: it sends its searches from an ephemeral UDP port and listens there for the replies.

UPnP upstream ingest (optional, LAN-only). A companion opt-in (upnpUpstream.enabled, default off) points the other way: instead of serving your library to a renderer, the bridge ingests another UPnP / DLNA MediaServer on your LAN — a Chord 2Go's own SD card, a NAS media server — and proxies its audio to your phone. It records that server's identity and current stream URLs in bridge.db (your own LAN gear's address) so it can re-fetch tracks; it proxies the bytes bit-exact and never copies them to disk. Its searches are multicast and stay on the LAN — SSDP multicast can't cross Tailscale — and the upstream's private byte URLs aren't reachable from a public VPS, so a public-mode bridge refuses to enable it. It opens no fixed port either, only an ephemeral UDP port for the replies to its searches. Nothing about the upstream is sent off your host.

Per-device data: device identity, playlist backup, favorites, listening history

The bridge can hold a small amount of per-device state to back up your playlists and (optionally) your listening history. All of it lives in bridge.db on your machine and is visible to you in the admin console's Data page. None of it is ever sent to ars.md or any third party. The playlist backups and listening history are user-wide — pooled across all of your own paired devices, so you can restore a playlist onto a new phone and see your combined history — but never shared beyond the devices you've paired.

  • Device identity (device_registrations). So your backups survive a re-pair or app reinstall, the iOS app generates a durable random per-device token (kept in the iOS Keychain, not iCloud-synced) and sends it as an X-Device-Token header. The bridge records that token, the auth token it is currently bound to, the device name, and first/last-seen timestamps. The device token is the recovery key for that device's backups; it is stored as the bridge receives it.
  • Playlist backup (playlists / playlist_items). When the bridge advertises the playlistBackup capability, the iOS app can back up its playlists here so they survive a reinstall. Backups ride that durable device token rather than the bearer token, and are pooled across all of your paired devices so any of them can restore any playlist (the bridge records which device last wrote each one). A playlist may reference tracks that live on a different bridge or on local/SMB storage — those items are stored only as opaque references (an origin fingerprint + an origin path); the bridge never resolves, opens, or serves another source's files. Your iOS app re-resolves those references locally when you restore.
  • Favorites (favorite_tracks / favorite_albums). The tracks and albums you heart in the iOS app back up here the same way playlists do, as a single document your app replaces whole on each change, readable by any device you've paired. As with playlists, a favorite that lives on a different source — another bridge, SMB, or on-device storage — is stored only as an opaque reference (an origin fingerprint plus a path the bridge never resolves, opens, or serves). Favorites stored here also feed a Smart Mix built from them. Nothing about them leaves your machine.
  • Listening history (playback_history). This is opt-in — you turn it on per-bridge in the iOS app, and it is off by default. When enabled, the iOS app reports each play to POST /v1/history/batch: the track's library-relative path, when it started, how long you listened, the codec, and (where the OS exposes it) the audio output route and device name (e.g. "CarPlay", "AirPods Max") and the sample rate delivered to the DAC. This data stays on your machine for your own use; it is combined across all your paired devices and can be read back by any of them. It is surfaced in the admin console (histograms, a most-played list, and a paginated event log) and is not sent off-host by the bridge unless you explicitly export it (see the Data-page note below).

Admin exports. The console lets you, the operator, view and export the playlist backups and listening history above — a playlist as JSON, CSV or an .m3u8 file from Browse, the history as JSON or CSV from Listening history, and everything at once as one JSON bundle from /api/export. The admin console is loopback-only in the default deployment and password-gated in public mode, so these exports are operator-facing only. Note that the .m3u8 export deliberately writes the absolute filesystem path of each local track, so the playlist is directly playable by a desktop player (e.g. VLC) on the bridge host — anyone with whom you share that file receives those paths. JSON and CSV exports use library-relative paths.

What the bridge sends to the internet

If you enable metadata enrichment, the bridge looks up missing album / artist information from public APIs. For each lookup, the bridge sends the artist and album names read from your files (or a release identifier) only. No user identifier, no library inventory, no IP address you control — just the standard HTTPS request that any browser would make. Whether those names identify you depends on the files: a commercial album's tags are shared by millions, but an unreleased mix or a personal recording carries tags unique to you, and those go out too — so this page describes the traffic rather than calling it anonymous. Artist photos looked up by the iOS app for a plain SMB share go to Deezer from the phone, not through the bridge; the app privacy policy covers that.

  • MusicBrainz — https://musicbrainz.org/ws/2/release/?query=…. Sends artist name + album title. Receives release IDs. MusicBrainz data is CC0.
  • Cover Art Archive — https://coverartarchive.org/release/<mbid>/front-…. Sends a release ID in the URL path; receives a JPEG. Cached locally.
  • Deezer public API — https://api.deezer.com/search/artist?q=…. Sends artist name; receives artist photo URL (then fetched from Deezer's CDN). Deezer privacy.

All four are rate-limited server-side and can be disabled by removing the corresponding section from bridge.yaml (or by running with --no-enrich).

Acoustic fingerprinting (opt-in, off by default). Only when you enable fingerprint.enabled and supply your own AcoustID application key does the bridge contact https://api.acoustid.org/v2/lookup. Each request carries an acoustic fingerprint of one track, that track's duration, and your key — no filename, no path, no tags, no library inventory, no device token, and never the audio itself. Full detail under acoustic fingerprinting above.

Atlas premium metadata (opt-in, off by default). Only when you enable the bridge-direct bulk harvest (atlas.harvestEnabled) and your iOS app has provisioned a credential does the bridge contact the Atlas service — by default our reference instance at atlas.ars.md, or a self-hosted HTTPS instance you configure. It sends your library's distinct artist and release MusicBrainz IDs (public catalog identifiers, which together describe which artists and albums the library holds) and receives bios, descriptions, cover references and PDF album booklets in return; it sends no file paths, no device tokens, and no listening history. The default app-mediated path makes no bridge-side outbound request at all. Full detail under Atlas metadata above.

Let's Encrypt (public-mode deployments only). When autocert.enabled: true, the bridge fetches and renews a TLS certificate from Let's Encrypt via the ACME v2 protocol (https://acme-v02.api.letsencrypt.org/). The handshake sends the configured public domain name, the operator-supplied contact email (which Let's Encrypt registers against the ACME account), and the standard challenge response (HTTP-01 or TLS-ALPN-01). No library data, device tokens, or usage statistics are sent. The cert + account key are cached locally under <dataDir>/acme/; renewal happens in-process roughly 30 days before expiry. Operators on loopback-only deployments do not need this path and should leave autocert.enabled at its default false.

Update check

The bridge polls https://api.github.com/repos/acoseac/1-bit-bridge/releases/latest every six hours to discover new releases. The poll is unauthenticated, sends no device identifier, no library statistics, no usage data — just a standard GET that any unauthenticated GitHub API client would make.

As with any standard HTTPS request, GitHub will see the source IP address of the bridge machine. We do not see it, and the request carries no other identifier tying it to you. update.checkIntervalHours in bridge.yaml tunes the cadence (values below 1 hour are clamped at 1 hour; 0 resolves to the 6-hour default rather than disabling the poll). To stop the bridge from making any outbound requests at all, stop the service or block its egress at your firewall — operators running over Tor, a VPN, or Tailscale tend to do the latter for everything except their tunnel anyway.

Admin authentication (public-mode deployments only)

When deployment.mode: public is set, the admin console requires a username and password. bridge init --public generates a 16-character random password (~92 bits of entropy, drawn from an ambiguity-free alphabet) and prints it once to the operator's terminal; only its bcrypt hash is persisted to adminauth.json. The password can be rotated later with bridge admin reset-password.

Successful logins set a session cookie carrying a random 256-bit token (base64url-encoded). The server stores only the SHA-256 hash of that token, with the times the session was issued and last used, in adminauth.json beside the password hash; the token itself is never stored on the bridge. Sessions are idle-expired after 24 hours and hard-expired after 7 days. A bridge restart neither ends a session nor resets either clock, and signing out deletes the session's record.

To resist brute-force attempts, the login handler rate-limits per (client IP, username) tuple. Failed attempts and rate-limit refusals are logged with the client IP (see Logs above for the full disclosure). The IP-bucket state lives in memory only and is discarded on restart.

Pairing & tokens

Pairing happens entirely between your machine and the iOS app. The admin console generates a 256-bit random token, shows it to you once (as a bridge://pair?… URL or QR code), and persists only its SHA-256 hash. The raw token never touches disk on the server. From bridge version 0.2.1 the QR code and link also carry a one-time code. An iOS app that supports it trades it for a fresh token over the connection pinned to the bridge's certificate, and the token the link carried stops working at that moment, so a copy of the link (a screenshot, a forwarded message) is no use once your device has paired. An older app, or a link without a code, pairs with the link's token itself, which stays valid until you rotate or revoke that device. The bridge holds only the SHA-256 of each code, in memory, for at most 10 minutes, and a restart forgets them. From the admin console you can rename, rotate, or revoke any paired device at any time; revoked tokens are hard-deleted from tokens.json on the next persist and fail the very next request — there is no tombstone, no audit log, and no orphan retention.

Admin-approval pairing. When an iOS device on your network taps "Discover" and selects your bridge, it submits a pairing request that lands on the admin console for you to approve or reject. While the request is pending, the bridge holds an in-memory record containing: the device-chosen display name, the iOS app's self-reported version, the device's IP address as observed on your network (display-only, surfaced in the admin console so you can spot LAN-spam patterns), a hash of the device's poll-secret, a 6-digit verification code (shown on the device for you to confirm), and the bridge's TLS certificate fingerprint at request time (so a mid-flow cert rotation aborts the pairing rather than silently completing against a stale pin). Approval mints the long-lived token described above and discards the pending-request record; rejection or timeout discards it without minting. None of this state survives a bridge restart, and the client's TLS fingerprint is captured by the iOS app for client-side pinning — the bridge never persists it.

What we do not do

  • No telemetry, no analytics, no usage reporting.
  • No crash reporting to ars.md infrastructure.
  • No advertising, no third-party trackers, no ad identifiers.
  • No transcoding (the bridge is bit-exact by mission).
  • No user account.
  • By default, no ars.md-operated server the bridge connects to — the optional Atlas premium-metadata harvest (off by default, self-hostable) is the sole exception, detailed above.
  • No data sold, shared, or disclosed to third parties.
  • No knowingly-collected data from children.

Your rights

ars.md holds none of your data, so there is nothing for us to access, correct, or delete. The data the bridge stores is entirely under your control on your own host: to remove all of it, stop the service and delete the data directory listed above.

Security

Vulnerabilities go through the process documented in SECURITY.md — GitHub's private security advisory flow, or support@1-bit.app with [1-bit-bridge-security] in the subject.

Changes to this policy

If this policy changes, the updated version replaces this page with a new "Last updated" date. The bridge's source is public, so the behaviour this page describes can always be checked against the code at github.com/acoseac/1-bit-bridge.

Contact

support@1-bit.app