82 karma · joined May 5, 2025
https://github.com/evelant/synchrotron
Idea is to sync business logic calls instead of state. Let business logic resolve all conflicts client side. Logical clocks give consistent ordering. RLS gives permissions and access control. No dedicated conflict resolution logic necessary but still guarantees semantic consistency and maximally preserves user intentions. That’s the idea at least, requires more thought and hacking.
I think there's probably a lot of value to be gained in tooling for coding agents that codify and enhance the describe -> explore -> plan -> refine -> implement -> verify cycle. With most popular tools (cursor, claude code, roo, augment, windsurf, etc) you have to do this workflow "manually" usually by having the model write out .md files, it isn't super smooth.
As for the specific scenario, if a client sets a task as COMPLETE and another sets it as CANCELLED before seeing the COMPLETE from the other client here's what would happen.
Client1: { id: 1, action: completeTask, taskId: 123, clock: ...}
Client1: SYNC -> No newer events, accepted by server
Client2: { id: 2, action: cancelTask, taskId: 123, clock: ...}
Client2: SYNC -> Newer events detected.
Client2: Fetch latest events
Client2: action id: 1 is older than most recent local action, reconcile
Client2: rollback to action just before id: 1 per total logical clock ordering
Client2: Replay action { id: 1, action: completeTask, taskId: 123, clock: ...}
Client2: Replay action { id: 2, action: cancelTask, taskId: 123, clock: ...} <-- This is running exactly the same application logic as the first cancelTask. It can do whatever you want per app semantics. In this case we'll no-op since transition from completed -> cancelled is not valid.
Client2: SYNC -> no newer actions in remote, accepted
Client1: SYNC -> newer actions in remote, none local, fetch newer actions, apply action { id: 2, action: cancelTask, ...}
At this point client1, client2, and the central DB all have the same consistent state. The task is COMPLETE. Data is consistent and application semantics are preserved.
There's a little more to it than that to handle corner cases and prevent data growth, but that's the gist of it. More details in the repo.
The great thing is that state is reconciled by actually running your business logic functions -- that means that your app always ends up in a valid state. It ends up in the same state it would have ended up in if the app was entirely online and centralized with traditional API calls. Same outcome but works totally offline.
Does that clarify the idea?
You could argue that this would be confusing for Client2 since they set the task to cancelled but it ended up as complete. This isn't any different than a traditional backend api where two users take incompatible actions. The solution is the same, if necessary show an indicator in the UI that some action was not applied as expected because it was no longer valid.
edit: I think I should improve the readme with a written out example like this since it's a bit hard to explain the advantages of this system (or I'm just not thinking of a better way)
* first update sets task cancelled_at and cancellation_reason
* second update wants the task to be in progress, so sets started_at
CRDT's operate only at the column/field level. In this situation you'd have a task with cancelled_at, cancellation_reason, status in progress, and started_at. That makes no sense semantically, a task can't both be cancelled and in progress. CRDTs do nothing to solve this. My solution is aimed at exactly this kind of thing. Since it replicates _intentions_ instead of just data it would work like this:
action1: setCancelled(reason) action2: setInProgress
When reconciling total order of actions using logical clocks the app logic for setCancelled runs first then setInProgress runs second on every client once they see these actions. The app logic dictates what should happen, which depends on the application. You could have it discard action2. You could also have it remove the cancellation status and set in_progress. It depends on the needs of the application but the application invariants / semantics are preserved and user intentions are preserved maximally in a way that plain CRDTs cannot do.
At least in my thinking/prototyping on the problem so far I think this solution offers some unique properties. It lets clients operate offline as long as they like. It delegates the heavy lifting of resolving state from actions/events to clients, requiring minimal server logic. It prevents unbounded growth of action logs by doing a sort of "rebase" for clients beyond a cutoff. It seems to me like it maximally preserves intentions without requiring specific conflict resolution logic. IMO worth exploring further.
The gist is:
* Replicating intentions (actions, immutable function call definitions that advance state) instead of just replicating state.
* Hybrid logical clocks for total ordering.
* Some client side db magic to make action functions deterministic.
This ensures application semantics are always preserved with no special conflict resolution considerations while still having strong eventual consistency. Check out the readme for more info. I haven’t gotten to take it much further beyond an experiment but the approach seems promising.
I've run into a lot of situations where doing the "simplest thing that could possibly work" ultimately led to mountains of pain and failure. I've also run into situations where things were over-engineered to account for situations that never came to be resulting in similar pain.
It boils down to "it depends" -- a careful analysis of the requirements, tradeoffs, future possibilities, and a mountain of judgement based on experience.
For sure, err on the side of "simple" when there's uncertainty but don't be dogmatic about it. Apply simple principles like loose coupling, pure functions, minimal state, avoid shared state, pick boring tools, and so on to ensure that "the simplest thing that could possibly work" doesn't become "the simplest thing that used to work but is now utter hell in the face of the unexpected". It all depends.
For example at least here in the US lithium ion tool batteries come with lots of different connector shapes just to lock users into a brand and are not repairable. Manufacturers charge a lot for the batteries, often more than the tools, and if (when) they go bad they just have to be thrown out. Manufacturers even occasionally "upgrade" their battery designs so that they're not backwards compatible to force purchase of new batteries for newer tools. A universal repairable tool battery would likely be a big hit!
Another use case might be USB power packs. These are all sealed and not repairable. They go bad after awhile and the whole thing is waste when it's most likely just a single cell failing.
It is opinionated and not for every use case, also very experimental, but you might find some of the ideas interesting.