Skip to content
I'm Shuo
Go back

I Built My Own Firefox Bookmark Sync Tool: bmsync

This post is an AI translation of the Chinese original. If there are any discrepancies, the Chinese version takes precedence.

For work reasons, I live with two laptops: a company machine for email and day-to-day office work, and a personal machine for everything that doesn’t fit inside corporate permissions.

Firefox has been my main browser for years. But our IT policy blocks signing in to personal accounts in any browser. That’s a real, everyday annoyance: bookmarks I save while researching on my personal laptop aren’t there on my work machine, and useful links I stash at the office won’t open when I get home.

I tried the usual community options like floccus. They’re built for general use, though — lots of features, lots of setup and dependencies. What I wanted was much narrower: stable two-way sync across a few machines, with data living only on my own VPS. Nothing extra.

Since nothing quite fit, I wrote my own tool and called it bmsync. It’s a tiny Firefox extension plus a lightweight backend. Each click on the extension icon runs a full two-way sync of the bookmark tree across devices, and data never leaves my own server.

Here’s a record of the design thinking, the trade-offs, and a mid-course change of direction.

Overall Design: Server-Side Merge and Full-State Transfer

The easiest way to get multi-device sync wrong is to let each client compute its own diff. Once two sides disagree about state, merge logic turns into a mess fast.

To keep things simple, I made the browser extension almost dumb. It only does two things:

  1. Collect: read the local bookmark tree, turn it into a full snapshot, and send it to the server;
  2. Apply: take the latest complete snapshot back from the server and overwrite local bookmarks with it.

All merge logic lives on the server. That gives me idempotency for free: retries after a flaky network, an accidental double-click, or two machines syncing back-to-back all end up at the same result. There’s no risk of two clients racing to merge and corrupting data.

For transport I went with full-state transfer. In practice, 5,000 bookmarks serialize to around 800 KB of JSON, and even less after gzip. On today’s networks, that extra bandwidth is cheap compared to maintaining incremental version numbers and dealing with resume logic and out-of-order updates.

No Global IDs — So How Do We Recognize the Same Bookmark?

Firefox’s Bookmarks API only hands out local auto-increment integer IDs. The same bookmark gets a different ID on each device, and the API won’t store a custom global field for me.

My workaround is to derive a stable identity from content hashes:

There’s a deliberate idea baked in here: a bookmark’s key doesn’t include its parent folder. When you move a bookmark from folder A to folder B, the key itself stays the same — only the parent link changes. So the server reads that as a “move”, not as “delete the old one and create a new one”.

The flip side: if you edit a bookmark’s URL, or rename its parent folder, the system treats that as “delete the old entry plus create a new one”.

Deletes and Tombstones

In sync systems, deletes are trickier than inserts.

If you just compare bookmark lists, and the server sees a bookmark on machine A that’s missing on machine B, it can’t tell which story is true: “machine B just deleted it” or “machine B is a fresh device that hasn’t pulled it yet”.

So a local delete doesn’t physically remove the record. Instead it leaves a tombstone marked with d: true. Tombstones follow the same last-write-wins rule as normal bookmarks and stick around on the server for 90 days. That’s plenty of time for my regular machines to come online and pick up the deletion. Only after 90 days does the server purge expired tombstones for good.

Dodging Clock Skew: Hybrid Logical Clock (HLC)

Once timestamps decide which state wins, physical clock drift between devices becomes a real problem. If one laptop runs a few minutes fast, its older change could steamroll a newer edit from a machine with the correct time.

To take physical drift out of the picture, the project uses an HLC (hybrid logical clock). Once device B has seen device A’s state, every operation B produces afterward carries a logical timestamp strictly greater than A’s.

Both the server and the extension implement HLC on their own side, and a shared set of test cases pins down the behavior so both ends advance logical time the same way.

Stack Choice: From Go to C#

The backend started life in Go. For a personal utility, Go’s single-binary output and small memory footprint were genuinely convenient to deploy.

But when I reviewed the Go code — first sketched out quickly with AI help — it didn’t feel good to maintain. Without strict typing and object-oriented structure, logic sat scattered across flat functions, and each change meant re-tracing who depended on what.

In the end I switched back to C#, the language I know best, and leaned on NativeAOT to keep deployment light:

I also kept sync as a manual action on purpose. The extension doesn’t poll in the background — one click, one sync. That kind of predictability is exactly what I want from a small utility.

Known Trade-Offs

Since this is a tool tailored to my own use, I cut a few corners on edge cases quite deliberately:

  1. No ordering inside folders: synced bookmarks get appended at the end. Content and folder structure stay consistent; order doesn’t;
  2. URLs are globally unique: if you bookmark the same URL in two folders, sync merges them into one;
  3. Folder renames are pricey: renaming a folder recomputes keys for the whole subtree. Nothing is lost, but it leaves a batch of old tombstones behind;
  4. Don’t stay offline past 90 days: if a device stays disconnected for over 90 days, the server will already have purged the tombstones, so it may re-upload its stale bookmarks as if they were new;
  5. Core bookmarks only: no tags, notes, favicons, or reading-list extras.

Both the server and the extension are open source, and the extension is listed on the official Firefox store:

This isn’t a public SaaS — you’ll need to host the backend yourself. The repo ships a docker-compose.yml. Bring it up, then drop your server URL and access token into the extension’s settings page.

If you’re also stuck syncing bookmarks across locked-down machines and want the data to stay entirely on your own server, give it a try.


This post is licensed under CC BY-NC-SA 4.0. Please credit the author and include a link back when sharing.

Share this post:

Previous Post
What We Talk About When We Talk About Work Responsibility
Next Post
Small-Team Agile Collaboration in Practice with Leantime