Under the hood
One extension. A database, two AI engines and Python. No server.
The hook. Four engines, zero servers.SurrealDB, ONNX Runtime Web, WebLLM and Pyodide, all client-side.
Version texte
- Under the hood: One extension. A database, two AI engines and Python. No server.
Under the hood is a series about how AWFlow, a browser-native automation tool, works inside. This is #2.
AWFlow has no backend for its core features. Yet a single Chrome extension runs a real database, two AI inference engines and a full Python interpreter. All four are possible because of WebAssembly. Here’s what each one does, how it’s wired up, and what it costs.
The idea
Your browser is quietly an operating system.
It runs real software: storage, threads, a GPU, a sandbox.IndexedDB, Web Workers, WebGPU and WebAssembly give it the parts of an OS.
Version texte
- The idea: Your browser is quietly an operating system.
The browser has the parts of an OS
WebAssembly (WASM) is a binary format that languages like Rust, C and C++ compile to. The browser runs it at close to native speed, inside the same sandbox as any web page: no install and no system access. If you want the plain-words tour of how that works, WebAssembly, explained covers it; this post is about what we built on top.
For a product like ours, it changes the architecture. Instead of running engines on a server and sending users’ data to them, we ship the engines to the user. And the browser already has the pieces an engine expects from an operating system:
Rules about what code may do.Same-origin sandbox plus MV3 CSP: script-src 'self' 'wasm-unsafe-eval'.
Version texte
- An operating system: A disk ≈ Your browser: IndexedDB and the Cache API
- An operating system: Processes ≈ Your browser: Web Workers
- An operating system: Drivers for the GPU ≈ Your browser: WebGPU
- An operating system: Programs you install ≈ Your browser: WASM engines shipped in the extension
- An operating system: Permissions ≈ Your browser: The sandbox and the extension's CSP
Here are the four engines, from the bottom of the stack to the top:
| Engine | Compiled from | What it does in AWFlow |
|---|---|---|
| SurrealDB | Rust | The local database: workflows, run history, agents, knowledge |
| ONNX Runtime Web | C++ | Local AI tasks via transformers.js: classify, summarize, translate, transcribe, embed |
| WebLLM (MLC) | WASM runtime + WebGPU | Chat LLMs on the user’s GPU |
| Pyodide | CPython | Real Python for the Code node |
- Your workflowsData, AI steps and Python code, on your machine.
- Four enginesSurrealDB, ONNX Runtime Web, WebLLM, Pyodide.
- Web WorkersEach heavy engine in its own thread, off the UI.
- WASM + WebGPUCompiled engines at close to native speed, GPU kernels.
- Browser sandboxSame sandbox as any web page: no install, no system access.
Peel it apart: a sandbox, a runtime, threads, engines, then your work.Browser sandbox → WebAssembly + WebGPU → dedicated workers → four engines → workflows.
Version texte
- Your workflows / Four engines / Web Workers / WASM + WebGPU / Browser sandbox
- Your workflows: Data, AI steps and Python code, on your machine. · Four engines: SurrealDB, ONNX Runtime Web, WebLLM, Pyodide. · Web Workers: Each heavy engine in its own thread, off the UI. · WASM + WebGPU: Compiled engines at close to native speed, GPU kernels. · Browser sandbox: Same sandbox as any web page: no install, no system access.
Three engines that used to need a server
SurrealDB: a database you ship to the user
SurrealDB’s WASM build includes a storage engine that persists to IndexedDB. Using it looks like using a remote database, except the address is local:
import { Surreal } from 'surrealdb';
import { createWasmEngines } from '@surrealdb/wasm';
const db = new Surreal({ engines: { ...createWasmEngines() } });
await db.connect('indxdb://<local-db-name>');
await db.use({ namespace, database });
Two design choices worth copying:
- One long-lived connection. The local database is a singleton that stays open for the life of the app. The init promise is cached, so concurrent callers share one connection instead of racing to create two.
- Same engine, more uses. The same SurrealDB setup also backs our local knowledge bases and vector search.
This is the reason AWFlow works with no account: your workflows and history live in your browser from the first second.
Ship the engines instead. They run where you are.SurrealDB, ONNX Runtime Web, WebLLM and Pyodide run client-side as WebAssembly.
Version texte
- Your browser: Your browser · Our servers: A typical backend
- Your browser → A typical backend: your workflows and data
- A typical backend → Your browser: results
- Server to run: yes · Account needed: yes
- A typical backend → Your browser · Server to run: none · Account needed: no · Data sent: none
Two places engines can live
The classic way: the database, the AI and the code runner sit on a server.
Your data travels
Every query, prompt and script goes over the network to that server.
Results come back
The server answers, every time.
Someone runs that server
So there is a server to scale and secure, and an account to create before you start.
Ship the engines instead
Put the engines in the browser and the trip disappears: no server for us to run, no account to start, no data to send.
ONNX Runtime Web and WebLLM: two AI engines, each in a worker
- ONNX Runtime Web, through transformers.js, runs task models: classification, summarization, translation, speech-to-text, embeddings. For each task it uses WebGPU if the GPU path is known to work for that task, and WASM otherwise.
- WebLLM (from the MLC project) runs chat models. Its runtime is WebAssembly, and the heavy math runs as WebGPU kernels on the user’s graphics card.
Both run in dedicated Web Workers, so downloading or compiling a 2 GB model never freezes the UI. (TensorFlow.js is in the mix too, for a few vision and classic models, but it uses WebGPU, WebGL or CPU backends, so it isn’t one of the four WASM engines.)
The MV3 trap we hit is covered in detail in Running LLMs inside a Chrome extension: ONNX Runtime fetches its WASM files from a CDN by default, and an extension’s CSP (script-src 'self' 'wasm-unsafe-eval') forbids that. You have to ship the runtime files inside the extension and point the library at them.
Real Python, locked in a box
The Code node runs Pyodide, so the Python you write is real CPython.
The standard Python interpreter, compiled to WebAssembly so it runs inside a browser.
CPython compiled to WASM by the Pyodide project; loads in a worker and runs pure-Python code with no server.
Real Python, not a look-alike.Actual CPython, compiled to WebAssembly.
Version texte
- The Code node runs Pyodide, so the Python you write is real CPython.
- Pyodide: The standard Python interpreter, compiled to WebAssembly so it runs inside a browser. CPython compiled to WASM by the Pyodide project; loads in a worker and runs pure-Python code with no server. (CPython, WebAssembly, Web Worker)
Running user-supplied Python (possibly from a workflow someone else shared) inside your browser needs isolation, so the design is layered:
Five locks on the box
How the Python sandbox is built
- Its own worker (fait)automatisé
- A restricted js module (fait)automatisé
- Capabilities removed after boot (fait)automatisé
- A fresh interpreter per run (fait)automatisé
- Timeouts that terminate the worker (fait)automatisé
Time's up means stopped.On timeout or abort the worker is terminated, not just ignored.
Version texte
- How the Python sandbox is built
- Its own worker
- A restricted js module
- Capabilities removed after boot
- A fresh interpreter per run
- Timeouts that terminate the worker
In more detail:
- Its own worker. Python never runs in the app page. A worker has no DOM, no
chrome.*APIs and no page state. - A restricted
jsmodule. Pyodide normally exposes the host’sglobalThisto Python. We replace it with a tiny allow-list, and stub outpyodide.http. - Capabilities removed after boot. Once the interpreter has loaded, the worker’s own network, storage, messaging and nested-worker globals are deleted, as defense in depth.
- A fresh interpreter per run. Reusing one and resetting its namespace would let one run’s code hook the next run’s input. So every run gets a new interpreter.
- Timeouts that actually stop the code. On timeout or abort, the worker is terminated. We don’t just stop waiting for it.
A fresh interpreter takes about 2 seconds to boot with a warm cache (around 5 seconds cold), so we keep one pre-warmed spare: a worker that has loaded Pyodide but never run user code. Each run takes the spare and immediately starts warming the next one. An unused spare is shut down after a minute.
No boot wait, and still a fresh interpreter every run.Isolation per run is kept; an unused spare shuts down after a minute.
Version texte
- Cold boot: boot ~5 s (0–5), run (5–6)
- Warm cache: boot ~2 s (0–2), run (2–3)
- Warm spare: run (0–1), warm the next (1–3)
- no boot wait
What it costs, and what it buys
Shipping engines to the browser isn’t free. Here is what it costs us and how we handle each cost:
| Cost | How we handle it |
|---|---|
| Extensions can’t load code from a CDN | Every WASM runtime ships inside the extension |
| Engines are heavy | Each one is lazy-loaded the first time it’s used (a test checks the heavy dependencies stay out of the main bundle) |
| Models are big downloads | Downloaded once, cached, visible and deletable in a Local Models page |
| Not every machine has WebGPU | Always a WASM or CPU fallback |
And what it buys:
- No server for us to run, scale or secure, and none for users to trust.
- No account needed to start.
- No data sent: prompts, pages and workflow data can stay on the machine.
The easiest way to see the local database at work is to build a first workflow without signing in. This docs example reads the text you select on a page and shapes it into fields; it is saved in the browser’s local database the moment you create it.
Try it
Run a workflow with no account
Open it in AWFlow in a local workspace: it is stored in your browser's database, with no sign-up.
Ouvrir dans AWFlow (s’ouvre dans un nouvel onglet)docs.awflow.io/examples/first-workflow.awfCall to action. Viewers scan the code, readers click the link.Docs example: When Started → Get Selected Text → Edit Fields, stored in local SurrealDB (IndexedDB).
Version texte
- Run a workflow with no account · Open it in AWFlow in a local workspace: it is stored in your browser's database, with no sign-up. · https://docs.awflow.io/examples/first-workflow.awf
If you’re building a browser extension or a local-first web app, the takeaway is that the browser is now a credible runtime for “server” components.
Shipping engines to the browser
- A database, an inference engine and an interpreter all fit
- Put each one in a worker
- Load them lazily
- Keep a fallback
A fallback for every machine.WebGPU when available, WASM or CPU otherwise.
Version texte
- Shipping engines to the browser
- A database, an inference engine and an interpreter all fit
- Put each one in a worker
- Load them lazily
- Keep a fallback
Next in the series: how WebLLM turns your GPU into an AI server. AWFlow is free to install.



