# Sync — your data across your devices

How Bring Your LM replicates to a Couchbase Edge Server you run, what travels and what stays on the device, why secrets don't ride along by default, what the status icon means, and how to stand a server up.

Status: Shipped

Sync is off until you turn it on. When you do, Bring Your LM replicates your
data to a **Couchbase Edge Server** that **you** run — there's no BYLM account
and no backend we operate. Your agents, conversations, notes, and settings then
follow you from one device to another, and each device keeps working offline and
catches up when it reconnects.

## Why it matters

You bring the model and the API keys, so the app's job is to hold your setup and
history without shipping it anywhere you didn't choose. Sync keeps that promise:
you point the app at a server on your own machine or network, replication runs
both ways continuously, and the things that only make sense on one device — or
that must never leave it — are deliberately held back.

## The one hard rule: Couchbase Lite 3.x

Bring Your LM stores everything in **Couchbase Lite 3.x** and replicates with
that version's protocol. The server you run has to speak the same protocol, and
this is the part that trips people up:

- **Couchbase Edge Server is currently a 1.x product built for the Couchbase
  Lite 3.x line.** There is no Edge Server release that targets the newer
  Couchbase Lite 4.x. So even though 4.x is the current Couchbase Lite version,
  BYLM stays on 3.x on purpose — moving the app's engine to 4.x would leave it
  with no server to sync against.
- **Pin your server to a build that speaks the 3.x protocol.** The reference
  deployment in this project pins the `couchbase-edge-server` 1.x image for
  exactly this reason. If a future Edge Server adds 4.x support, the app has to
  move first — don't get ahead of it on the server side.

If sync connects but immediately errors with a protocol or version complaint,
this mismatch is the first thing to check.

## What syncs, and what stays on the device

Everything replicates through a single set of collections. When sync is on, the
app pushes and pulls **all** of these:

- Your **agents**, and the **providers** they run on — but a provider's API key
  is **not** in the synced record (see the next section).
- Your **conversations** and every message in them, including **image
  attachments**.
- Your **tools** and your **notes / memories**.
- **Workspaces**, and app **settings** — including the sync connection itself.
- The **background job queue**, so a queued turn can be seen (and picked up)
  across devices.
- The **device registry**, so every device can put a name to the device ids
  stamped on jobs and messages.
- **Embedded-model catalogue entries** — the content-identified record that a
  given local model exists — so an assembled model shows up on your other
  devices (the actual GGUF file does not travel; see below).
- **Secret offers** and **pending mid-turn input requests**, so those round-trips
  survive an app restart or a hand-off to another device.
- **Saved secrets' names and revisions** — the list on **Settings › Secrets** and
  which providers and tools use each secret, never a secret's value.
- **Constants** from **Settings › Constants**, with each workspace's own text for
  them.
- **Unsent drafts**, and the records behind a conversation's compactions, spend
  and tool captures.

Some things are created on every device but **never** handed to the replicator,
because they're meaningless — or unsafe — anywhere but where they were written:

- **Where your GGUF model files sit on this device.** The embedded engine keeps
  a local cache mapping each model to its path on this machine. A path on one
  device says nothing on another, so it stays put. (It's rebuilt by re-scanning
  your models folder, so losing it costs nothing.)
- **The embedded engine's per-device runtime settings** — the compute backend
  you pinned (CPU / Vulkan / …), the decode-thread count and when a vision
  projector loads. A GPU one machine has is not the GPU another has, so these are
  held local.
- **The security records behind secret sharing** — the trust pins for other
  devices' sharing keys, the record of secret offers you've already handled, and
  which revision of each saved secret this device holds, which devices it sent
  one to, and the hosts this device approved it for.
  These are local by design, not just convenience: if they synced, the same
  attacker who could tamper with the channel could rewrite the very records that
  guard it.

## Secrets never ride the sync connection

Your provider and fetch-tool **API keys live in your device's OS keystore**, not
in the app's database — so they are simply not among the things that replicate.
A synced provider record carries its address and options and only which saved
secret it uses; the secret's value stays on the device you typed it into. By
default, then, **turning on sync shares no secrets at all**, and you re-enter a
secret once on each device.

### Sharing a secret on purpose

When you **do** want a secret on another device without retyping it, BYLM has a
deliberate, per-secret path — not a blanket "sync my secrets" switch:

- You pick a **specific secret** — **Share** on its page under **Settings ›
  Secrets** — and a **specific target device** to send it to.
- The secret is **sealed to that device's public key** before it leaves —
  encrypted so only that one device can open it — and then carried inside the
  synced **key-offers** collection. Because it's ciphertext sealed to one recipient, the
  server relays it without ever being able to read it.
- The target device has to be **verified first** (you confirm its fingerprint),
  which is what stops a secret being sent to an impostor.
- On the other device you **accept** the offer; the secret is unsealed straight
  into that device's keystore, and the offer is retired.

So secrets are private by default and shared only one by one, device by device,
end-to-end encrypted — never dropped into the general sync stream in the clear.

### When a secret is rotated

Rotating a saved secret on **Settings › Secrets** raises its revision, and every
offer names the revision it carries — so an offer sealed before a rotation can't
install the old value after it; accepting it is refused as outdated. When you
rotate, you choose which verified devices get the new value now; the ones this
device sent the secret to before are ticked. Every other device sees through sync
that its copy is outdated, keeps using it unless you chose to stop the old value
at once, and can ask for the new one — an ask only a device holding the new
revision can answer. Receiving an update is still an accept you make, labelled
as a new revision of that secret.

### Asking for a secret you don't have

Sharing starts on the device that _has_ the secret, which is the wrong end when
you are sitting in front of the device that needs one. So you can also ask:

- A secret's page under **Settings › Secrets** offers **Ask my other devices for
  this secret** while this device lacks it or holds an older revision, and has
  **Ask other devices** in its menu. A provider or tool missing its secret links
  there with **Open saved secret**.
- The ask names **no device** — nothing in BYLM publishes which of your devices
  holds which secret, and you may not remember either. It goes to all of them,
  and each one shows it only if it actually has that secret stored. A device that
  cannot help never mentions it.
- On a device that can, the ask appears under **Settings → Devices** with
  **approve** or **decline**. Approving simply shares the secret the normal way —
  fingerprint check included if that device isn't verified yet — so an ask can
  never move a secret anywhere sharing couldn't.
- The ask carries **no value**: it names a secret, nothing more. What comes back
  is an ordinary sealed offer, and it turns up on the secret's page you asked
  from: accept it there and every provider and tool using that secret has it. It
  also waits under **Settings → Devices** with
  everything else, and accepting it either way clears the ask from your other
  devices — along with any second copy another of them sent for the same secret.
- Declining hides the ask on **that** device only, leaving your other devices
  free to answer it. You can withdraw an ask you raised from
  **Settings → Devices**, and one nobody answers expires by itself.

Because nothing replicates while an app is closed, this is a "go and open the
other device" flow: your ask reaches a device the next time you open BYLM there,
and the badge on Settings points you at it.

### Managing your devices

**Settings → Devices** lists every device in your registry — the names that show
up in a message's per-device metrics. From here you can rename a device, delete a
stale record for one you no longer use, and see each device's trust badge and key
fingerprint. Verifying a device means comparing that fingerprint across the two
screens; if a device's keys later change, it turns into a loud "Keys changed"
badge rather than staying invisible, and a device that changes its name has to be
re-verified before it can send or receive secrets. Incoming secret offers also
land here, each with an explicit **accept** (which verifies the sender first) or
**decline** — as do asks from your other devices for secrets this one holds, and
the secrets this device is still waiting for. A badge points you to whatever is
waiting, so nothing sits unnoticed.

Each device also shows the BYLM version it last started with. A device running
an older version than the one you're holding is marked for updating, and when
another device runs a newer one, this device's own entry says so — your devices
read each other's data best when they're on the same version. While another
device runs a newer version, the **Settings** menu item and the **Devices** row
carry a badge, so a desktop that missed the update your phone installed by
itself says so without you opening the list. A device shows "version unknown"
until it has started once on a version that reports it.

## Reading the sync status icon

Whenever sync is on, a small status icon rides in the top bar (and in the
navigation drawer) so you always have an at-a-glance read on the connection.
Tap it to jump straight to the sync settings. When sync is off, the icon isn't
shown at all.

Here is every state it can show:

![The eleven sync status states, each with its icon, label, and meaning](/images/sync_states.png)

- **Up to date** and **Syncing…** are the healthy pair, in your theme's primary
  colour: **Up to date** (a ticked cloud) means connected with nothing left to
  move; **Syncing…** (spinning arrows) means changes are flowing right now.
- **Connecting…**, **Waiting for network…**, and **Reconnecting…** are the calm
  in-between states, in your theme's accent colour. **Waiting for network** shows
  while the server's hostname isn't resolving yet, or while the device has no
  network at all; **Reconnecting** is a brief blip the app retries quietly — it
  deliberately waits about 45 seconds before it will show a blip as a real error,
  so a dropped wifi link, a server restart, or the moment after you unlock your
  phone doesn't flash red.
- **Starting…**, **Idle**, and **Stopped** are muted, low-key states: the
  replicator is coming up, enabled-but-not-yet-running, or stopped.
- **Offline**, **Setup error**, and **Sync error** are the ones that want
  attention, in an error colour. **Offline** means the network has been gone
  long enough to be worth saying so (it will still reconnect on its own);
  **Setup error** means the connection settings couldn't be applied — a
  malformed URL is the usual cause; **Sync error** means the
  connection has kept failing past the grace window. For these, open the sync
  settings, fix what's wrong, and use **Retry now** rather than waiting out the
  automatic backoff.

## What sync does while your phone sleeps

An Android phone spends most of its day locked, and a locked phone cuts the
network off from apps that aren't in front of you. A replicator left running
into that would spend the night retrying a connection it cannot make, once every
half minute, burning battery and filling the app's diagnostics buffer with
hundreds of identical "can't resolve the server" errors — which then crowd out
the records a bug report is actually for.

So Bring Your LM parks it instead. If the connection fails while the app is in
the background, sync stops until you come back, and reconnects the moment you
open the app again. A connection that is _healthy_ when you switch away is left
alone, so a reply that lands while you're in another app still reaches your other
devices.

That is also why coming back to the app doesn't greet you with a red icon. The
grace window before a problem is called an error counts only the time you could
actually _see_ it: a phone that spent the night locked was not watching sync
fail, so the attempt made when you unlock it gets the full window to succeed —
long enough that a normal reconnect finishes quietly.

The flip side is that the red icon means something specific. It says sync has
been unable to reach your server for about forty-five seconds _with the app in
front of you_, which is the question you actually wanted answered: not "is
something wrong somewhere" but "is this still just coming up, or is it really
broken?" Leave the app open and a genuine outage turns red and stays red.

## Turning sync on in the app

Open **Settings**, then **Sync**, and fill in the connection:

- **Enable sync** — the master switch. Off by default; nothing connects until you
  turn it on.
- **Endpoint URL** — `ws://` or `wss://`, then the host and port, then a slash
  and the **database name** configured on the server. For example
  `ws://192.168.1.10:59840/byllm`, or `wss://cbe.example.com/byllm` behind a
  TLS proxy. Always include the port — leaving it off falls back to 80/443, not
  the server's listener. Couchbase Edge Server's default port is `59840`. The
  path is the database name, not a collection.
- **Anonymous access** — leave off to sign in with a username and password;
  turn on only if your server is configured to allow anonymous clients.
- **Username** and **Password** — shown when anonymous access is off.
- **Apply changes** saves the form and restarts the replicator. The endpoint is
  checked as you type, and sync can't be switched on until it is a `ws://` or
  `wss://` URL naming a host — the field says what is wrong. Leaving the page
  with an endpoint or login typed but not applied asks before discarding it.
  Below it, a
  **Status** line shows the live activity, a running count of documents moved
  this session, and any error in full; **Retry now** restarts immediately.

Use `wss://` (TLS) for anything beyond your own trusted LAN — the endpoint
carries all your synced data.

## Standing up the server

You supply the server; the app is only the client. For a copy-and-run example —
a Dockerfile, a `docker run` command, and the config and users files — see
[Running a Couchbase Edge Server](/docs/features/couchbase-edge-server). The essentials
are the same however you run it:

- **Run Couchbase Edge Server as a container**, listening on `59840`. The
  reference deployment builds the official `couchbase-edge-server` 1.x package
  into a small image and exposes that port. Persist the server's data directory
  on a volume so it survives restarts.
- **Define one database** in the server config — call it `byllm` to match the
  reference — with `create` on, client writes on, and client sync set to
  `bidirectional`.
- **List the collections to serve, explicitly**, each under the default scope:
  `_default.memories`, `_default.tools`, `_default.api_providers`,
  `_default.bots`, `_default.conversations`, `_default.conversation_messages`,
  `_default.image_blobs`, `_default.image_blob_chunks`, `_default.settings`,
  `_default.workspaces`, `_default.jobs`, `_default.devices`,
  `_default.key_offers`, `_default.key_requests`, `_default.secret_entries`,
  `_default.constant_entries`, `_default.embedded_model_bundles`,
  `_default.elicitations`, `_default.compaction_events`,
  `_default.spend_events`, `_default.tool_captures`, and
  `_default.conversation_drafts`. This list must cover every collection the app
  replicates — a collection the app pushes but the server doesn't serve will
  404. (Don't add the server's own `_default._default`; the app deliberately
  doesn't sync it.)
- **Set up access.** Either define users with `bcrypt`-hashed passwords and the
  `replicate` role in the server's users file — then fill the matching username
  and password into the app — or turn on anonymous users for an open server and
  switch **Anonymous access** on in the app. Prefer real users for anything
  reachable off your own machine.
- **Front it with TLS** for remote access — a reverse proxy (the reference setup
  uses Traefik) terminates HTTPS on a hostname and forwards to `59840`, so the
  app connects with `wss://<host>/byllm`.

If you manage servers with Ansible, this project's companion role automates all
of the above: you provide the admin user's password (from a vault), the public
hostname the proxy answers on, and the database-to-collections map shown above,
and it renders the config and users files and runs the container for you.

Whenever you add a new collection to the app in a future version, remember the
server's collection list has to grow to match, or that new data won't sync.

## What it doesn't do yet

Sync is a single endpoint with a single user — there's no per-collection or
per-device filtering, no channels, and no selective sync: enable it and the full
collection set above replicates. And it only talks to a Couchbase Edge Server (or
a compatible Sync Gateway) speaking the Couchbase Lite 3.x protocol — see the
version rule above.

More in [Features](/docs/features): [Providers — bring your own LLM](/docs/features/providers) · [On-device models — run fully offline](/docs/features/embedded-models) · [Agents — the thing you build](/docs/features/agents) · [Tools — giving an agent more than the model knows](/docs/features/tools) · [Workspaces — keep your work in scopes](/docs/features/workspaces) · [Conversations — how a turn runs](/docs/features/conversations) · [Attachments — images and files in a message](/docs/features/attachments) · [Context management — what the model still sees](/docs/features/context-management) · [Background turns — replies that keep running](/docs/features/background-turns) · [Local-first & private by design](/docs/features/local-first) · [Sharing — move agents, tools and providers as a file](/docs/features/sharing) · [Marketplace — find and publish shared agents, tools and providers](/docs/features/marketplace) · [Running a Couchbase Edge Server](/docs/features/couchbase-edge-server) · [Spell checking — catch typos while you write](/docs/features/spell-check)

---

[Bring Your LM](/) · [Docs](/docs/features) · [Marketplace](/marketplace)

[Articles](/articles) · [Releases](/releases) · [Bundle Format](/marketplace/bundle-format) · [Marketplace Terms](/terms) · [App Privacy](/privacy) · [Website Privacy](/website-privacy) · [Open Source](/open-source) · [llms.txt](/llms.txt)
