Modern form builders have a habit of pretending the browser is a reliable execution environment.
It isn’t.
It’s a wildly inconsistent collection of underpowered mobiles, locked-down corporate laptops, ageing desktops, flaky networks, and browsers doing their best despite being asked to behave like application servers. If your architecture depends on client-side performance, you’ve already lost control of your system.
We learned this the hard way.
The browser is the weakest part of your stack
Simple forms are fine. Name, email, message — no one’s breaking a sweat there. But once forms stop being decorative and start doing real work — calculations, conditional logic, large datasets, repeatable structures — the browser becomes the bottleneck.
And worse, it’s a bottleneck you don’t control.
Over the years we tried everything:
- Paginated, server-rendered forms
- Splitting workflows across multiple forms
- Aggressively pruning features
- Quietly hoping users would upgrade their hardware
- Occasionally resorting to “please use a better computer”
None of that scales. It just spreads the pain around.
If your system only works well on good devices, your system doesn’t really work.
Client-side logic leaks things it shouldn’t
Performance wasn’t even the biggest red flag.
The real problem was logic.
Most form builders push conditional rules and calculations into the browser because that’s where instant feedback is easiest. The trade-off is that all of your business logic gets shipped to the user.
Developers tend to shrug at this. Clients don’t.
Pricing formulas, scoring rules, internal thresholds — once they’re in JavaScript, they’re public. Browser dev tools don’t require a warrant. Several of our clients quite reasonably said: absolutely not.
At that point, “but everyone does it this way” stops being a compelling argument.
Our decision: the browser gets no intelligence
So when we started FormWork, we made a deliberately unfashionable decision:
All form processing happens server-side. No exceptions.
The browser renders fields. It captures input. That’s it.
Every value change is immediately sent to our servers. We evaluate the new state, apply rules, run calculations, enrich data, and return only the minimal set of changes required to update the form.
Think of the browser as a keyboard and a screen — not an execution engine.
This isn’t an optimisation. It’s a line in the sand.
Yes, this makes everything harder
Doing everything server-side is not the easy option. Anyone suggesting otherwise hasn’t actually built it.
We own all the load
What most systems distribute across thousands of client devices, we handle centrally. Traffic spikes, worst-case forms, pathological users — it’s all our problem.
Latency suddenly matters everywhere
Every keystroke has a round trip. Network variability, processing time, rendering delays — all amplified. You don’t get to hide behind “the browser will sort it out”.
Offline mode is off the table
Server-driven forms don’t work offline. We considered hybrid approaches, but they collapse under their own complexity and undermine the architecture. We chose capability and correctness over pretending offline support was free.
These are not accidental downsides. They’re conscious trade-offs.
What we get in return is worth it
Practically unbounded form complexity
Browser memory is no longer the limiting factor. Form size stops being a concern. Nested repeaters stop being scary.
A million rows? Fine. Deeply recursive structures? Also fine. You probably shouldn’t — but you can.
Business logic stays private
Rules live where they belong: on the server. Nothing sensitive leaks. Nothing needs to be trusted to the client. Compliance teams sleep better.
Real-time data enrichment
If we’re already recalculating state server-side, we can do more than basic maths. We can cross-reference previous submissions, pull in external data, and enrich responses live — without exposing implementation details.
This is where forms stop being “inputs” and start becoming systems.
How we make this viable
Once you commit to server-side processing, efficiency stops being optional.
We built FormWork in Go because it’s fast, predictable, and boring in the best possible way. When every interaction hits your backend, predictability beats cleverness every time.
Submissions are stored in Postgres as structured JSON for flexibility. Normalisation and downstream processing happen later, when they’re easier to evolve without breaking live behaviour.
Latency is measured obsessively. The goal isn’t theoretical perfection — it’s keeping updates fast enough that users don’t consciously perceive delay. If interaction feels instant, the architecture disappears. That’s the point.
Scaling doesn’t get easier — but it gets possible
Handling thousands of concurrent users is manageable. Handling millions, globally distributed, all generating incremental updates is significantly harder.
The difference is that this architecture fails gracefully. It gives us room to optimise, cache, shard, and evolve without discovering too late that the browser was the wrong place to put the intelligence.
We’d rather solve hard infrastructure problems than debug why someone’s 2014 laptop can’t cope with our form.
— Simon & Andrius