Syncing your chats
Your conversation history lives on the device where the chat happened. Syncing copies it to a folder or storage bucket you own, which every device you sign in on reads and writes.
Nothing goes to our servers, and whatever holds the files can't read them: they're encrypted on your device before they're written.
Turn it on in Settings → Storage → Sync your chats.
What syncing does
- Carries conversations and messages, including their attachments and generated images, between every device signed in to your account.
- Runs continuously. A change reaches the target within about a second, and each device checks for other devices' changes every minute and whenever you come back to the tab.
- Merges rather than overwrites. When two devices change different things in the same conversation, such as a rename on one and a new message on the other, both changes are kept.
What it leaves out:
| Not synced | Why |
|---|---|
| Settings | Theme, session timeout, and routing preferences are per-device on purpose. |
| Payment records | Your tickets are on-chain facts, not chat content. |
| Search index | Rebuilt locally on each device from the chats it has. |
Syncing is not a backup: delete a conversation on one device and it's deleted on all of them. For a copy that doesn't change, use Back up chats in the same settings pane.
Pick where it syncs
| A folder on this computer | A storage bucket | |
|---|---|---|
| Where it works | Desktop Chrome or Edge, and Brave once a flag is on | Every browser, phones included |
| What you provide | A folder something already syncs — Dropbox, Google Drive, iCloud Drive, OneDrive, Syncthing | An S3-compatible bucket you own |
| Setup | Point at the folder | Create a bucket, add a CORS rule, paste two keys — in the provider's console, or with one terminal command |
| Other companies involved | Whoever runs the sync app | Whoever runs the bucket — or nobody, if you host it |
To switch between them, stop syncing, then set it up again. Files already saved stay where they are.
Start setup
In Settings → Storage → Sync your chats, a card shows where syncing stands. Until something is connected it reads Your chats stay on this device. Press Set up sync → to open setup in a dialog.
The same setup lives at zerosignal.ai/sync, which is also where a scanned code lands.
The first screen, How do you want to sync?, offers three choices:
- I already sync on another device: join a bucket another device set up. See Add another device.
- A folder on this computer: see Sync through a folder.
- My own storage bucket: works in every browser, including phones. See Sync through a bucket.
How private is this? under the choices opens a two-line summary of what the storage can see.
On a phone the folder choice isn't shown, and the screen says Easiest on a computer — set it up there, then scan the code here. Set up the bucket on a computer, then use Add another device to bring the phone in.
Sync through a folder
Choose the folder option
Pick A folder on this computer. It works in desktop Chrome and Edge.
In a browser that can't do it, the choice is greyed out and says why. On Safari and Firefox, use a bucket instead. On Brave, turn on the flag in Folder sync in Brave first.
Pick the folder
Press Choose folder. Your browser asks which folder, and for permission to write to it. Choose a folder your sync app already watches, such as the Dropbox folder, an iCloud Drive folder, or a Syncthing share.
The app then shows the folder's name with a Change button, and a line saying whether it recognises the folder, such as Looks like Dropbox — your other computers should pick these up. A folder it doesn't recognise still works. The app just can't tell whether anything syncs it: If this folder isn't synced, these chats stay on this computer.
Press Done.
Repeat on your other computers
The last screen says to set up your other computers the same way, using the same folder. Your sync app carries the encrypted files between them, and the app on each computer finds the others' chats there.
Browsers drop folder permission when they restart, so you'll occasionally see Reconnect on the Storage card. Press it to pick up where it left off.
Folder sync in Brave
Brave turns off the browser feature folder sync needs, the File System Access API, by default. Until it's on, the folder choice is greyed out and suggests Chrome or Edge. To turn it on:
- Copy
brave://flags/#file-system-access-apiand paste it into Brave's address bar. Brave blocks links to its own settings pages, so it has to be pasted or typed. - Set File System Access API to Enabled.
- Relaunch Brave, then open setup again.
Brave keeps it off to reduce fingerprinting. With it on, a site still can't reach anything until you pick a folder for it, and this app only touches the folder you choose.
Sync through a bucket
The app generates the bucket name and fills in everything its provider preset knows. You do the parts a browser isn't allowed to do: create the bucket, give it a CORS rule, and make a key. Nothing is saved until the checks pass.
Pick a provider
The providers are in three groups:
- Someone hosts it: Filebase, Cloudflare R2, Amazon S3.
- You host it: Garage, SeaweedFS.
- Something else: Other S3-compatible, for anything else that speaks S3, including MinIO.
Filebase is tagged Fewest steps. Under Set by the preset the app shows the endpoint, region and addressing the provider uses. You can't edit them here. Only Other S3-compatible has an Advanced section, where you set the endpoint, the region, a folder inside the bucket, and path-style addressing.
See the provider reference for what's specific to each one.
Create the bucket
A toggle at the top switches between In the console and In a terminal. Each shows its own checklist for your provider. The tick boxes are only for your own tracking. Nothing depends on them.
The app suggests a bucket name, zs- followed by random characters, with a
Copy button. On Filebase, R2 and Amazon S3, the rows link to the console
pages where each step happens. Garage and SeaweedFS have no console, so their
rows are the commands to run.
In the console, the list covers creating the bucket, making a key, and applying the CORS rule:
- Filebase: create the bucket with bucket type S3, not IPFS. On the bucket's CORS tab, choose Public Read-Write and save. Read-Only passes the first checks and then fails on write. Or paste the rule yourself opens the rule as JSON, if you'd rather apply it by hand.
- Every other provider: the list shows the CORS rule to paste, with a Restrict it to option that narrows it to the app's own address.
On Filebase, Amazon S3 and SeaweedFS, a note under the list says to keep versioning off: a version history keeps the copy of a chat you asked to delete.
In a terminal, the screen shows the setup command, already filled in with your provider and bucket name:
npx @txnlab/zs-sync-setup --provider filebase --bucket zs-…
The command applies the CORS rule, checks versioning, runs its own checks, and prints a link. Opening that link connects this device, and you can skip the rest of the setup. On most providers the command also creates the bucket. Two are different:
- Filebase: create the bucket in the console first, as type S3. A bucket created through the API is always an IPFS one.
- Cloudflare R2: the token the app keeps (Object Read & Write) can't create a bucket or change its CORS rule. Create the bucket in the dashboard, or run the command with an Admin Read & Write token.
See Set up from a terminal for what the command does.
Paste your keys
Fill in:
- Bucket, already filled in with the generated name. Change it if you created the bucket under a different one.
- The access key and secret, under the names your provider's dashboard uses for them. For example, Filebase calls them Access token and Secret key, and Garage calls them Key ID and Secret key. On Filebase, R2 and Amazon S3, a link (Open Filebase keys, for example) goes to the page where you'll find them.
- Account endpoint, on Cloudflare R2 only:
https://<account-id>.r2.cloudflarestorage.com. - Region, on Amazon S3 only. The endpoint follows from it.
Advanced holds the folder inside the bucket, the region, and path-style addressing. A folder inside the bucket lets one bucket hold more than one account's chats.
Your secret is Stored in this browser, encrypted under your passkey. Anyone who can use this browser while you're signed in can use the bucket.
Press Run the checks →.
Run the checks
The checks start on their own, talking to your bucket directly from this browser:
| Check | What it shows |
|---|---|
| Reachable | The endpoint answers and the bucket exists there. |
| Keys accepted | The bucket accepts the keys, and they can list its contents. |
| CORS allows this app | The bucket's CORS rule lets this app's requests through. |
| Can write | The keys can write a test object, and read it back unchanged. |
| Can delete | The keys can delete the test object again. |
The first three come from one request, so they resolve together. If that request fails, only the check it tells you about turns red. The others stay unticked, because the failure says nothing about them.
A failed check says what's wrong and offers Fix →, which takes you to the screen and field to change:
- Reachable goes to the Bucket field.
- Keys accepted goes to the access key field.
- CORS allows this app and Can write go back to the bucket's checklist, where the CORS rule is.
- Can delete goes to the keys.
A signature rejected because of the wrong region looks like a key problem but
isn't, so its Fix → goes to Region instead (under Advanced, except
on Amazon S3). Filebase and Cloudflare R2 want auto. Amazon S3 wants the
bucket's own region.
After changing something, press Run the checks again.
Connect
Connect is enabled only once all five checks pass. It runs them once more, then saves the connection.
The last screen says Connected, and your history starts copying across. Under it is the Connect your next device card. See Add another device.
The CORS rule
The bucket needs a CORS rule before a browser may talk to it:
[
{
"AllowedOrigins": ["*"],
"AllowedMethods": ["GET", "PUT", "DELETE", "HEAD"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3600
}
]
"*" is safe here: a request is authorized by its signature, not by where it
came from, and no cookie is involved. It also keeps working when the app is
served from an address that changes with each release. To narrow it anyway,
use Restrict it to under the rule in the app, which swaps "*" for the
app's own address.
On Filebase, the CORS tab's Public Read-Write preset applies this rule. Read-Only isn't enough.
Provider reference
| Provider | Notes | |
|---|---|---|
| Someone hosts it | Filebase | Endpoint https://s3.filebase.io, region auto. Create the bucket in the console as an S3 bucket, not an IPFS one. Objects in an IPFS bucket stay fetchable by content ID from public gateways, and unpinning is not a guaranteed delete. Keys are under Access Keys. |
| Cloudflare R2 | Endpoint https://<account-id>.r2.cloudflarestorage.com, region auto. Create an API token with Object Read & Write, scoped to the bucket: that's the token the app keeps. Creating the bucket and editing CORS need the dashboard or an Admin Read & Write token. | |
| Amazon S3 | Leave Block Public Access on. Make an IAM user allowed only this bucket: s3:ListBucket on the bucket, and s3:GetObject, s3:PutObject and s3:DeleteObject on its contents. The terminal tool also needs s3:CreateBucket and s3:PutBucketCORS while it runs. | |
| You host it | Garage | S3 port 3900, and it serves no TLS itself, so put a reverse proxy in front. The region must match s3_region in garage.toml; the app starts on garage, which is what the published samples use. A wrong region is refused as a bad signature, not as a bad region. For the terminal tool to create the bucket, the key needs --create-bucket; or create it with garage bucket create. |
| SeaweedFS | S3 port 8333. Region is us-east-1 unless you configured one. Make a dedicated identity with Read, Write and List on the bucket rather than using an admin one. | |
| Something else | Other S3-compatible | Anything that can list a bucket and get, put and delete objects, MinIO included. Listing is the one to check: some services allow the other three without it. Path-style addressing is the default; turn it off only if the certificate covers <bucket>.<host>. |
With any provider:
- Never turn on object versioning, or any retention or undelete feature. A version history keeps copies of what you deleted, and the Can delete check can't tell.
- Give it its own bucket. The app owns the layout inside it.
- Use an
https://endpoint with a public hostname. The app can't reach a plainhttp://address on the internet, some browsers refuse one on your own network, and a phone on another network can't reach your local network at all.
Set up from a terminal
npx @txnlab/zs-sync-setup does the bucket-side steps a browser isn't allowed
to do. It needs Node 20 or later. Run it with no options and it asks for the
provider, the keys and a bucket name:
npx @txnlab/zs-sync-setup
It reports each step with a ✓ or ✗ and, when one fails, what to change:
- Bucket. Checks that the bucket exists and creates it if it doesn't. On Filebase it never creates one, because a bucket created through the API is always IPFS. Create it in the console as type S3 first.
- Bucket type, on Filebase only. Writes and deletes a test object, and refuses an IPFS bucket.
- CORS. Applies the rule the app needs, then reads it back where the provider supports that.
- Versioning. Warns if versioning is on. It never changes the setting.
- Checks. Lists the bucket, writes a test object, reads it back, and deletes it: the same checks the app runs.
- Output. A link (
https://zerosignal.ai/sync#c=zsmirror1:…) that connects the app on this device when you open it, and the barezsmirror1:string for pasting into /sync on any device. Add--qrto also print the link as a QR code.
Before the steps run, it warns about a plain http:// endpoint or one on a
private address. Those pass every step here and are then refused by the
browser.
Every step is safe to re-run, so the answer to any failure is: fix that, then
run the same command again. It exits with 0 when everything passed, 1 when
a step failed, and 2 for a usage error.
Nothing is written to disk, and nothing is sent anywhere except the endpoint you give it.
Keys. The tool reads the access key and secret from ZS_ACCESS_KEY_ID and
ZS_SECRET_ACCESS_KEY, or asks for them at a masked prompt. A
--secret-access-key flag works too, but it ends up in your shell history.
Options. Every prompt has a flag, so a run can be one line:
ZS_ACCESS_KEY_ID=… ZS_SECRET_ACCESS_KEY=… \
npx @txnlab/zs-sync-setup --provider filebase --bucket zs-abc123
| Flag | What it sets |
|---|---|
--provider <id> | filebase, r2, aws, garage, seaweedfs, or other. |
--endpoint <url> | The endpoint, host only, such as https://s3.filebase.io. |
--region <name> | The signing region: auto for Filebase and R2. |
--bucket <name> | The bucket. Without it, the tool suggests a random zs-… name. |
--prefix <path> | A folder inside the bucket. Empty by default, as in the app. |
--origin <url> | Narrows the CORS rule to one origin. Repeat it for more than one. The default is *. |
--app-url <url> | The app address the link points at. Defaults to https://zerosignal.ai. |
--path-style / --no-path-style | Path-style or virtual-host addressing. Path-style is the default everywhere but Amazon S3. |
--qr | Also print a QR code of the link. |
--json | No prompts. Prints the settings, every step, the connection string and the link as JSON. |
--yes | Skip confirmations. |
Three of those need care:
--jsonoutput contains your secret key. Don't pipe it into a log.--prefixmust match the folder set in the app. Otherwise the two sync into separate folders in the same bucket and never see each other.--app-urlmust be the address where your passkey lives. A link built for another address sends the device somewhere its passkey doesn't exist.
The link and the string carry the keys you ran the tool with. On Cloudflare R2, that means a run with an Admin Read & Write token hands the app that token. To have the app keep the narrower Object Read & Write token, create the bucket and its CORS rule in the dashboard and use the console route in the app.
Add another device
Once a bucket is connected, you don't retype its details on the next device.
Show the code on a device that's already syncing
The Connect your next device card is on the last setup screen. Later, open Settings → Storage and press Connect another device. The button is there only for a bucket, and only while syncing isn't paused.
Choose Scan for a QR code or Paste for a string, then press Reveal the code. Your passkey confirms it's you, and the code or the string appears. One reveal covers both, so you can switch between them without another prompt. Copy connection string copies the string.
The card counts down (Hides in 1:30) and then hides the code. Hide puts it away sooner. Show again brings it back, after another passkey prompt.
Open it on the new device
Scan the code with the new device's camera. It opens zerosignal.ai/sync with everything filled in, with no app to install and no camera permission to grant.
Or open /sync on the new device, choose I already sync on another
device, and paste the string.
Connect
The new device shows which bucket and provider it's about to join and runs the same five checks. If you aren't signed in yet, it asks you to sign in with the same passkey as your other device: your chats are encrypted under it, and a different one couldn't read them. Then press Connect this device, and your chats arrive.
The connection string carries the bucket's keys. Anyone who has it can read and write your bucket, so treat it like the secret key itself. Don't post it, and don't leave the QR code on screen.
The part of the scanned link that carries the settings is never sent to a server, and the app clears it from the address bar before doing anything else.
What the storage can see
Everything is encrypted on your device before it's written, with a key derived from your passkey. The same passkey derives the same key on another device, so there's no passphrase to invent or transfer.
Whoever holds the folder or bucket sees ciphertext and metadata: how many files there are, how big they are, when they changed, and how many devices you sync from. It never sees a title, a message, or who you talked to.
So the storage provider cannot help you read your chats, and neither can we. If you lose your passkey, the files are recovered through your recovery phrase, not through the bucket.
Deleting
Deleting a conversation deletes it on every device. A delete wins over any change made elsewhere, including a later one, so a chat deleted while syncing doesn't come back.
One exception: if you delete a chat while a device is disconnected from syncing, and then connect it to a fresh target, that chat can reappear. Delete it again and it goes for good. The app doesn't carry deletions across to a new target, because doing so could wipe an archive irreversibly.
When syncing pauses
Chat keeps working; only the copying stops. The Storage card says what's wrong and offers one action:
| What it says | What to do |
|---|---|
| The bucket is refusing these credentials. | The keys were revoked or their permissions narrowed. Make a new key, then press Fix →. |
| The browser refused to send the request. | Almost always the bucket's CORS rule. Press Fix →. |
| This browser needs your permission again before it can reach the folder. | Browsers drop folder access when they restart. Press Reconnect. |
| The folder isn't reachable right now | A disconnected drive, or a sync app that isn't running. Fix that, then press Check now. |
| The bucket isn't answering right now. | Usually the network, or the service having a moment. Nothing is lost, and the app keeps trying. Check now tries straight away. |
| The bucket is out of space / The folder is full | Free some up, then press Check now. |
| This device's clock is more than 15 minutes off | Signed requests are rejected for that alone. Fix the clock; your keys are fine. |
| Another tab of this app is handling the sync | Exactly one tab does the syncing. Close the other one, or use it instead. |
| Couldn't reach the bucket / Couldn't reach the folder | Something else went wrong. Nothing is lost, and the app tries again. |
Fix → opens a two-step repair for the connection you already have:
- Paste the new key for refused credentials, or Let this browser write again for a CORS refusal, which shows the CORS rule in full with a link to your provider.
- The five checks. When they pass, the repair closes and syncing resumes.
Every other case gets Check now, which runs a sync straight away.
Stopping or switching
Press Disconnect on the Storage card. The app asks Stop syncing to this bucket? (or folder?); press Stop syncing. Everything already saved stays exactly where it is. This browser just stops adding to it.
To switch to a different folder or bucket, disconnect, then start setup again.
If the card says Syncing needs a browser feature this one doesn't have, syncing can't run in this browser at all. Use Back up chats in the same pane to move history between devices.
What's next
- Conversations & history — the sidebar, search, and compacting.
- Settings — backup and restore, which live alongside syncing.
- Privacy & security — what each party in the system can see.