Phone vibe coding limits, named in source
If you are reading this on a phone, you are already inside the interesting half of the question. The honest answer is much smaller than the category would have you believe. One Tailwind breakpoint changes the layout, two real-world constraints follow, and everything else runs identically to the laptop.
Direct answer (verified 2026-05-14)
Three real limits when vibe coding from a phone on mk0r.com. First, below the lg: breakpoint (1024px) the chat pane and the preview pane cannot be visible at the same time; a single-pane toggle takes over via the MobileBottomBar at src/app/(landing)/page.tsx line 1413. Second, your session lives in localStorage under mk0r_session_key and is bound to that browser until you sign in. Third, the phone keyboard is the slowest link in the loop. The model, the sandbox, the state, and the six anonymous turns are identical on phone and laptop.
The whole phone delta is one Tailwind breakpoint
Every page that talks about vibe coding on mobile treats the phone as a different product. It is not. On mk0r the phone path is the same site that the laptop loads. Same URL, same React tree, same Next.js bundle. The thing that changes is the CSS.
The chat column has lg:flex lg:w-[var(--sidebar-w)] in its class list (page.tsx line 623). Above 1024px wide that applies, and the chat is a fixed-width column on the left. The preview is its right-hand sibling. Below 1024px, neither lg: rule fires, so both panes fall back to mobilePane === "chat" ? "flex" : "hidden" (lines 624 and 756). One is visible, the other is hidden, and a state hook at line 147 decides which.
The toggle UI is a real component at line 1413 named MobileBottomBar, rendered with lg:hidden so it disappears on laptops. Every tap on Chat or Preview fires a PostHog event:
posthog?.capture("mobile_pane_switched", {
from: mobilePane,
to: p,
});That one event is the metric the team watches to know whether phone users are actually using the loop or bouncing after the first preview. If you are on a phone right now and have ever tapped from chat to preview, that data point has your anonymous session id on it.
What the layout does on either side of 1024px
Chat column on the left at a fixed sidebar width, a 3px cursor-col-resize bar, and the live preview iframe on the right. You read the streamed model response and watch the iframe repaint in the same eyeful. The preview defaults to desktop mode (no 390x844 frame). The MobileBottomBar is in the React tree but lg:hidden keeps it off screen.
- Chat and preview side by side
- Resize handle between them
- MobileBottomBar present in DOM, hidden
- No mobile_pane_switched events fire
The two limits that are not the breakpoint
If the layout collapse were the only thing, vibe coding on a phone would be a non-event. There are two more constraints, both real, both narrow.
Your session is bound to that browser until you sign in
The session key is one localStorage entry, mk0r_session_key, set on src/app/(landing)/page.tsx line 33. An anonymous Firebase uid is paired to that key, and both live in your phone's browser storage. The laptop browser has its own anonymous uid and its own key. They do not see each other's projects until you sign in with Google, at which point POST /api/auth/migrate rewrites every Firestore document from the old anonymous uid to your signed-in uid in a batch. After that one round trip, your phone session and your laptop session are the same session.
The practical version: if you have been tinkering on your phone all weekend and want to keep going on a laptop on Monday, sign in first. The historyStack, every git SHA, the published domain, and the residential-IP toggle all migrate in the same batch.
The phone keyboard is the slow link in the loop
This is the one thing the code cannot solve. The model streams in seconds. The preview HMR-repaints in under 800ms (HMR_WAIT_MS in src/components/phone-preview.tsx line 32). Reading the diff takes a few seconds. The slow link, on a phone, is the next prompt you have to type with your thumbs.
The honest move is to keep each prompt to one specific change. “Smaller header.” “Add an undo button to the card.” “Make the buttons round.” Voice dictation also works. So does drafting the next prompt in a notes app and pasting it into the chat. None of these are mk0r tricks; they are general phone-keyboard tricks. The product just happens to be one of the places where they matter.
A full phone session, top to bottom
Open mk0r.com on your phone
Anonymous session UUID written to localStorage on first paint. No account form, no app install, no QR code. Tap the URL bar and start typing the first prompt.
The layout swaps under 1024px
Tailwind's lg: prefix flips. The side-by-side workspace becomes a single-pane stack. Chat is on top by default, the MobileBottomBar (page.tsx line 1413) renders below the home indicator.
Type one sentence, send
Claude Haiku 4.5 starts streaming. The model and the six-turn anonymous cap are identical to the laptop path. The progress is visible in the chat pane while you wait.
Tap Preview to see the result
The bottom-bar Preview button fires PostHog mobile_pane_switched (page.tsx line 997) with from="chat" to="preview". The iframe at 390x844 is now full-width on your phone, which is roughly 1:1 with your real device.
Background the tab to read a text
The preview iframe clears its src to stop HMR battery burn. The E2B sandbox does not pause; that ceiling is one hour of idle time, server-side. Switch back and the iframe reloads.
Sign in to keep the session on every device
POST /api/auth/migrate rewrites every app_sessions and projects doc from the anonymous uid to your signed-in uid. After that, the same session opens on your laptop with the historyStack intact.
What is identical on phone and laptop
Worth saying clearly because most articles on this topic imply the opposite. The interesting half of vibe coding on mk0r (the model, the sandbox, the verification loop, the services) does not change on a phone. Each card below traces to a file and a line.
The model
Claude Haiku 4.5, pinned in src/app/api/chat/model/route.ts line 5 as FREE_MODEL. Same on phone, same on laptop.
The sandbox
Same E2B template, same Vite + React + Tailwind v4 project at /app, same dev server on port 5173 with HMR.
The anonymous turn cap
ANON_TURN_LIMIT = 6 at src/app/api/chat/route.ts line 23. Six free turns whether you opened the tab on iOS Safari or on Chrome desktop.
The idle ceiling
E2B_TIMEOUT_MS = 3_600_000 ms at src/core/e2b.ts line 33. One hour of inactivity pauses the sandbox; reconnect is automatic on the next prompt.
The undo
git checkout sha -- . inside the VM. Driven by undoTurn in src/core/e2b.ts line 1731. Every turn is a real git commit on every device.
The publish flow
/api/publish maps your app to <your-subdomain>.mk0r.com via the same wildcard A record. The deployed app does not care which device built it.
Where the phone path actually loses to the laptop
One honest place. Multi-screen apps where the screens depend on each other. The whole reason a side-by-side layout exists on a laptop is that vibe coding is a tight loop between “say what you want” and “look at what happened.” When the app has one screen, you can toggle between chat and preview on a phone and the loop stays tight. When the app has three screens and a flow between them, you cannot hold both ends of the loop in your head at once on a phone. You start tapping back and forth, you lose the thread, and the prompts get vaguer.
The fix is not on mk0r's side. The fix is to keep the phone-built apps single-screen and to migrate to a laptop when the second screen shows up. Tight scope wins on a phone the same way it wins on a laptop, only more so.
None of this is a hard wall. You can build a multi-screen app on a phone. It is just slower per iteration than the same work on a laptop, and the friction compounds. A vacation weekend on the phone, a Monday morning on the laptop, sign in once between them, is a fine pattern.
Want to vibe code your idea while we walk through it?
Twenty minutes, you open mk0r.com on whatever device is in your hand, I show you the first six turns on the specific thing you are trying to build. Phone is fine.
Frequently asked questions
What is the single biggest thing that changes when I open mk0r on a phone?
One CSS breakpoint. Tailwind's lg: prefix fires at 1024px. Above that width, the chat pane and the live preview sit side by side and the resize handle between them is a real cursor-col-resize bar. Below that width, the two panes collide and only one can be visible at a time. The MobileBottomBar at src/app/(landing)/page.tsx:1413 is rendered with lg:hidden and gives you a Chat / Preview toggle. Every tap on that toggle fires a PostHog event named mobile_pane_switched with from and to fields (page.tsx line 997). That is the entire phone UX delta.
Why a single-pane toggle and not a clever split screen?
Because a phone in portrait is roughly 390x844, and the live preview itself targets 390x844 (DEVICE_SIZE.mobile in src/components/phone-preview.tsx line 9). If you try to render both the chat column and a 390-wide preview on a 390-wide phone, neither one is usable. A horizontal split would put each pane around 195 pixels wide, which is below the floor for reading streamed model output. A toggle keeps the chat at full width while you type, and the preview at full width while you check what changed. It is the same trick most messaging apps use when they show inline media.
Does my mk0r session follow me from my laptop to my phone?
Only if you sign in. The session key lives in localStorage under mk0r_session_key (src/app/(landing)/page.tsx line 33). Anonymous Firebase uids are bound to a single browser, so the projects you spun up on your laptop without signing in are not visible on your phone, and vice versa. After you sign in with Google on the second device, POST /api/auth/migrate calls migrateSessionOwnership and rewrites every app_sessions and projects doc from the old anonymous uid to the new signed-in uid in a Firestore batch. After that one round trip, the historyStack, the published domain, the residential-IP toggle, and everything else moves with you.
Does anything about the model or the sandbox change on a phone?
Nothing. The model is Claude Haiku 4.5 for anonymous sessions on every device (FREE_MODEL = "haiku" at src/app/api/chat/model/route.ts line 5). The anonymous turn cap is six on every device (ANON_TURN_LIMIT = 6 at src/app/api/chat/route.ts line 23). The sandbox is the same E2B template with Vite + React + Tailwind v4 at /app and Playwright MCP attached. The one-hour idle pause is the same constant (E2B_TIMEOUT_MS = 3_600_000 at src/core/e2b.ts line 33). Your phone session and your laptop session, opened side by side, run identically to the byte.
What about the keyboard, is that a real limit?
Yes, and it is the one phone-only limit that the code cannot fix. Typing a useful prompt on a phone keyboard is the slow part of the loop. The agent's turn finishes in seconds, the preview HMR-repaints in under 800ms (HMR_WAIT_MS in src/components/phone-preview.tsx line 32), and your eyes scan the diff in another two seconds. The bottleneck is the human side. Voice dictation helps a lot here. So does writing the next prompt in a notes app and pasting it in. So does keeping prompts to a single noun-phrase change like "smaller header" or "add an undo button to the card". The product cannot make you a faster phone typist, but it does not need to.
Does the phone preview pause when I background the tab to check a message?
Sort of. The iframe sets paused via the AppPreview prop in src/components/phone-preview.tsx and the iframe's src clears when paused flips to true, which kills the HMR WebSocket so the parent tab does not burn battery on a backgrounded preview. When you come back, the iframe reloads and reconnects. The deeper layer (the sandbox at E2B) does not pause on a backgrounded tab. It pauses on the idle ceiling, E2B_TIMEOUT_MS, which is one hour and runs server-side. So a quick switch to your inbox and back loses nothing visible. A two-hour gap pauses the sandbox, and ensureSessionLoaded reconnects you byte-exact on your next prompt.
iOS safe-area and the notch, does mk0r handle it on a phone?
Yes. The MobileBottomBar wrapper at src/app/(landing)/page.tsx line 1440 sets paddingBottom: env(safe-area-inset-bottom) so the bottom row of Chat / Preview / Versions / Schedule / History buttons sits above the home indicator on iPhones that have one. Without that one CSS line, the bottom row would render under the home gesture area on iOS Safari and the Schedule button would be impossible to tap. The rest of the chrome uses min-h-dvh for the viewport so the iOS dynamic toolbar does not clip the chat input when the keyboard pops up.
Can I share an app I built on my phone the same way I share one built on a laptop?
Yes, identically. The publish flow lives at /api/publish and writes a domain mapping that is independent of which device built the app. The link looks like https://<your-subdomain>.mk0r.com whether you built the app on a phone or a laptop. The wildcard A record *.mk0r.com points at the same Cloud Run load balancer for both, so the deployed app loads on every device, not just the one you built it on.
If the only UX change below 1024px is one breakpoint, why does it feel different on a phone?
Because of the rest of the operating environment, not mk0r. Your phone has tighter battery rules that throttle background JavaScript more aggressively. iOS Safari has stricter content-blocking. Notifications interrupt you. The clipboard handoff between apps is one extra tap. mk0r itself is the same site, but the context around the site is different. The honest framing is: the limits are on the device, not on the product. A laptop with a bad mouse is a worse vibe coding experience than a phone in good light.
Where does the phone-only path actually break down?
When the app you are building has more than one screen and the screens depend on each other. Reading two parallel screens is what the chat-plus-preview side-by-side layout is for, and the phone cannot do that. The honest move on a phone is to keep the scope tight: one screen, one job, iterate by sentence. If the app you are vibing toward is a multi-screen flow with state moving between routes, the loop is faster on a laptop. Not because the engine changes, but because you can see both the prompt and the rendered screen at the same time. That visibility is what the breakpoint takes away.
Open the site from your phone. The breakpoint and the single-pane toggle will be the first thing you see, and the first prompt will not need an account.
Start a session
Comments (••)
Leave a comment to see what others are saying.Public and anonymous. No signup.