Appy Pie alternative / Pre-warmed runtime

Every Appy Pie guide covers the template loop. None of them cover the VM that is already running before you finish typing.

Appy Pie's output is a published mobile app, so there is nothing to pre-warm. mk0r runs a real Linux sandbox for every app, and it keeps a small Firestore-backed pool of these sandboxes booted and idle. Landing on mk0r.com fires POST /api/vm/prewarm from a useEffect. By the time you click “build”, the VM is already yours.

Try mk0r, claim a warm sandbox
m
mk0r
10 min
4.8from 10K+ builders
No signup needed to claim a warm sandbox
Firestore runTransaction, atomic claim
Pool refills in the background after every claim

What every guide about this topic skips

Pull up the top articles covering Appy Pie's AI app builder. They all say the same thing. You describe an idea, the AI picks from a library of templates, a draft is generated in sixty seconds, Appy Pie submits it to Play and the App Store. Some of them list pricing tiers. A few mention HIPAA and GDPR compliance. All of them stop at “the app is built.”

None of them ask what the app runs on at build time, because the answer for Appy Pie is “nothing.” Templates assemble, metadata is filled in, a bundle is produced, and the app gets handed to Apple and Google. There is no persistent environment for the app between prompts. There is nothing to pre-warm.

mk0r is the other category. Every app you build runs inside a Linux sandbox with Vite plus React plus Tailwind plus a live HMR dev server. Because a fresh sandbox takes a few seconds to boot and another second or two to bring ACP up, the product pays that cost up front, in a background worker, and hands you a sandbox that is already warm. The rest of this page is where that lives in the code.

Anchor fact

The landing page POSTs /api/vm/prewarm from a useEffect on mount (src/app/(landing)/page.tsx:64). That route calls topupPool(), which reads vm_pool in Firestore and spawns prewarmSession() for each missing slot up to VM_POOL_TARGET_SIZE. The claim is a Firestore runTransaction with limit(1) and an atomic tx.delete.

File map: src/core/e2b.ts lines 1846 (POOL_COLLECTION), 1849 (POOL_MAX_AGE_MS), 1852 (getPoolTargetSize), 1911 (prewarmSession), 2054 (claimPrewarmedSession), 2291 (topupPool), 2339 (getPoolStatus). src/app/api/vm/prewarm/route.ts is 84 lines, POST calls topupPool(target?). src/app/(landing)/page.tsx line 64 is the one-line fire-and-forget on mount.

The four moments that make the pre-warm real

Four steps, four files. The first step happens when you load the page. The fourth happens when you type. Everything in between is a server problem, paid in advance.

1

You open mk0r.com. A useEffect fires.

The landing page mounts. One useEffect calls fetch('/api/vm/prewarm', { method: 'POST' }). It is fire-and-forget. No response handling, no UI. The server is now working while you are still reading the page.

src/app/(landing)/page.tsx:64
fetch("/api/vm/prewarm", { method: "POST" })
2

topupPool reads Firestore and counts what is ready

The route invokes topupPool. That function calls cleanupStalePool, then queries vm_pool where specHash matches the current E2B template, counts how many rows are 'ready' and 'warming', computes the deficit against VM_POOL_TARGET_SIZE, and returns the counts.

src/core/e2b.ts:2291
const deficit = Math.max(0, t - ready - warming)
3

prewarmSession boots a full sandbox, non-awaited

For each missing slot, topupPool fires prewarmSession() without awaiting. Each call creates a sandbox, POSTs /initialize with the shared API key, POSTs /session/new with the default app-builder system prompt, sets the model to haiku, provisions a git repo, and writes a PoolDoc with status 'ready'.

src/core/e2b.ts:1911 (prewarmSession)
await poolRef.set(doc) // status: "ready"
4

You type the first prompt. claimPrewarmedSession runs.

On the first real request for your session key, getOrCreateSession calls claimPrewarmedSession. It opens a Firestore runTransaction, queries vm_pool for 'ready' and matching specHash with limit(1), and deletes the winning doc atomically. You get that sandbox. Simultaneously, another topupPool kicks off in the background so the next visitor is also covered.

src/core/e2b.ts:2054 (claimPrewarmedSession)
await db.runTransaction(async (tx) => { tx.delete(doc.ref) })

The claim is a Firestore transaction with limit(1)

No queue, no pubsub, no locks. If two users arrive at the same instant and exactly one pool doc is ready, exactly one transaction commits and the other transaction sees an empty snapshot and falls through to booting a fresh sandbox. That is the whole design.

src/core/e2b.ts (excerpt, line 2054)

The re-init path checks whether the ACP bridge had to restart because the shared pre-warm credentials were swapped for the real user's. If it did, the claim only pays a single /session/new round trip, not another full sandbox boot.

topupPool is a deficit loop, not a scheduler

No cron is required for the common case. Two callers are enough: the landing page mount and every successful claim. The function below is what both of them ultimately hit.

src/core/e2b.ts (excerpt, line 2291)

Who talks to whom during a single prewarm

Five actors from the landing page mount to a ready sandbox sitting in Firestore. The ACP bridge inside the sandbox does the non-trivial work; everything else is orchestration.

One prewarm cycle, mount to ready

Clientmk0r APIFirestoreE2BACP bridgePOST /api/vm/prewarm (useEffect on mount)query vm_pool where specHash = currentcounts { ready, warming }deficit = max(0, target - ready - warming)Sandbox.create(mk0r-app-builder template)ACP bridge boots on port 3002POST /initialize (shared API key)POST /session/new (system prompt, MCP config)POST /session/set_model (haiku)set vm_pool/<id> status: readyPoolDoc persisted

The numbers that pin the design

Read directly from the repo, not invented. Every one of these is a constant or env var you can grep.

0Default pool target size (VM_POOL_TARGET_SIZE)
0Minute age cap on a pool entry (POOL_MAX_AGE_MS)
0Minute sandbox timeout (E2B_TIMEOUT_MS)
0Firestore query limit on claim (limit(1))

The pool target defaults to 0 and is capped in getPoolTargetSize at line 1852 of src/core/e2b.ts. The 45-minute age cap sits at line 0 as POOL_MAX_AGE_MS. The cap is not 60 minutes because the sandbox itself expires at 60, and you want a margin to kill a pre-warm before it becomes a surprise stale hit.

Sources: src/core/e2b.tsline 1846 (POOL_COLLECTION = "vm_pool"), line 1849 (POOL_MAX_AGE_MS = 45 * 60 * 1000), line 1852 (getPoolTargetSize reads VM_POOL_TARGET_SIZE, default 1), line 2074 (limit(1) inside the runTransaction).

The data path, as a diagram

Three producers feed the pool: the landing page mount, the post-claim refill, and the admin ping (if Cloud Scheduler is wired up). One consumer drains it: the session handler calling claimPrewarmedSession. Firestore is the rendezvous.

Producers into vm_pool, consumers out

Landing page mount
Post-claim refill
Admin / scheduler
vm_pool (Firestore)
Session handler
runTransaction
Your sandbox
POST /api/vm/prewarmtopupPool(target?)prewarmSession()claimPrewarmedSession(sessionKey, ...)vm_pool (Firestore collection)status: "ready" | "warming"specHash: e2b-template-<id>POOL_MAX_AGE_MS = 45 * 60 * 1000VM_POOL_TARGET_SIZE (env)runTransaction + limit(1)

The Appy Pie path vs the mk0r path, on one screen

Two different interpretations of the phrase “AI app builder.” The template path optimizes for shipping an app bundle to the stores. The sandbox path optimizes for iterating on a running app in a browser tab. The pre-warmed pool only makes sense on the second path.

Template output vs live sandbox runtime

You describe an idea. Appy Pie picks from a template catalog and fills in the fields. The output is a native APK and IPA submitted to Play and the App Store. There is no sandbox in the loop and nothing for a pool to keep warm.

  • No runtime per app, so nothing to pre-warm
  • First-time output: a store submission, not an editor
  • Cold boot is irrelevant because there is no boot
  • Best fit: simple business apps you plan to publish

Four design properties of the pool

The pool is a Firestore collection, not an in-memory queue

POOL_COLLECTION = 'vm_pool'. Every pool entry is a Firestore doc with status, specHash, sandboxId, host, acpUrl, and the ACP session metadata from /initialize and /session/new. The design survives Cloud Run instances being recycled because the next instance can read the same pool.

Claims are atomic by construction

runTransaction with limit(1) means exactly one claimant wins the row. Firestore handles the contention, not application code.

Stale entries self-destruct

cleanupStalePool runs at the top of every topupPool call. Anything past POOL_MAX_AGE_MS (45 minutes) or with a non-matching specHash is killed on E2B and deleted from Firestore before new slots are spawned.

The refill is fire-and-forget

prewarmSession is spawned inside a plain for loop without await, so topupPool returns immediately after counting. The caller never blocks on a boot, which matters because one of the callers is the landing-page mount.

Why this beats the Appy Pie flow for anyone who wants to iterate

If you are building a business app whose job is to live on a phone home screen, Appy Pie is a real choice. The template library is deep and submission to the stores is automated. If your goal is that exact artifact, nothing on this page is a reason to change products.

If your goal is to keep editing, testing, and re-running, the template model is a tax. Every iteration goes back through the template engine. There is no environment you can log into, no dev server to reload, no file tree to edit, no Playwright on the same machine running the app. The pre-warmed sandbox model was built around a different answer: the first prompt you type should land on a VM that is already alive.

Hence this page. Not a takedown of Appy Pie, which is fine for what it is. A description of the one architectural property nobody else explains because nobody else has it: what happens between page load and first prompt.

Want to see a warm sandbox claim live?

Book fifteen minutes. We load mk0r.com in a browser, POST to /api/vm/prewarm from a curl, and show you the Firestore row disappear the moment your session key lands.

Book a call

Frequently asked questions

What specifically is pre-warmed?

A full E2B sandbox. The prewarmSession() function in src/core/e2b.ts (line 1911) boots a fresh sandbox from the mk0r-app-builder template, POSTs /initialize to the in-VM ACP bridge with a shared API key, POSTs /session/new with the default app-builder system prompt and the Playwright MCP server config, POSTs /session/set_model with modelId 'haiku', provisions a git repo, and writes a PoolDoc with status 'ready' to the Firestore vm_pool collection. Everything that would otherwise happen during your first prompt has already happened before you typed it.

How does Appy Pie compare here?

It does not. Appy Pie generates native mobile app templates from a prompt and submits them to the Play Store and the App Store. The output is a published APK or IPA, not a live environment. There is nothing on Appy Pie's side that could be 'pre-warmed' because the product does not run a sandbox for your app at build time. The comparison only makes sense because both products advertise 'AI app builder' in their name, and that label papers over a very different architecture.

What triggers the pool topup?

Two paths. The foreground path is the landing page: src/app/(landing)/page.tsx line 64 calls fetch('/api/vm/prewarm', { method: 'POST' }) inside a useEffect on mount. That route (src/app/api/vm/prewarm/route.ts) invokes topupPool() from @/core/vm. The background path is every successful claim: after claimPrewarmedSession returns a sandbox, the session handler calls 'void topupPool().catch(() => {})' at src/core/e2b.ts line 1159 so the pool is refilled without blocking the claim response.

How big is the pool?

VM_POOL_TARGET_SIZE. Default 1, parsed and clamped to non-negative integers at src/core/e2b.ts line 1852. Pool entries are considered stale after 45 minutes (POOL_MAX_AGE_MS at line 1849) because the E2B sandbox timeout is 1 hour, and cleanupStalePool() kills the underlying sandbox and deletes the row for any entry past that age or with a non-matching specHash.

How is a warm sandbox actually claimed without two users grabbing the same one?

claimPrewarmedSession (src/core/e2b.ts line 2054) runs a Firestore runTransaction. The transaction queries vm_pool where status equals 'ready' and specHash matches, with limit(1), and deletes the matching doc inside the same transaction. The first transaction to commit wins the doc; everyone else sees an empty snapshot and falls through to booting a fresh sandbox. No locks, no queues, no race.

What happens after a claim if the credentials don't match the pre-warmed ACP session?

The claim re-runs /initialize against the pre-warmed ACP bridge with the real user's API key or OAuth token. The bridge detects the auth fingerprint change and sets 'restarted: true' in the response. When that happens, the claim path immediately POSTs /session/new again to get a fresh sessionId, then /session/set_model back to haiku. Cost is one /session/new round trip (~2 seconds), not a full sandbox boot (~5 to 7 seconds). See src/core/e2b.ts lines 2127 to 2177.

What does a reader have to believe for this angle to be real?

That the claims above are checkable in the repo. They are. src/core/e2b.ts defines POOL_COLLECTION = 'vm_pool' at line 1846, POOL_MAX_AGE_MS at line 1849, getPoolTargetSize reading VM_POOL_TARGET_SIZE at line 1852, prewarmSession at line 1911, claimPrewarmedSession at line 2054, and topupPool at line 2291. src/app/api/vm/prewarm/route.ts is the 84-line endpoint that POSTs call topupPool. The landing page fires that POST on mount.

Do I need an account for the pool to do anything?

No. The landing page client fires the prewarm POST regardless of auth state, and the mk0r app gives every first-time visitor a random UUID session key stored in localStorage (src/app/(landing)/page.tsx lines 32 to 51). An anonymous user can claim a warm sandbox just as well as a signed-in user. That is the whole point of this page's angle versus Appy Pie's signup-first flow.

What if the pool is empty when I arrive?

The session handler falls through to booting a fresh sandbox: createSandbox, /initialize, /session/new, /session/set_model, git provisioning (src/core/e2b.ts lines 1171 to 1259). That is the path every request hits on a cold start. The pool exists to make the common case fast, not to be the only path. And because topupPool is called after every claim, the next visitor in your neighborhood usually does get the warm path.

Can I inspect the pool myself?

GET /api/vm/prewarm returns the output of getPoolStatus (src/core/e2b.ts line 2339), which counts ready, warming, and stale rows in vm_pool and returns the current target size, the current specHash, and the host of the first ready row. If POOL_ADMIN_TOKEN is set on Cloud Run, requests need an Authorization: Bearer header; if unset the endpoint is open because the only side effect is spawning up to target size.

An AI app builder where the VM is already running by the time you are ready to prompt it.

Open mk0r