Stop Writing Validation Twice: How I Built a "Shared Brain" Sync Engine with Go & WASM

Stop Writing Validation Twice: How I Built a "Shared Brain" Sync Engine with Go & WASM

Source: Dev.to

The "Shared Brain" Architecture ## The Old Way (Duplication Hell): ## The GoSync Way: ## The Hard Part: syscall/js and Merkle Trees ## 1. The Bridge (Go ↔ JS) ## 2. The Sync Protocol (Merkle Trees) ## The "Ah-Ha" Moment: Dogfooding ## Try it out (and roast my code) ## πŸ”— Links ## Features We have all been there. You spend Monday writing complex form validation logic in JavaScript for your frontend. You check for email formats, negative numbers, and required fields. Then, you spend Tuesday writing the exact same logic in Go (or Python/Node) for your backend API. The problem? Six months later, you update the backend rule but forget the frontend. Suddenly, your UI says "Success!" but your API throws a 400 Bad Request. Users are confused. You are frustrated. I got tired of this "Two-Language Drift." I wanted a way to write my business logic once in Go and have it run everywhereβ€”on the server and inside the user's browser. The idea was simple but ambitious: What if the browser was just another Go node? By compiling Go to WebAssembly (WASM), I realized I could run the exact same structs and validation functions on the client side. I defined a Syncable interface. You write your logic once in a shared Go package. The server runs it natively; the browser runs it via WASM. Getting Go to talk to the browser isn't exactly plug-and-play yet. I had to heavily utilize syscall/js to bridge the Go runtime with the browser's IndexedDB. The biggest challenge was the single-threaded nature of WASM. If you aren't careful, a heavy Go routine can deadlock the browser UI. I ended up writing a custom async wrapper to handle the I/O without freezing the DOM. Sending the whole database back and forth is a bandwidth killer. To make this "Local-First," I implemented a Merkle Tree synchronization protocol. It's efficient, fast, and mathematically guarantees consistency. I didn't want to just write a library; I wanted to prove it works. So I built the documentation website using the engine itself. When you visit the site, it actually: You can type in one box (Client A), and watch it sync to the "Server" visualization in real-time. You can even simulate "Offline Mode" on the site. You'll see the data get queued locally in IndexedDB, and the second you toggle "Online," the Merkle sync kicks in and flushes the queue. GoSync is currently in Public Beta (v1.0). I am specifically looking for feedback on my syscall/js implementation. I suspect there might be a performance bottleneck there during high-throughput writes, and I'd love to hear from other Go WASM hackers. Let me know what you think! I'm happy to answer questions about the WASM bridge, the sync protocol, or any architectural decisions. Built with Go, WASM, and too much β˜• by Harshal Patel Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK: // Frontend (JS) - Maintained by Team A function validateTask(task) { if (!task.title) throw "Title required"; // ... more logic } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: // Frontend (JS) - Maintained by Team A function validateTask(task) { if (!task.title) throw "Title required"; // ... more logic } CODE_BLOCK: // Frontend (JS) - Maintained by Team A function validateTask(task) { if (!task.title) throw "Title required"; // ... more logic } CODE_BLOCK: // Backend (Go) - Maintained by Team B func ValidateTask(t Task) error { if t.Title == "" { return errors.New("required") } // ... logic duplicated } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: // Backend (Go) - Maintained by Team B func ValidateTask(t Task) error { if t.Title == "" { return errors.New("required") } // ... logic duplicated } CODE_BLOCK: // Backend (Go) - Maintained by Team B func ValidateTask(t Task) error { if t.Title == "" { return errors.New("required") } // ... logic duplicated } CODE_BLOCK: // shared/logic.go // Compiled to BOTH Native Binary and WASM func Validate(t *Task) error { // This code runs in your Chrome tab AND your Linux server if t.Title == "" { return errors.New("required") } return nil } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: // shared/logic.go // Compiled to BOTH Native Binary and WASM func Validate(t *Task) error { // This code runs in your Chrome tab AND your Linux server if t.Title == "" { return errors.New("required") } return nil } CODE_BLOCK: // shared/logic.go // Compiled to BOTH Native Binary and WASM func Validate(t *Task) error { // This code runs in your Chrome tab AND your Linux server if t.Title == "" { return errors.New("required") } return nil } CODE_BLOCK: β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Root Hash β”‚ β”‚ (Changes if ANY child changes) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Branch Aβ”‚ β”‚ Branch Bβ”‚ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β”‚ β”‚ β”Œβ”€β”€β”€β”΄β”€β”€β”€β” β”Œβ”€β”€β”€β”΄β”€β”€β”€β” β–Ό β–Ό β–Ό β–Ό [Leaf1] [Leaf2] [Leaf3] [Leaf4] βœ“ Same βœ“ Same βœ— DIFF! βœ“ Same ↑ Only this syncs! Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Root Hash β”‚ β”‚ (Changes if ANY child changes) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Branch Aβ”‚ β”‚ Branch Bβ”‚ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β”‚ β”‚ β”Œβ”€β”€β”€β”΄β”€β”€β”€β” β”Œβ”€β”€β”€β”΄β”€β”€β”€β” β–Ό β–Ό β–Ό β–Ό [Leaf1] [Leaf2] [Leaf3] [Leaf4] βœ“ Same βœ“ Same βœ— DIFF! βœ“ Same ↑ Only this syncs! CODE_BLOCK: β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Root Hash β”‚ β”‚ (Changes if ANY child changes) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Branch Aβ”‚ β”‚ Branch Bβ”‚ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β”‚ β”‚ β”Œβ”€β”€β”€β”΄β”€β”€β”€β” β”Œβ”€β”€β”€β”΄β”€β”€β”€β” β–Ό β–Ό β–Ό β–Ό [Leaf1] [Leaf2] [Leaf3] [Leaf4] βœ“ Same βœ“ Same βœ— DIFF! βœ“ Same ↑ Only this syncs! - Both the client (IndexedDB) and Server (SQLite) maintain a hash tree of their data. - When they connect, they compare the "Root Hash." - If it matches? Zero data sent. - If it differs? They drill down the branches to find exactly which record changed (the delta) and swap only that item. - Downloads the WASM binary (~1.2MB) - Boots up the Go engine in your browser - Connects to a test server via WebSocket - Live Demo & Docs: gosync-zero.vercel.app - Source Code: github.com/HarshalPatel1972/GoSync