
Engineering
Three ways to run untrusted code safely: JSONLogic, WASM, and a QuickJS scripting tier
FastYoke Engineering · 8 min read · Jul 20, 2026
- Architecture
- Sandboxing
- WebAssembly
- Security
The problem
FastYoke exists so that the people who run a business can encode how it runs — the guard that decides when a job may advance, the rule that prices an order, the script that reshapes an inbound payload before it lands. All of that is logic our customers author, and every bit of it executes on our infrastructure, in a process shared with other tenants, sometimes on hardware we don't control at all.
That leaves us with an uncomfortable job: run code we didn't write, from
organizations we can't vet line by line, without letting any of it read a
neighbor's data, wedge the machine, or reach the network. The tempting
shortcut is the one nearly every low-code platform reaches for at least
once — take the customer's expression, hand it to eval() or a hastily
built string parser, and hope the input is friendly. That shortcut is how
you end up with a remote-code-execution hole with a form field in front of
it.
Running untrusted code isn't one problem
The honest answer is that "run untrusted code safely" is not one problem with one solution. It's a spectrum. A predicate that checks whether an invoice total exceeds a threshold has almost nothing in common — in risk, in cost, in the machinery it needs — with a script that walks a nested payload and rewrites it. Force both through the same mechanism and you either overbuild for the trivial case or underbuild for the demanding one.
So the useful question isn't "what's the one safe way to run customer logic." It's "what's the lightest mechanism that safely runs this piece of logic." A comparison wants almost no machinery. A scoring loop wants a real, bounded execution environment. A hand-written script wants that environment plus a language runtime — safely nested inside it. Three weights of logic, three matched answers.
The naive answers, and why they fail
There are two obvious ways to run other people's logic, and both collapse under a multi-tenant load.
The first is a string-parsed expression language — invent a little syntax for guard conditions and evaluate it. The moment that evaluator can reach anything real, a crafted string is an attack, and the more capable you make the language, the larger the attack surface grows. You've built an interpreter you now have to keep safe against every hostile input forever.
The second is a raw in-process interpreter — embed a scripting engine directly in the host and hand tenant scripts to it. It's fast and cheap per call, but the engine runs with the host's full authority. A bug or a hostile input is a path straight into everything the server process can touch: other tenants' memory, the filesystem, the network.
What we actually want is isolation strong enough that a hostile input stays harmless, at a cost low enough to run thousands of times a day per tenant. No single tool hits that target for every weight of logic, which is why FastYoke layers three.
How FastYoke approaches it
The platform doesn't have a sandbox. It has three, each matched to a different weight of logic, and the discipline is reaching for the lightest one that does the job.
Declarative JSONLogic predicates, with nothing to parse. The
overwhelming majority of business logic is a yes-or-no question asked of
some data — is the order over the approval limit? has the inspection been
signed? These are the guard conditions on finite-state-machine
transitions, and they are exactly where a naive platform would introduce an
expression language. FastYoke's default layer refuses that framing: guards
are JSONLogic predicates, where a rule is a JSON
data structure, not source code. {"<": [{"var": "total"}, 500]} is a
tree the evaluator walks — an array whose first element names an operation
and whose rest are its arguments. There is no tokenizer, no parser reading
arbitrary syntax, no step where a crafted string becomes executable. A
predicate can read fields off the record in front of it and compare them,
and that is the entire extent of its reach — it cannot open a socket or
touch a file, because the vocabulary contains no operation that does. This
is the layer to want: fast, serializable, diffable, and safe by
construction rather than by after-the-fact policing. Until the compute-heavy path below is genuinely
warranted, it's the only one FastYoke uses.
A WebAssembly host with resource caps, for logic that computes. Some
decisions genuinely need computation — a scoring model that loops over line
items, a geometric check, a pricing rule with real arithmetic and
branching. Rather than grow the declarative evaluator into a full language
by accident, FastYoke runs that logic inside
wasmtime, a production
WebAssembly runtime, hosted
in-process by WasmHost::evaluate. WebAssembly earns its place on three
properties: it's compiled, so it runs at near-native speed; it's
memory-safe, so a module cannot read outside its own linear memory; and
it's capability-based, so a module can do nothing to the outside world
unless the host hands it that ability — and we hand it almost nothing. Two
ceilings keep a module from taking a node down. A fixed fuel budget
meters execution and halts a module that burns through its allotment, so an
infinite loop fails one evaluation instead of pinning a core. A hard
memory ceiling caps allocation, so a runaway module fails safely instead
of starving its neighbors. The invariant from the first layer holds
unchanged: no host syscalls, no network, no filesystem — capabilities the
sandbox is built without, not granted and then restricted.
A QuickJS scripting tier, running inside that sandbox. The third layer serves someone who wants to write an ordinary script — plain, readable logic to reshape a payload — without hand-authoring a WebAssembly module. Embedding a JavaScript engine directly in the host would undo everything the first two layers bought us, so FastYoke doesn't. The scripting tier is a QuickJS engine compiled to WebAssembly and run inside the layer-two sandbox: a shared QuickJS instance loaded in wasmtime evaluates tenant scripts, which inherit every guarantee already described — the fuel budget, the memory ceiling, no network, no filesystem — because they run one level deeper than the boundary, not outside it. The seam that makes this safe is what the engine is not given. QuickJS normally expects host services — a clock, a way to write output — and reaches for them through WASI imports. FastYoke stubs those out: the call for the current time returns zero, and the call to write bytes discards them. Those shims exist only because the module won't instantiate without something bound to those names; they are compile-time scaffolding, not a capability the guest gets to use. A script cannot read the host clock and cannot emit to the host — its only I/O is the narrow set of host imports FastYoke defines on purpose.
What to watch out for
The discipline these layers demand is choosing the lightest one that works, and resisting the pull toward the heaviest. A guard that is genuinely a comparison should be a JSONLogic predicate, not a script — wrapping a one-line threshold check in a QuickJS module buys nothing and costs a compile, an instantiation, and a fuel budget to babysit. The compute-heavy paths earn their overhead only when the logic actually computes.
The other trap is mistaking the sandbox's guarantees for a license to put anything inside it. The fuel budget bounds what a script can do; it does not make a script that orchestrates five systems a good idea. Logic in any layer should stay bounded — read the input in front of it, decide or transform, return. When a guard or script starts wanting to make network calls, wait on other systems, or run for a meaningful stretch of wall-clock time, that's the signal the work belongs in a job or a connector that a transition kicks off, not in the predicate or script running under the sandbox's ceiling. The boundary stays strong precisely because we keep the things crossing it small.
Where to go next
The layered sandbox is what lets FastYoke run your logic on shared infrastructure without asking you to trust your neighbors — the same engine runs in our cloud, in your own data center, or fully air-gapped, and the guarantee doesn't change. For the design decision underneath the second and third layers, read why FastYoke chose WebAssembly; for where these predicates live in a workflow, see finite state machines are the right abstraction and the workflow builder docs. If your logic has to hold up to an auditor, regulated workflows is built on exactly this boundary, and pricing shows which tiers include the scripting tier.