// all posts
jun 29, 2026 · Ariel Deschapell · ~9 min

Introducing Web Actors

The browser quietly grew every primitive the actor model needs. Here is where peerd's web-actor framing is real, where it is a stretch, and why we did it in a tab instead of on a server.

For fifty years the actor model has been the cleanest answer we have to one hard question: how do you build a system out of parts that fail independently? Carl Hewitt named it in 1973. Erlang made it boring and shipped it into telephone switches that stayed up for years. The recipe never really changed. Isolate state. Talk only by passing messages. Address things by name. When a part breaks, let it crash and let something else decide what to do about it.

That recipe has always needed a runtime to enforce it, and a server to run the runtime on. The BEAM for Erlang and Elixir. The JVM for Akka. .NET for Orleans. Always a machine in a datacenter that you operate.

peerd does not have a server. It is an AI agent that runs entirely inside your browser: it drives your tabs, runs code in sandboxes, talks to other people's browsers peer to peer, and uses the model you bring (your key, your bill, nobody in the middle). While building it, I kept reaching for the same four words to describe what each piece was: isolated, message-passing, addressed, supervised. Those are the actor words. And it turns out the browser, almost by accident, has grown every primitive you need to build with them.

We call the result web actors. This is the honest version of the pitch: where the framing holds, where it is a stretch, and what it costs to do this in a browser instead of on the BEAM.

The browser grew up

An actor needs four things from its substrate. The browser now provides all four, and most of it arrived in the last three years.

  • Isolated units with private state. A Web Worker is a thread with its own heap; nothing is shared unless you explicitly send it. An opaque-origin iframe is a boundary the browser enforces at the process level. A Linux VM compiled to WebAssembly (we use CheerpX) is a whole machine. Each gets a private disk through OPFS, the origin-private file system. Share-nothing is the default here, not something you opt into.
  • Message passing. postMessage with structured clone moves data between contexts, and there are no shared pointers to corrupt by construction. Between machines, WebRTC data channels carry the same kind of opaque, signed messages.
  • Names, not addresses. WebCrypto now generates Ed25519 keys in every major browser. That gives you did:key: an identity that is a public key you can prove you hold. An actor's address becomes a key, not an IP and not a row in someone's directory.
  • A way to find each other without a middleman. WebRTC plus a tiny rendezvous (or a pasted code) is enough for two browsers to open a direct channel. After the introduction, there is no server in the path.

None of this was designed to build actor systems. It was built for video calls, offline apps, and games. But it composes into almost exactly the substrate Hewitt described.

What peerd actually does with them

Here is where I have to be careful, because the easy version of this post ("we built an actor system in the browser") is not true, and you would be right to call it out.

peerd is not an actor runtime. It is an agent harness that borrows the actor model where the model genuinely fits, and uses plainer machinery where it does not. Three places it fits well:

The sandboxes are share-nothing processes

Every Notebook (a sealed JS worker), App (an opaque-origin iframe), and WebVM (a CheerpX Linux machine) gets a private OPFS disk and a sealed global scope, and reaches the rest of the system only through messages. The agent never shares memory with the code it runs. For an agent that executes model-generated code, that isolation is not a nicety, it is the threat model: a prompt-injected script in a sandbox cannot touch your vault or another sandbox's disk, because the browser will not let it.

The peer node is a real actor

The code that runs the distributed layer takes messages in and emits messages out, with no coupling to the transport underneath. It runs over a WebRTC mesh in production and over in-memory pipes in tests, and the test harness literally describes it as "a pure actor over the mesh." You can stand up N of them in a simulator with no network and watch a gossip topic converge, deterministically. That is the actor model working exactly as advertised.

The agent is the supervisor, and this is the new shape

In OTP, a supervisor is a few lines of declarative config: which children to start, and what to do when one dies. In peerd the supervisor is the reasoning loop. It decides what to spawn (subagents, sandboxes), narrows what each child may do (a child gets a strict subset of tools, and capabilities it was not granted are stripped from its context entirely), and bounds how deep the tree may go.

I have not seen another actor system where the supervisor is a model that reasons about the tree at runtime instead of a static policy. Whether that is a good idea is a fair question. It is at least a new one.

Heavy influence, named

I want to be explicit about the debt, because none of this is original thinking. It is borrowed thinking applied to a new substrate.

From Erlang and OTP we took share-nothing as the default rather than the exception, message passing as the only way across a boundary, addressing by name, and "let it crash": a hung Notebook is not debugged in place, it is killed and its sandbox is thrown away. Disposable, isolated units are cheap to reason about precisely because you are allowed to give up on them.

From the Elixir culture we took the insistence that the boring failure path is the important one. Most of the engineering in peerd is not the happy path where the agent does something clever. It is the part where a peer drops, a sandbox runs out of memory, a model returns garbage, and the system has to stay coherent anyway.

Fault tolerance comes from isolation plus supervision, not from trying to prevent failures.

the one-sentence version of OTP, for anyone who has not written Erlang

Where the browser is not the BEAM

Now the bill. Every one of these is a real limitation, not a roadmap item I am dressing up as almost-done.

// the honest costs
  • No preemptive scheduling. The BEAM preempts every process after a couple thousand reductions, so one greedy actor cannot starve the rest. The browser does not. A tight loop in a worker just blocks that worker. We bound it with timeouts and step caps, which is a smoke detector, not a scheduler.
  • Actors are heavy. Erlang spawns millions of processes because a process costs a few hundred bytes. A Web Worker is a thread with its own heap, costing megabytes; a tab is a renderer process, costing tens of megabytes. You get dozens of live actors, not millions. Fan-out is bounded by RAM, not by imagination.
  • No supervision trees, really. peerd has depth caps, output caps, capability narrowing, and disposable sandboxes. It does not have one_for_one, automatic restart, or DeathWatch. If a child dies, the parent finds out the next time it drains, not through a supervision signal. These are guardrails, not OTP's restart strategies.
  • Messaging is best-effort and unordered between machines. WebRTC is UDP under DTLS. Gossip is flood-with-dedup. Sync is best-effort backfill, sized for demos, not set reconciliation at scale. No global order, no guaranteed delivery, no exactly-once. We are on the open internet, which promises none of that.
  • State is evictable, not durable. OPFS and IndexedDB can be evicted under memory pressure. There is no write-ahead log and no replay. "Durable" means the user exported. Cloudflare's Durable Objects persist and replay; peerd's state is a working set, not a system of record.
  • Single-threaded per context, serialized across them. Two tool calls in one context run one after another. Every cross-context message is structured-cloned. There is no handing a pointer to your neighbor.

And the framing itself is partly aspirational. The peer node is a real actor. The sandboxes are real share-nothing processes. The agent-as-supervisor is real but ad-hoc. Stretch any one of those into "peerd is an actor system" and you have outrun the code. So I am not going to.

It has been done before, on servers

To be fair to the prior art, almost every idea here has a better-engineered ancestor. The difference is always the substrate.

systemwhat it has that peerd does notwhat peerd has that it does not
Erlang / OTPpreemption, millions of cheap processes, real supervision treesno runtime to operate; it is just the browser
Akkacluster sharding, mature tooling, JVM performancepeer-to-peer with no cluster to run
Orleansvirtual actors, automatic activation, durable grain statean AI agent as the thing doing the activating
Durable Objectspersistence, replay, single-writer consistencyno platform owns your state; it is on your device
Spritely Goblinsreal object capabilities, CapTP, formal sealinga shipped browser product with a concrete transport

The honest read: peerd does not beat any of these at being an actor system. It is the first one I know of where the actor system is the browser you already have, the distribution is peer-to-peer with no relay, the identity is a key instead of a directory entry, and the supervisor is a model that reasons.

So why do it

With that bill on the table: why build it this way instead of the obvious way, a server with a real actor runtime?

Because the browser substrate buys three things a server cannot.

It runs on the machine in front of the user, with nothing to deploy and no account to create. The actor system is wherever the person is. You install an extension and it is already running.

It is private by construction. The state never leaves the device unless the user sends it. There is no backend to subpoena, breach, or bill you for, because there is no backend.

The isolation is not ours to get wrong. peerd inherits the browser's process model: the same site-isolation boundary that keeps a malicious ad off your bank tab keeps a prompt-injected sandbox off your vault. We get a decade of adversarial hardening for free, by being a tab instead of re-implementing it.

That last one is the whole reason the framing matters. For an agent that runs untrusted, model-generated code on your behalf, share-nothing isolation is not an aesthetic. It is the only shape that makes the thing safe to hand real capabilities. The actor model did not just happen to fit. It is the constraint that keeps the agent from being a liability.

Web actors

Web actors are not a framework we are selling. They are a pattern the platform backed into: isolated units with private state, talking by messages, addressed by keys, distributed peer to peer, with no server in the middle. peerd is one attempt to build something real on top of that pattern and to be honest about the seams.

The strict version, one more time: the peer node is a pure actor, the sandboxes are share-nothing processes, the agent is a supervisor that reasons, and the browser is not the BEAM and never will be. All of it is in the open. Read the docs for the precise version, or the source if you would rather not take my word for any of it.

The browser is the runtime. It turned out to be a better actor substrate than anyone meant it to be.


peerd is local-first, BYOK, Apache 2.0, no telemetry. If you build on the web-actors idea, or think the framing is wrong, I want to hear it: contact@peerd.ai.