No-Code Workflow Platform · Zino
A platform that builds business apps — without code
Forms, approvals, dashboards and AI agents, assembled visually and running minutes later. Here is how the thing underneath works — and what each decision cost.
- Role
- Tech lead — builder & runtime
- Period
- 2023 — Present
- Stack
- React 19 · Go · PostgreSQL · NATS
What it is
A multi-tenant platform where a company builds its own internal applications from settings rather than source code — workflows, forms, views, roles, triggers, AI agents — and a separate runtime serves those apps to end users.
The Studio does not generate code. That one distinction drives most of the architecture below.
The problem under every no-code platform
When a customer invents a field at runtime, it has to go somewhere. There are three classic answers, and each hurts in a different place.
EAV
one row per field value
+ Schema stays fixed
− Every list query becomes a pile of self-joins
Pure JSONB
one blob per record
+ Stores any shape, honestly
− Filtering, sorting and charts are slow and untyped
Real columns
DDL per tenant
+ Fast and properly typed
− Unbounded schema, and DROP COLUMN on live data
Most platforms pick one and live with its weakness. This one refuses to.
Compile the design graph; never read it at runtime
Letting the runtime read the builder's tables is simple and wrong twice over: a six-to-ten table join on every request, and every builder schema change becomes a runtime schema change. Publishing is a compile step instead.
Design graph
normalized, in the Studio
Runtime image
one document per version
In-memory graph
loaded at boot
Then: interpreted per request. No codegen, no per-request compile.
Chose
Deploy compiles the design graph into a runtime image. The runtime never reads the authoring tables.
What it costs
A deploy becomes a real event, not a save — and the compiled image can drift from the authoring model, which means repair tooling.
JSONB is the truth; the typed table is disposable
The decision I'd defend hardest, because it looks like a compromise and isn't. Because the JSONB is authoritative, the typed table can be dropped, retyped, renamed or rebuilt from nothing — the backfill replays it. That is what makes deploy-time DDL against customer data survivable rather than terrifying.
Source of truth
JSONB record
Every field the customer invented. Authoritative, never rebuilt.
Disposable
Typed companion table
Real indexed columns, only for fields a view or chart reads. Drop it, rebuild it.
The case that proves someone thought it through is the in-flight one: dropping a column an older still-deployed version references would corrupt running instances. So a deploy unions every active version's field set and aborts on a type conflict rather than guessing.
Chose
JSONB as source of truth, with a per-workflow typed table derived from it at deploy.
What it costs
Every mutation dual-writes, sync is best-effort so drift needs a reconciler, and a full backfill makes deploy a downtime activity by design.
Versions pinned, with a hotfix door
Instances are pinned to the version they started on, and a published version rejects structural edits. The usual answer stops there — “you can't change a published workflow” — which isn't good enough, because what most needs fixing in production isn't structural.
Frozen
States, transitions, fields — anything structural
Stays open
Permissions, triggers, prefills, rule logic — reaching live instances at the next deploy
Known gap: view configuration isn't versioned, so it can drift from the workflow version it belongs to.
What I owned
The builder — Studio frontend and backend — and the shared runtime engine, AI agents included. Design-time and runtime, not a single service. Most of the work went into the read and commit paths.
Record-view read path
Composite index, projected selects, SQL resolution hoisted out of the row loop
SQL injection in ORDER BY
Found it; fixed at the sink with an allowlist so callers can't reintroduce it
COUNT(*) OVER() split
Pagination counts were dragging the page off its index
Activity commit path
One transactional update plus a combined upsert; audit moved off the request path
Trigger snapshot serving
Removed live DB reads from the node hot path
Secret redaction
Across trigger logs and audit — a sweep, not a patch