Field guide

Vibe coding state, auth, and iteration limits, traced through one first-prompt flow

When people on X talk about the limits of vibe coding, they usually name three things in the same breath: state evaporates on a refresh, auth blocks the first prompt, and iteration drifts after turn fifteen. They are three independent failure modes with three independent mechanisms. Most guides treat them separately. On mk0r they get dissolved together, inside one flow that runs the moment you type your first prompt. Here is the trace, with file paths.

M
Matthew Diakonov
8 min
Direct answer (verified 2026-04-30)

State, auth, and iteration are three independent vibe coding limits. State is where the project lives between sessions; if it lives in browser memory, a refresh kills it. Auth is whether you can start prompting at all; a signup form before turn one is a 60-to-120-second deadweight on the prototyping loop. Iteration is whether prompt fifteen still helps; if the builder re-rolls the project from a saved transcript on every turn, the file structure drifts and undo stops being byte-exact. mk0r addresses all three on first prompt: anonymous Firebase uid (auth), parallel Neon and Resend and GitHub provisioning via Promise.allSettled (state), and a persistent ACP session inside an E2B sandbox with per-turn commitTurn and byte-exact undoTurn (iteration). Source linked throughout this page.

The three limits, named separately

A useful first move is to stop calling these “the wall” and start calling them by their distinct names. They compound when they all hit, but they have different fixes.

What each limit actually looks like in practice

  • State limit: you tab away, your phone backgrounds, the dev server idles, and the project disappears or rolls back to an earlier draft.
  • Auth limit: a signup form, an email verification link, or a password reset blocks the very first prompt and breaks the maker loop.
  • Iteration limit: turn fifteen rewrites things you did not ask it to touch, undo gives you a version close to but not equal to last turn, the agent forgets a correction you made three turns ago.

Most articles you can find on this topic recommend a generic hardening checklist: add rate limits, validate inputs, log errors, require sign-in for sensitive paths. That advice is right for production, but it does not actually map onto the prototyping loop. The prototyping loop wants the limits gone before turn one, and the shape of how they get removed matters more than the fact that they do.

The state limit, removed by parallel provisioning

Most AI app builders write your project into browser localStorage or an in-memory blob. That is fine until the tab refreshes. Then it is not. The fix is to put the project on a real filesystem with a real git repo, and to make sure that filesystem has the supporting services it needs (a database, a transactional email provider, a remote git origin) before the agent ever touches it.

On first prompt, mk0r calls provisionServices(sessionKey) in src/core/service-provisioning.ts. Inside that function, Promise.allSettled fires three real API calls at three real services in parallel. None of them block the others. None of them block the prompt for very long.

provisionServices(): one anonymous uid in, four real services out

Anonymous Firebase uid
Session key
First prompt
provisionServices()
Neon Postgres 17
Resend audience + key
Private GitHub repo
PostHog appId

By the time the agent boots, a /app/.env file is written by buildEnvFileContent(envVars) and contains DATABASE_URL with a real Neon connection URI, RESEND_API_KEY plus RESEND_AUDIENCE_ID, GITHUB_REPO_URL, and the PostHog keys for per-app event isolation. You did not paste any of those values. The state limit is removed not by adding a database button but by handing the project a complete environment before it asks for one.

Once that happens, “state” means a real disk. A backgrounded phone, a closed laptop, a Cloud Run restart, none of it loses your project. When you reattach, the session key in localStorage points back at the same sandbox; if the sandbox itself was paused or reaped, the project is restored to the head SHA from Firestore.

The auth limit, deferred to publish-time

Most writing on auth limits is about operations: how to throttle login attempts, how to cap OTP emails, how to configure your IdP to block credential stuffing. Those are real concerns, but they bite you after you ship. For an AI app prototype, the auth limit that actually blocks progress is the one on turn one: the signup form, the verification email, the second password prompt after Google refused your first one. Each of those is a 15-to-120-second gap between “I want to try this” and “I am typing a prompt.” Prototyping loops die at that latency.

The mk0r approach is anonymous-first. The landing page runs Firebase signInAnonymously on mount through onAuthStateChanged in src/components/auth-provider.tsx. If the page loads and there is no Firebase user, the provider quietly creates one. That anonymous uid owns your app_sessions doc, pairs with the E2B sandbox you get on your first prompt, and owns the private GitHub repo provisioning created. You never see a login screen.

The actual sign-in gate is enforced lazily, through requireAuth(action) on AuthContext. Roughly: if the user is already non-anonymous, run the action immediately, otherwise stash the action in a ref, open the sign-in modal, fire the action only after the Google popup resolves. Publishing to a custom domain calls requireAuth. Sending a prompt does not. The gate is drawn around the actions that cost money to run, not around the path that lets you build.

When the gate does fire, the migration path handles the awkward case where your anonymous uid bumps into an existing Google account. migrateSessionOwnership in src/lib/auth-server.ts opens a single Firestore batch, rewrites every app_sessions and projects doc with the new uid, and commits. Your sandbox and your git history survive the identity swap.

The iteration limit, defused by treating turns as commits

The iteration limit is the spookiest of the three because it shows up gradually. Turns one through ten feel great. Around turn fifteen the agent starts rewriting parts of the layout you did not touch. Undo brings something close to the previous version, but a couple of small things have shifted. By turn twenty-five you cannot trust the tool anymore.

The two underlying causes are usually (1) the builder is regenerating from a saved transcript every turn rather than editing in place, and (2) undo is implemented as a re-prompt rather than as a real revert. mk0r treats turns as commits and undo as checkout. Each prompt is handed to the same persistent ACP session, so the agent edits the working directory rather than producing the project again. Each successful turn writes a real git commit through commitTurn (src/core/e2b.ts:1635). Undo runs git checkout <sha> -- . via undoTurn (src/core/e2b.ts:1731). The disk state after undo is byte-exact to the previous turn. The model is not consulted. Zero token cost, zero variance.

One iteration turn, end to end

YouServerACP sessionVM /appPOST /api/chat (prompt)send to same sessionIdedit files in /apptool resultsstream text + tool callscommitTurn() git add+commitnew SHAstream done, history updated

And when you undo three times to try a different idea, the slice at src/core/e2b.ts:1676 forks the history. If the active pointer is anywhere except the tail when a new commit lands, drop everything past it. This is how editors handle undo. It is not how most AI builders handle it, which is why iteration starts feeling weird around turn fifteen. The slice is one line.

The full first-prompt timeline

Here is the whole sequence, top to bottom. Each step is the mechanism that retires one of the three limits. The order is what matters; if any of these moves later in the flow, the limits start biting again.

1

Page paints, anonymous Firebase uid created

AuthProvider runs signInAnonymously through onAuthStateChanged in src/components/auth-provider.tsx. You are a real Firebase user before you have seen a sign-in screen. This retires the auth limit for turn one.

2

First prompt POSTs /api/chat with the session key

The session key in localStorage is what links the anonymous uid to a sandbox. If a sandbox does not yet exist for that key, the next step provisions one.

3

provisionServices() fires Neon, Resend, GitHub in parallel

src/core/service-provisioning.ts runs Promise.allSettled across the three external API calls. PostHog runs synchronously. /app/.env is written into the sandbox before the agent boots. This retires the state limit before the project exists.

4

ACP session boots inside the E2B sandbox

The agent runs inside one persistent session for the lifetime of the sandbox. Same sessionId across every turn, edits land on the working directory at /app rather than producing the project again. Session reuse logic is in src/core/e2b.ts around line 909.

5

First turn writes a real git commit

commitTurn (src/core/e2b.ts:1635) runs git add -A, git commit, git rev-parse HEAD. The resulting SHA is pushed onto session.historyStack and persisted to Firestore. This retires the iteration limit by making turns first-class objects.

6

Undo means git checkout, not re-prompt

undoTurn (src/core/e2b.ts:1731) runs git checkout <sha> -- . inside the sandbox and writes a new commit with --allow-empty. Disk state is byte-exact to the prior turn. The model is not consulted on undo.

7

Sign-in is deferred to publish, not required for iteration

requireAuth(action) in AuthContext enforces the gate only on actions that cost money: publishing to a custom domain, switching Claude models, etc. Sending a prompt and editing a project do not require sign-in.

The limit that stays a limit

None of this makes vibe coding magic. The thing that still has a ceiling is the envelope. mk0r generates working HTML, CSS, and JS or a Vite plus React plus Tailwind project. The iteration model holds up well within that envelope. It does not generate native iOS or Android, multi-user real-time state, payments with serious data invariants, or backend logic more complex than a single Postgres and a couple of Resend audiences can cover.

So the honest exit from vibe coding is when the requirements outgrow the envelope, not when iteration starts feeling slow. If iteration is the only thing breaking down, the fix is mechanical: clearer prompts, smaller scope per turn, undo to a known-good SHA, ask the agent to re-read its memories. If the envelope itself is wrong, no tool fixes that for you, and that is when bringing in code and developers is the right move.

Calling out that ceiling in public is more useful than pretending it does not exist. Most posts on this topic oversell. The point of this one is that three classic limits do not need to live in three separate complaints, but the fourth (the envelope) still has to.

Want help shipping a vibe-coded app past the envelope?

Bring the prototype, the stuck point, and the requirements. We will look at whether iteration alone gets you there or whether the envelope is the wrong shape.

Frequently asked questions

Are state, auth, and iteration the same problem or three different problems?

Three different problems with three different mechanisms, even though they get bundled together when people complain. State is about where the project actually lives between sessions; if it lives in browser memory, a refresh loses it. Auth is about whether you can start prompting at all; if there is a signup wall, you cannot. Iteration is about whether prompt fifteen still produces useful changes; if the tool re-rolls the project from a transcript every turn, you hit a drift wall. They feel like one wall because they tend to compound, but each has its own root cause and its own fix.

How does mk0r remove the auth limit before a prototype starts?

On first visit, the AuthProvider calls Firebase signInAnonymously through onAuthStateChanged in src/components/auth-provider.tsx. By the time the landing page paints, you already have a real Firebase uid that owns an app_sessions doc in Firestore. There is no signup form, no verification email, no password manager dance. The first thing you do on the site is type a prompt, not fill in a form. The actual sign-in gate is drawn around publish, model switching, and a few other monetized actions through requireAuth(action) on AuthContext, which is roughly four lines: if the user exists and is not anonymous, run the action immediately, otherwise queue the action, open the sign-in modal, and only fire the action after Google popup resolves.

What about state, where does the project actually live during iteration?

On a real filesystem inside an isolated E2B sandbox at /app, with a real git repo. None of the project state lives in browser memory or a database serialized blob. On the very first prompt, the server calls provisionServices(sessionKey) in src/core/service-provisioning.ts; that function uses Promise.allSettled to fire three external API calls in parallel: a Neon Postgres 17 project in aws-us-east-2 under org-steep-sunset-62973058, a Resend audience plus a sending-only restricted API key, and a private GitHub repo at m13v/mk0r-{slug}. PostHog runs synchronously and gives you a per-app appId. A /app/.env file is written into the sandbox before the agent boots. None of those values were ever pasted by you.

What does iteration look like once provisioning is done?

Each prompt is handed to a persistent ACP session that lives for the lifetime of the sandbox. Same sessionId across every turn, so the agent edits files in /app rather than producing the project again from scratch. The session reuse logic is in src/core/e2b.ts around line 909. Every successful turn writes a real commit through commitTurn at src/core/e2b.ts:1635: git add -A, git commit, git rev-parse HEAD, then push the resulting SHA onto session.historyStack and persist to Firestore. A tab refresh, a phone backgrounding, or a Cloud Run restart cannot desync the project from what the model thinks exists, because the model is reading from the same disk that just got committed.

Why is undo so unreliable in most AI builders, and what does mk0r do differently?

Most builders implement undo as a re-prompt. They drop the last user message from the saved transcript, replay the older prompts, and ask the model to produce the project again. The model is non-deterministic, so the 'undone' project is not the project that existed before. File structure shifts, components reappear, sometimes new bugs arrive. mk0r implements undo as git checkout. The undoTurn function at src/core/e2b.ts:1731 reads historyStack[activeIndex - 1], runs git checkout <sha> -- . inside the sandbox, stages, and writes a new commit with --allow-empty. The disk state is byte-exact to the previous turn. The model is not consulted. Zero token cost, zero variance.

What if I undo three times and try a different idea, does the abandoned future come back to bite me?

No. mk0r forks the history. The slice at src/core/e2b.ts:1676 reads historyStack = historyStack.slice(0, activeIndex + 1) before pushing a new SHA. If the active pointer is anywhere except the tail when a new commit lands, drop everything past it. This is how editors handle undo. Most AI builders keep both branches alive with no clear current pointer, which is why iteration starts feeling weird around turn fifteen. The slice is one line. It is the difference between trying a new direction and being scared to branch.

Where is the limit that stays a limit, even on mk0r?

The envelope. mk0r generates working HTML, CSS, and JS or a Vite plus React plus Tailwind project. The iteration model holds up well within that envelope. The honest exit point is when the requirements outgrow the envelope: native iOS or Android, multi-user real-time state, payments with serious data invariants, complex backend logic that generated TypeScript can't cover. If iteration is the only thing breaking down, the fix is usually mechanical (clearer prompts, smaller scope per turn, undo to a known-good SHA) and you should keep going. If the envelope itself is wrong, no amount of vibe coding fixes it, and that is when you bring in a developer or write code yourself.

What happens if my anonymous uid eventually signs in with a Google account that already exists?

This is the interesting edge case for the auth side of things. The default path is Firebase linkWithPopup, which merges the anonymous identity with the Google credential and your anonymous uid just gains an email. But if that Google account has signed in before, Firebase throws auth/credential-already-in-use and refuses to link. The client catches that error, pulls the OAuth credential out with GoogleAuthProvider.credentialFromError, signs in with signInWithCredential, and POSTs /api/auth/migrate. The server runs migrateSessionOwnership in src/lib/auth-server.ts, which opens a single Firestore batch, updates every app_sessions doc where userId equals the old uid, updates every projects doc where ownerUid equals the old uid, and commits. Your sandbox, your prompts, your git history, and your published domain all survive the identity swap.

Do I need to sign up to test any of this?

No. Open mk0r.com, type a prompt, and you are an anonymous Firebase user. The session key lives in localStorage, the sandbox is provisioned on first prompt, every turn writes a commit to a private repo provisioned to that anonymous identity. Sign in later if you want to publish to a custom domain or move work to another device; before then, your project is reachable from the same browser as long as you do not clear storage. The whole iteration loop runs without an account.

mk0r.AI app builder
© 2026 mk0r. All rights reserved.