Why we built Forge: how we run per-user app sandboxes at AppX

E2B, Modal, Daytona, Replit, Bloom — we evaluated them all. None fit the shape of an AI app builder that needs sub-2s phone preview, long-lived Expo dev servers, and 80 containers per VPS. So we built Forge.

AppX team ·

Why we built Forge: how we run per-user app sandboxes at AppX

AppX is an AI mobile app builder. A user types "make me a habit tracker with a streak counter" and a few seconds later their phone is holding a QR code that opens the running app inside Expo Go. Edit a screen via chat, watch the change land on the phone before the user puts it back down.

That sentence is the whole product. The chat-to-code generation is the marketing video. The "your phone is already holding the live preview" is the moat. If the preview is slow, broken, or expensive, AppX is a code-gen demo with extra steps.

Behind that preview is a Docker container per user running their Expo dev server, Metro bundling React Native, a reverse proxy mapping a hostname onto that container, and a backend pushing newly-generated files in on every chat edit. We tried to buy this. We built it. The thing we built is Forge.

What we evaluated

OptionBuilt forWhy it didn't fit AppX
E2BUntrusted code, Firecracker microVMs, short-lived agent runsPer-second billing is wrong for hour-long previews; no Expo/Metro preinstalled
ModalPython, function-shaped, ML inferenceOur unit of work is a stateful long-running Node/Metro process, not a function
DaytonaDev-environments-as-a-servicePersona is a dev opening a workspace — not a thousand phones on long-lived containers
ReplitAll-in-one build + host + AIHard lock-in; AppX users export to their own GitHub and App Store
BloomWeb-targeted AI app builderDifferent stack (Convex/Bun, web); we target native mobile via Expo Go
Build it (Forge)Long-lived Expo previews, ~80 per 24GB, fractional cents/hrThe only option that matched our unit economics

None of the off-the-shelf options match the shape of what we run.

E2B

Firecracker microVMs, real isolation, sub-200ms start, per-second billing. If you run untrusted code in an enterprise agent product, E2B is correct. Two things did not fit. Per-second billing is built for short-lived agent execution — sandboxes that spin up, run a tool call, disappear. Our previews are long-lived: a user leaves the chat tab open for an hour, edits seven screens, walks away to lunch with the app still hot-reloading. Per-second pricing on a 1-hour session is the wrong unit economics. And E2B sandboxes do not come with Expo + Metro pre-installed — we would still run a warm "Expo-ready" pool on top, paying microVM overhead on top of pool overhead.

Modal

Modal is Python-first and function-shaped: decorate a function, deploy, it autoscales. The right model for ML inference and serverless endpoints. Our unit of work is a Node process. Metro is a stateful long-running server that holds an in-memory dependency graph, watches the filesystem, and serves React Native bundles to Expo Go over a custom protocol. There is no clean way to model Metro as a Modal function.

Daytona

Closest in shape: stateful sandboxes, sub-90ms start, snapshot/restore. Daytona's persona is a developer or agent opening a workspace, doing work, closing it. Ours is a thousand consumer phones each holding a QR code pointed at one long-lived container expecting sub-second hot-reload. Different shape.

Replit

Closest product: always-on previews, AI agent, ships to mobile. Hard lock-in — user code, database, deployment all live in Replit. AppX users export to their own GitHub and ship to their own App Store account; building on Replit would mean handing our compute layer to a competitor.

Bloom

Most direct AI-app-builder competitor. Different stack — Convex backend, Bun runtime, web-targeted bundling. We target native mobile through Expo Go with its own Metro protocol. Genuinely different compute shape.

Build-vs-buy verdict

At our cost ceiling — ~80 active containers on a single 24GB VPS, fractional dollars per container-hour — nothing matched the unit economics. Every sandbox-as-a-service option was charging for microVM isolation we do not need.

What Forge does

Two components: a control plane on the API node, and a node agent on every host where user containers live. Both Go, static binaries, supervised by systemd.

  user generates app
         |
         v
  +---------------+    +---------------------+
  |  API backend  |--->|  Forge control      |
  | claims warm   |    |  plane (pool size,  |
  | sandbox, push |    |  lifecycle in PG,   |
  | source files  |    |  project->host map) |
  +---------------+    +----------+----------+
                                  |
                                  v
                       +---------------------+    +-----------------+
                       | Node agent (host)   |--->|  reverse proxy  |
                       | Docker driver       |    |  hostname route |
                       | emits state webhook |    +-----------------+
                       +----------+----------+
                                  |
                                  v
                       +---------------------+
                       | Per-user container  |
                       | Expo + Metro        |
                       | shared node_modules |
                       +---------------------+

Four design decisions matter.

Pool model. Containers are pre-warmed before the user shows up. When a generation finishes, the backend claims a warm sandbox and pushes generated source files in. No wait for Docker pull, no wait for npm install. Pool size is load-aware — grows under spikes, shrinks when idle, survives node restarts via state replay.

Webhook-driven lifecycle. The node agent emits state transitions the moment they happen — provisioning -> running, running -> sleeping, sleeping -> destroyed. The control plane writes them to Postgres; the backend reads them. No polling, no drift.

{ "sandbox_id": "snd_a1b2c3", "from": "provisioning", "to": "running", "at": "2026-05-16T12:00:00Z" }

Dynamic routing. A reverse proxy maps {project-slug}.preview.appx.uz to whichever container is currently warm. Routes are added and removed as containers come and go, and survive proxy restarts.

Shared dependencies. Every Expo SDK 54 container needs the same ~800MB of node_modules. Forge mounts a shared node_modules layer from the host into each container as a read-only bind. Per-container memory drops from ~600MB to ~80-100MB. This is the single biggest reason we fit 80 containers on one box.

The numbers

  • Cold-start, pool hit: under 2 seconds from "user clicks generate" to "QR code on phone."
  • Cold-start, pool miss: ~15 seconds. Rare — pool sizing tracks recent demand.
  • Per-container memory: 80-100MB steady-state.
  • Density: ~80 active containers per 24GB VPS.
  • Hot-reload latency: sub-second from "chat edit committed" to "Metro pushes new bundle to Expo Go."
  • Per-container cost per active user-hour: fractional cents on commodity VPS hardware. We charge per AI generation credit; compute is amortized.

What this enables product-wise

The sub-2-second generate-to-phone UX is the entire pitch — what makes AppX feel like magic instead of like another tool with a 30-second loading spinner.

Long-lived previews with hot-reload mean a user can chat-edit a screen and see the change land without losing scroll position, without re-installing dependencies, without re-scanning a QR code. The feedback loop is roughly expo start on a developer's laptop — except the laptop is ours and the user only has a phone.

Dense packing on cheap hardware means gross margin on a paying user is positive from the first generation.

What we gave up by building

We own the on-call. Image registries rotate credentials, Docker daemons wedge, disks fill, containers hit OOM — we get paged. No support contract to file against. We have written runbooks, shipped reconciler loops, and built drift detection that flags mismatches between the agent's view and Postgres. The work is real.

We do not get microVM isolation. Containers share a kernel. For our threat model — our own AI-generated code in a per-user namespace, no user-submitted binaries — that is correct. If a future feature lets users run arbitrary backend code, we revisit.

We carry one DevOps surface. The buy options would have given us a zero-SSH posture; Forge nodes need SSH, systemd, monitoring, log shipping, an image registry. We accepted that because the unit economics flipped the decision on its own.

Who should NOT build their own Forge

If you are running code from strangers, use E2B. If your workload is Python-shaped, use Modal. If you are building dev-environments-as-a-service, use Daytona. If compute is incidental to your product rather than central to its value, buy compute. The build-your-own-orchestrator trade only makes sense when the orchestrator is load-bearing to the product narrative.

Closing reflection

The lesson is not "build, don't buy." It is "look hard at the shape of your workload before assuming someone else has built for it." The microVM-sandbox category is excellent at what it was designed for, and none of it was designed for a thousand long-lived Expo dev servers serving QR codes to consumer phones.

If we ever need to run untrusted code, the upgrade path is clear: keep the pool, the webhook lifecycle, and the shared-deps mount, swap the container runtime for a microVM runtime under the node agent. The control plane does not care what the agent is driving.

We wrote Forge in Go for boring reasons — static binary, no runtime to install, no GC pause long enough to matter at our scale. A node agent should be the most boring binary on the box.

The interesting work at AppX is the AI generation pipeline, the chat UX, the design system, the React Native code we generate. Forge is plumbing — plumbing the product would not exist without, but plumbing. The win condition for infrastructure like this is that nobody at AppX has to think about it on a normal Tuesday. Some Tuesdays we get there.


Try your own app idea

Describe your app in AppX →