2026.07.11

making previews work like Lovable is harder than it looks

the impressive part is not putting generated code in an iframe. it is making the preview boot, stay isolated, pass checks, route correctly and fail in a way the user can recover from.

one of the things people notice quickly in tools like Lovable is the preview.

you type what you want. the system generates an app. a few moments later, there is a real thing on the right side of the screen. not a screenshot. not a fake mockup. an actual running interface you can click, inspect, refine and slowly argue with until it becomes useful.

from the outside, that looks like a frontend feature.

inside the system, it is closer to infrastructure.

i have been involved in building a vibecoding platform around this same kind of loop: prompt or design goes in, code comes out, the generated app runs somewhere safe, the user sees it, then the next instruction can refine it. the preview is not decoration around that loop. the preview is the place where the loop becomes honest.

if the code cannot run, generation has not really succeeded.

an iframe is not a preview system

the shallow version of this feature is easy to imagine:

  1. generate files;
  2. run npm install;
  3. run npm run dev;
  4. put the URL in an iframe.

that is a demo, not a product.

the product version has to answer worse questions.

where does the code run? how do you stop one user's generated project from touching another user's generated project? what happens when the generated app has no package.json? what happens when index.html is under the wrong folder? what if the dev server starts but the page is blank? what if the page works on desktop and breaks on mobile? what if npm install takes too long? what if the user edits the prompt again while the preview is still booting?

this is where the feature stops being cute.

the preview has to be isolated

generated code is still code.

even when the user did not intend anything malicious, the output can be messy. it can include strange dependencies, wrong scripts, unexpected file paths, broken imports, or an app that starts one process and leaves another one hanging. if the system treats generated code like trusted application code, it is already in trouble.

for this platform, the preview path is built around Docker-isolated runtime sessions. the generated project gets written into a workspace, then a preview runner starts it with resource limits and a known entrypoint.

the important idea is not "Docker because Docker sounds serious." the important idea is that every generated app needs a boundary.

text
project files -> preview workspace -> isolated runtime -> health check -> QA checks -> public preview route

that boundary gives the platform room to control CPU, memory, process limits, ports, networks and cleanup. without it, every preview is just another small production incident waiting to happen.

the runtime must understand ugly output

LLMs do not always produce projects the way a human senior engineer would.

sometimes the package.json is under src/. sometimes the Vite entry file is missing. sometimes there is an index.html, but it is not at the root. sometimes the app is basically static HTML. sometimes the generated code has a React component but no proper boot file.

you can respond to that by telling the model to be better.

that helps, but it is not enough.

the system needs a preview readiness layer before it spends time installing dependencies or starting containers. cheap static checks should catch the obvious failures:

  • is there a runnable app shape?
  • is package.json at the root?
  • is there a root index.html for Vite?
  • does a React app have an entry file?
  • is this a static HTML project instead of a Vite app?

this layer is not glamorous, but it saves time and gives better errors. "preview failed" is almost useless. "package.json must be at the project root" is something the system can repair or the user can understand.

booting is only half the job

a dev server can be alive and still be wrong.

it can return 200 and show a blank page. it can load on desktop and overflow horizontally on mobile. it can have broken images. it can print enough console errors that the user will be debugging the generator instead of their idea.

so the preview cannot stop at "port is open."

in this project, the preview gate runs browser checks after the server starts. Playwright opens the preview across mobile, tablet and desktop viewports, captures screenshots, watches console errors, checks for blank pages, broken images and layout overflow. if Playwright is not available, the system still has a lower-level HTTP fallback so the pipeline does not become blind.

that is the difference between a live preview and a useful preview.

a live preview says: something responded.

a useful preview says: the generated app looks like it can actually be shown to the user.

routing is where the small lies appear

local preview URLs are not browser-safe production URLs.

inside the API, a preview might be reachable as a Docker network hostname or a host-bound port. inside the user's browser, 127.0.0.1 means the user's machine, not the VPS. if you leak an internal preview URL into an iframe, everything may work in development and then collapse in production.

this is why the public preview route has to be a first-class design decision.

one option is a same-origin embed path:

text
nsisong.com/preview/embed/{projectId}/

another option is a wildcard preview host:

text
{projectId}.preview.nsisong.com

both approaches need careful reverse proxying, auth handling, WebSocket support and timeouts. the preview server may be Vite, and Vite wants HMR. HMR wants WebSockets. WebSockets want proxy headers to be correct. suddenly the iframe feature is negotiating with Nginx, TLS, cookies and browser origin rules.

this is the part nobody puts in the marketing screenshot.

HMR changes the product feel

the first preview can take time because the system has to prepare files, install dependencies, start a runtime and check the page.

but the second change should not feel like the first boot all over again.

if a user says "make the button bigger" or "change this to a dark dashboard," rebuilding the whole container from nothing every time is technically simpler and experientially worse. it makes the product feel like it is thinking deeply about a CSS class.

so the preview system needs to know when to reuse a live session. if the previous container is still healthy, patch the workspace and let HMR do its job. if the session is stale or hibernated, wake it up with the preserved workspace where possible. if the attempt fails, then rebuild cleanly.

the architecture becomes more stateful, but the product becomes calmer.

there is always a tradeoff here. reuse makes previews faster, but it also means you must track session status, container IDs, workspace paths, ports and whether the running process is actually still reachable. speed is not free. it moves complexity into lifecycle management.

failed previews should produce evidence

the worst possible preview failure is the silent one.

the user waits. the spinner stops. nothing works. maybe there is an error somewhere in the server logs, maybe the generated app printed something in the browser console, maybe the container never started. nobody knows.

for a system like this, failure needs artifacts:

  • the preview session status;
  • the user-facing error message;
  • the underlying raw error for developers;
  • console errors;
  • screenshots;
  • QA issues;
  • logs from the workspace or runtime;
  • whether the failure happened before boot, during install, during health check or during visual QA.

this is not only for debugging. it is part of product trust.

if a generated app fails, the next useful action depends on where it failed. a missing entry file is a generation repair problem. a broken image may be an asset problem. a blank page may be a runtime or import problem. a port timeout may be infrastructure.

when all of these collapse into "preview failed", the platform cannot improve itself.

repair loops need boundaries

it is tempting to make the system retry forever.

preview fails, send the errors back to the model, patch the files, run it again. if it fails again, repeat. this feels elegant until the system spends five minutes trying to save a fundamentally broken project while the user is stuck watching.

the better model is a bounded repair loop.

try to boot. run checks. if the failure is repairable, apply a targeted fix and retry. but cap the attempts. after that, preserve the evidence and fail honestly.

this matters because AI product engineering can very quickly become theatrical. the system looks busy, but it is not making progress. a senior system should know when to stop pretending and give the next layer something concrete.

the real product is the loop

the preview is not an isolated feature. it sits in the middle of the entire product loop.

text
generate code -> prepare files -> run preview -> check quality -> repair if possible -> show user -> accept refinement -> patch or regenerate -> preview again

every weak part of that loop shows up in the preview.

bad generation shows up as missing files. weak project normalization shows up as a dev server that cannot start. poor routing shows up as an iframe that only works locally. missing QA shows up as blank screens being shipped to the user. bad lifecycle management shows up as slow refinements. weak logging shows up as "it failed, but i don't know why."

this is why i do not think of preview as a frontend box on the page.

preview is the proof system for the generator.

what i respect about this problem

the hard thing about building something in this category is that the user expectation is magical, but the implementation cannot be magical.

the user wants to type an idea and see software.

the system has to do queueing, file normalization, dependency installation, sandboxing, routing, browser automation, auth, retries, screenshots, logs, cleanup, versioning and state transitions. all of that has to happen behind an interface that feels simple.

that is the engineering taste i care about here.

not shortcuts. not a fake preview. not a screenshot pretending to be an app. not "the AI generated it, good luck running it locally."

if this project is going to work, the preview has to be real. generated code should be forced to stand up, render and survive basic inspection before the system congratulates itself.

that is where the product earns trust.

and honestly, that is the fun part.