I stopped treating the API as the source of truth. Here’s when a sync engine is worth it

Marcus Webb

Marcus Webb

August 28, 2026

I stopped treating the API as the source of truth. Here’s when a sync engine is worth it

I treated REST as truth for years. The server had the row. The client had a hope. That was correct for the admin table I shipped last month. It was wrong for the field-notes app that died in a basement. I stopped using “the API is the source of truth” as a personality. It is a choice. A sync engine is worth it when the user is a writer who will be offline, or when two devices will meet on the same facts before HTTP does. It is not worth it because a talk said local-first. Talks do not take the page when the local store leaks a tenant.

The landscape is in from fetch-and-hope to sync engines. This is the buy: when I pay the tax.

When it is worth it

The user writes more than they read, and they write in places with a bad pipe: site visits, trains, warehouses, planes. If their job is “open, type, still have it,” fetch-and-hope will train them to hate you. A local store that accepts the write and syncs later is the product. I have watched people screenshot our spinner and use paper. Paper is a competitor. Sync is how you beat paper.

Two devices or two people touch the same objects in the same hour. Last-write-wins after a refresh is a data-loss story we dressed as eventual consistency. If I cannot write a one-sentence merge rule I would say to a customer, I am not ready. If I can — last writer on this field, or you keep both lines — then an engine that applies the rule is cheaper than a custom toast we will get wrong.

The app is daily software, not a document they visit. Daily software accumulates local context: drafts, cursors, unsent, the last twelve objects. Fetching the world on focus is a battery and a lie. Syncing a slice is the honest model.

Paper sketch of a conflict between two versions of a sentence

When it is not worth it

The UI is a report, a settings form used weekly, an admin grid, a CMS preview. The user can wait 200ms. Offline is not an hour of their week. I will fetch. I will cache like an adult. I will not introduce a client database so the architecture looks 2026.

The team cannot staff permissions on the sync path. If we barely get authz right on GET, we will get it wrong in a replica. A leaked local row is worse than a slow table. I have paused a sync pitch for a year because the tenancy model was a rumor. The rumor did not get a local store.

There is no owner for conflicts and migrations. Client schema plus server schema is two clocks. If nobody will own both, the engine will own you. I will not buy a research paper and assign it to “the frontend.”

The write rate is low and the object is large. Syncing a warehouse to a phone is a joke. If the slice is “one object the size of a novel,” I will think hard. Sometimes fetch of that object plus a local draft buffer is enough. A full engine is for many small writes, not one huge read.

Simple admin table on a monitor in daylight, fetch is enough

The middle I actually ship

A lot of products need a draft buffer, not a replica of the domain. Compose box, offline queue, flush when the pipe returns. That is not a sync engine. That is a queue. I have overbuilt the engine when a queue would have saved the basement. I ask: do we need other devices to see this before the flush, or only this device to not lose it. Only this device is a queue. Other devices is a sync problem.

Another middle: optimistic UI on fetch, with a real merge on 409. Worth it when conflicts are rare. Not worth it when two people type every afternoon. Rare is a number. I want the number before I buy a protocol.

Costs I put on the RFC

Client migrations. Debugging “it is on the phone but not on the server.” Auth token expiry while local writes pile up. First-open time if the slice is greedy. Testing two clocks. A support tool to inspect a user’s local state. If we will not build the support tool, we will debug via screenshots. I have done that. I will not pretend it is fine.

Backend work: the server must accept that it is not the only store. Idempotency, “this write is allowed,” audit. If the API team will only do request-response CRUD, I do not start the engine. I start the conversation. If the conversation dies, fetch stays.

A time I paid and a time I refused

We paid for a field app. Techs in concrete buildings. They needed the last job’s notes and the ability to add a photo caption without a bar. The engine earned its tax in the first month. Support still hurt until we built a way to see a device’s pending queue. The tool was part of “worth it.” Without it I would have called the project a maybe.

We refused it on a billing admin. The PM wanted “instant everything.” Instant was a vanity spinner. The writes were rare, the objects were money, and conflicts would have been a legal story. We fetched, we locked the row on the server, we showed a stale banner if someone else had it. Boring. Correct. I will refuse again on money grids. Sync is not how I want two ledgers to meet.

I also refused a “collaborative settings page.” Two admins a month. A 409 and a refresh was enough. Collaboration theater is how engines get into products that needed a lock.

What I need from the backend before I say yes

Idempotent writes. A way to reject a sync that fails authz without deleting the user’s local draft blindly — or a product sentence that we will drop it and tell them. An audit if the domain is regulated. A slice query that is not “give me the org.” If those are missing, worth-it becomes wish-it. I will not start the client work to force the server later. Later is how we ship a local store of the wrong tenant and call it a preview.

I want a kill switch: sync off, app still reads via fetch for the people we can still serve online. If we cannot degrade, we are betting the company on a protocol. I will not do that on v1 unless the product is the protocol.

The questions I ask on Monday

  • Is the user a writer in a bad-pipe hour?
  • Do two endpoints of the product meet on the same facts before HTTP?
  • Can I say the merge rule in one customer sentence?
  • Can we filter the local slice with the same authz as the GET?
  • Who owns client schema and the support tool?

Three yeses and I will argue for a sync engine, preferably one we adopt rather than invent. Two yeses and I look at a draft queue first. One yes and I fetch. Zero and I do not want the meeting.

I stopped treating the API as the source of truth because some products were lying to users when the pipe died. I did not stop for every product. Truth can still live on the server when the UI is a table. Truth belongs next to the writer when the writer cannot wait. That is when the engine is worth it. The rest of the time the API can keep the row, and I can keep my Friday. I like Fridays. I like engines when they beat paper. I do not like them as a default costume for a grid that was already fine.

If a founder asks for a one-line rule: writers in bad pipes get a store; readers of tables get a GET. If they ask for the logo of the engine, I ask the five questions first. Logos are easy. Worth-it is a staffing and a merge sentence. I will buy a logo after I can say both. I will not buy a logo to feel like the future. The future is already fetch for most of the pages I ship. The part that is not fetch is the part I will pay for, and only that part. That is the whole conversion from “API is truth” to “truth is where the writer is.” I needed years to make it a rule instead of a mood. The rule is cheaper than the mood. The mood bought me a science project. The rule bought me a basement that still had the note.

More articles for you