Appy Pie's AI app builder / pre-warmed pool

Appy Pie's AI app builder counts from signup. mk0r counts from page load, because the landing page pings a pre-warmed sandbox pool.

Every article about appy pie's ai app builder describes a template filler that outputs a draft app in sixty seconds, with a signup, email capture, and a plan picker in front of it. mk0r is a different shape. On mount the landing page fires POST /api/vm/prewarm, Firebase signs you in anonymously, and by the time you finish reading the hero the server has a fully booted E2B sandbox already waiting in vm_pool.

Open mk0r and claim a warm sandbox
m
mk0r
10 min
4.8from 10K+ creators
Prewarm ping on mount, no signup gate
Firestore vm_pool, runTransaction claim
Target pool size and age configurable

The thing every Appy Pie comparison skips

Read the top results for appy pie's ai app builder. Every one describes the same loop: describe your idea, the AI picks a template, a draft app is generated in sixty seconds, Appy Pie submits it to Google Play and the App Store. Pricing, compliance badges (GDPR, HIPAA, SOC 2), an AI assistant for the builder UI.

What none of them says is what actually causes the wait for a user to start. Template assembly is cheap. The expensive thing is everything in front of it: the signup, the email confirmation, the plan picker, the template browse. The “sixty seconds” starts counting after all of that.

mk0r is not a template filler and it is not trying to be. It ships a live Vite + React + Chromium sandbox per user, and the piece that makes it feel instant is not a better AI prompt, it is a pool of sandboxes that are already booted when you arrive. The rest of this page is where that pool lives in the code.

Anchor fact

When your first prompt arrives, claimPrewarmedSession runs a Firestore runTransaction that queries vm_pool for a doc where status == "ready" and specHash == computeSpecHash(), and pops it in the same transaction with tx.delete(doc.ref). Two users cannot be handed the same sandbox.

File map: src/app/(landing)/page.tsx line 64 (prewarm ping on mount), src/core/e2b.ts line 1849 (POOL_MAX_AGE_MS = 45 minutes), line 1852 (VM_POOL_TARGET_SIZE defaults to 1), lines 1911 to 1998 (prewarmSession), lines 2054 to 2091 (claimPrewarmedSession), lines 2291 to 2336 (topupPool), lines 2339 to 2372 (getPoolStatus).

Appy Pie path vs mk0r path, on the first second of contact

Same keyword, two different interpretations of where the clock starts and what the user is waiting for.

The first second after the user clicks the product link

The link opens a marketing landing page with a signup CTA. To start building anything the user has to make an account, confirm an email, and pick a plan. The AI app builder loads after all of that. The '60 seconds to an app' clock begins after the user has described the idea in the builder UI.

  • Signup + email confirm before the builder opens
  • AI generates a template-backed draft
  • Output is a mobile app submitted to Play / App Store
  • No pre-booted runtime, no dev server for the user

The pre-warm loop, end to end

Six steps across four files. Each one is a real function call, not a marketing beat. Most of the latency savings come from step 4: by the time the user is ready to claim, the sandbox they will claim has already paid the VM boot cost, the ACP initialize, and the session/new.

1

On mount, the landing page pings the pool

The client fires POST /api/vm/prewarm from a useEffect on the home route. The response is ignored. The only thing we want is the server-side side effect of topping the pool up toward its target size.

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

Firebase signs you in anonymously

In parallel, the auth provider observes that you have no Firebase user and calls signInAnonymously(auth). You get a uid, an ID token, and you never see a sign-up form. The sessionKey in localStorage is a crypto.randomUUID().

src/components/auth-provider.tsx:68 → signInAnonymously(auth)
src/app/(landing)/page.tsx:46 → crypto.randomUUID()
3

The server tops the pool up to VM_POOL_TARGET_SIZE

topupPool() reads the current spec hash, counts ready + warming docs in vm_pool, and for each missing slot kicks off prewarmSession() fire-and-forget. Each prewarmSession boots a sandbox, runs /initialize and /session/new, sets the model to haiku, and writes a 'ready' doc.

src/core/e2b.ts:2291 (topupPool)
src/core/e2b.ts:1911 (prewarmSession)
4

Your first prompt claims a ready doc

getOrCreateSession runs claimPrewarmedSession, which is a Firestore runTransaction. It queries vm_pool where status == 'ready' AND specHash == computeSpecHash(), limit 1, and tx.delete(doc.ref) in the same transaction. No two users can claim the same sandbox.

src/core/e2b.ts:2054 (claimPrewarmedSession)
query.where("status", "==", "ready").limit(1) → tx.delete
5

The sandbox is re-initialized with your credentials

The pool was booted with the shared ANTHROPIC_API_KEY. On claim, the server POSTs /initialize to the sandbox's ACP bridge with your own apiKey or OAuth token. The bridge sees the new auth fingerprint and restarts the ACP subprocess. A fresh session/new is issued, still on the same booted VM.

src/core/e2b.ts:2104 (re-/initialize)
src/core/e2b.ts:2139 (session/new after restart)
6

The pool refills in the background

Before getOrCreateSession returns, it schedules topupPool() fire-and-forget. By the time your browser has rendered the first streamed HTML, a new sandbox is already warming for the next visitor.

src/core/e2b.ts:1159 → void topupPool().catch(() => {})

The claim, verbatim

The Firestore transaction is short on purpose. Everything else (re-init, session/new, liveness probe) happens outside the transaction, because a transaction that holds network calls open can live-lock the collection under load.

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

After the function returns, the caller (getOrCreateSession around line 1159) schedules void topupPool().catch(() => {}) so the next visitor also lands on a warm sandbox.

The numbers that pin the pool

Read directly out of the source, not invented for this page.

0Minute max age in the pool (POOL_MAX_AGE_MS)
0Default pool target size (VM_POOL_TARGET_SIZE)
0CDP port Chromium listens on inside the VM
0Vite HMR port the preview is served on

A pre-warmed sandbox is still a real VM. It holds an ACP session against Claude Haiku, a Vite dev server streaming HMR, a Chromium ready for Playwright MCP on port 9222, and the 0-minute clock before cleanupStalePool (line 1880) tears it down. The 15-minute headroom under E2B's one-hour sandbox timeout is the safety margin against handing out a sandbox that expires during someone's build.

Sources: src/core/e2b.ts line 1849 (POOL_MAX_AGE_MS), line 1852 (getPoolTargetSize & VM_POOL_TARGET_SIZE), line 161 (buildMcpServersConfig, Playwright MCP on http://127.0.0.1:9222).

What the pool looks like, live

GET /api/vm/prewarm is the admin view. It returns the state of vm_pool as the server sees it. If POOL_ADMIN_TOKEN is set, a Bearer header is required. Here's what the steady-state looks like on a quiet production box.

live pool probe

Who talks to whom on first contact

Five actors, from the first paint of the landing page to the moment the user is holding a live sandbox. The prewarm ping and the claim are on two different routes, separated by however long the user reads the hero.

From page load to a claimed sandbox

Browsermk0r APIFirestoreE2BSandbox ACPGET / (landing page)HTML + React bundlePOST /api/vm/prewarmcount ready/warming in vm_pooldeficit = target - ready - warmingSandbox.create() (fire-and-forget)POST /initialize + /session/newwrite vm_pool doc { status: 'ready' }POST /api/chat (first prompt)runTransaction: query + tx.delete(doc)claimed PoolDocPOST /initialize (user's apiKey)authFingerprint, restarted?stream: progress(pool_claim, done)

The data path, as a diagram

Three inputs on the left feed the prewarm hub. Three downstream destinations on the right. The hub itself is one Next.js route: /api/vm/prewarm.

Page-load ping, pool fill, claim

Landing page useEffect
Cloud Scheduler (optional)
First prompt
/api/vm/prewarm
Firestore vm_pool
E2B sandbox
runTransaction claim
POST /api/vm/prewarmGET /api/vm/prewarmsignInAnonymously(auth)localStorage.mk0r_session_keycrypto.randomUUID()runTransaction(vm_pool)tx.delete(doc.ref)specHash=e2b-template-…POOL_MAX_AGE_MS=45mVM_POOL_TARGET_SIZE=1ANTHROPIC_API_KEY (shared)execInVm("echo ok")

The four properties this design is built around

Claim cannot race

The Firestore runTransaction wraps both the query and the tx.delete(doc.ref). Two concurrent claims on the same ready doc will resolve with exactly one winner; the loser retries or falls through to a fresh boot.

Refill is fire-and-forget

After claim, getOrCreateSession does void topupPool().catch(() => {}). The user is not waiting on the next user's pool slot. Pool depth is eventually consistent, not on the hot path.

Spec hash guards template drift

computeSpecHash returns 'e2b-template-<ID>'. Any ready doc whose specHash does not match the current env's template is stale and cleanupStalePool kills its sandbox and deletes the doc. Deploy a new template, the old pool evaporates.

Anonymous users can claim too

signInAnonymously gives you a Firebase uid without a sign-up form. The chat and ensure-session routes verify that Firebase ID token, so the same claim path serves accounted users and anonymous-first users with one implementation.

Appy Pie's AI app builder vs mk0r

Two products answering the same keyword with different assumptions about what a user should have to do first.

FeatureAppy Piemk0r
Signup required to start buildingYes (email + plan picker)No (Firebase anonymous sign-in on mount)
What's ready when you arriveA template catalog and a prompt boxA pre-booted E2B sandbox in vm_pool
First-prompt latency floorTemplate fill after signup (~60s marketed)ACP re-init on a warm sandbox (~2s typical)
OutputNative mobile app to Play / App StoreLive Vite + React web app in a sandbox
Agent runtime after generationNone (template is static)Claude Haiku in ACP + Playwright MCP on CDP 9222
How to inspect its healthDashboard + support ticketGET /api/vm/prewarm returns pool state JSON

Open mk0r. The sandbox is already warm.

One click, no signup. The landing page has already nudged /api/vm/prewarm by the time the hero has finished rendering. Your first prompt claims one with a Firestore transaction.

Try mk0r

Frequently asked questions

What does 'pre-warmed' actually mean here, in one paragraph?

The mk0r server runs a Firestore-backed pool (collection vm_pool). Each doc in that pool is a real, already-booted E2B sandbox: Vite + React + Chromium on CDP port 9222, with an ACP session already opened against Claude Haiku. When you type your first prompt, the server pops one of those docs out of the pool with runTransaction and wires your session key to it. The wait the user sees is roughly the ACP re-init (~2 seconds), not a full VM boot (~5 seconds or more).

What's the difference vs Appy Pie's '60 seconds to an app' claim?

Appy Pie describes a template filler. You describe your idea, the AI picks a template, a first draft is generated. The 60 seconds is the draft-generation time. The user has already signed up, picked a plan, and accepted emails. mk0r's clock starts on page load, because the landing page fires POST /api/vm/prewarm on mount (src/app/(landing)/page.tsx line 64) and the server is already topping up the pool while you're still reading the hero.

Do I have to create an account to try mk0r's builder?

No. The auth provider signs you in anonymously on first visit (src/components/auth-provider.tsx line 68 calls signInAnonymously). Your sessionKey is a crypto.randomUUID() in localStorage (src/app/(landing)/page.tsx line 46). The server accepts the anonymous Firebase ID token for the ensure-session and chat routes, so you can type a prompt and watch the app build without an email.

Where is the claim actually happening in the code?

src/core/e2b.ts, function claimPrewarmedSession at line 2054. It calls db.runTransaction, queries vm_pool with status=='ready' and specHash==computeSpecHash(), limits to 1, and on hit calls tx.delete(doc.ref). Same transaction, nothing else can grab the same doc. After the transaction returns, claimPrewarmedSession re-POSTs /initialize on the VM's ACP bridge with the real user's credentials and runs a deep liveness probe (echo ok over execInVm) before handing the session back to the caller.

What happens if the pool is empty?

getOrCreateSession falls through to a fresh boot (src/core/e2b.ts lines 1170 to 1175: createSandbox, then /initialize, then /session/new). That path is slower but still works. Either way, after a claim or a fresh boot the server runs topupPool() fire-and-forget (line 1159) so the next visitor has a warm sandbox queued.

How long does a pre-warmed sandbox live in the pool?

POOL_MAX_AGE_MS at src/core/e2b.ts line 1849 is 45 * 60 * 1000, i.e., 45 minutes. cleanupStalePool (line 1880) deletes entries older than that, or entries whose specHash no longer matches the current template. E2B's per-sandbox timeout is one hour, so 45 minutes is a safety margin against handing out a sandbox that expires mid-session.

What exactly is specHash and why does the pool check it?

computeSpecHash (line 142) returns 'e2b-template-' + E2B_TEMPLATE_ID. If the production or staging template changes (the Docker image baked by docker/e2b/e2b.toml gets rebuilt), every pool doc with the old specHash is stale and cleanupStalePool tears those sandboxes down. This is the guard that keeps a pre-warmed sandbox from serving stale code to a user after a template deploy.

Can I see the pool state myself?

GET /api/vm/prewarm returns { target, ready, warming, stale, specHash, readyHost } (src/app/api/vm/prewarm/route.ts line 43 -> getPoolStatus at e2b.ts line 2339). If POOL_ADMIN_TOKEN is set as an env var the endpoint requires a Bearer header; if it isn't set the endpoint is open, because the only side effect a stranger can cause is nudging the pool toward its target size.

What runs inside a pre-warmed sandbox before my prompt arrives?

A Vite + React + TypeScript + Tailwind v4 project at /app with HMR on port 5173, a Chromium already listening on 127.0.0.1:9222 for CDP, and an ACP session initialized against Claude Haiku via the shared API key (src/core/e2b.ts lines 1939 to 1977). Playwright MCP is wired in via buildMcpServersConfig (line 153) so the first turn can already screenshot or click the live preview.

Does Appy Pie's AI app builder have anything comparable?

No. Appy Pie's output is a native mobile app submitted to Play and the App Store. There is no VM, no dev server, no pre-booted Chromium, and no anonymous path. It is a different shape of product. The fair comparison is template catalog vs. sandbox, not clock time vs. clock time.

Not a template filler. A live sandbox that is already running when you land, because the landing page pings the pool and Firestore hands you a warm one.

Open mk0r