Guide

Single File HTML Side Project Setup, Or: The Three Things That Run Before You Type

Most guides on this topic give you a checklist of things to install. This one is about what happens when you skip that checklist entirely and let a hosted builder do the setup for you. Three side effects, three lines of code, no terminal. With file paths so you can verify each one yourself.

M
Matthew Diakonov
9 min read
4.8from single-file-first builders
Three real lines of client code, with file paths you can grep
No install step, no terminal, no node_modules
Anonymous Firebase auth, so no email gate
Pre-warmed E2B sandbox pool, default size of one

The thing every guide on this misses

Open any tutorial about setting up a single file HTML side project. They all hand you the same numbered list. Install a code editor. Save a file with the .html extension. Open it in a browser. Maybe install a live-reload extension. Maybe push to GitHub Pages. The instructions are correct, and they all assume you, the reader, are the one doing the setup.

That assumption is the gap. Setup is not a value you produce; it is friction the project absorbs before any work gets done. If you delegate the setup work to a hosted builder, the answer to "how do I set up a single file HTML side project" collapses to one URL. The setup still happens, but the system runs it for you.

Below is the literal code that does the setup work on mk0r, with file paths and line numbers. You can click through the source and verify each step. The point is not to sell you a builder. The point is to show what setup looks like when it stops being yours.

What "setup" means here, exactly

Three operations on the client, one operation on the server. Each one is named in real code, in this repo, on the same branch that powers the page you are reading.

Open mk0r.com → three side effects fire

Browser tab
useEffect on mount
Auth listener
Setup
localStorage
Firebase
VM pool

Side effect 1: a UUID lands in localStorage

The session identifier is a v4 UUID generated by crypto.randomUUID() and written to localStorage under the key mk0r_session_key. On a fresh tab the function checks for an existing key, finds none, and writes a new one. On every visit afterward the same UUID is read back and reused, so refreshing the tab does not lose your project.

The whole function is fifteen lines, lives at src/app/(landing)/page.tsx line 32, and looks like this:

src/app/(landing)/page.tsx

That is the entire identity story for an anonymous visitor. The UUID is what the API uses to scope sessions, project history, undo, redo, and which iframe to stream generated HTML into. There is no email. There is no password. There is no "create account" form, ever, on the single-file path.

Side effect 2: an anonymous Firebase user signs in

The localStorage UUID is great for the client, but the server needs an auth principal to attach work to. That comes from Firebase Anonymous Authentication. The auth provider mounts an onAuthStateChanged listener; if there is no user, it calls signInAnonymously(auth). The listener fires again with the new anonymous user, the rest of the app proceeds.

src/components/auth-provider.tsx

The line that matters is line 68: await signInAnonymously(auth). That is the point at which a Firebase auth UID exists. Server-side, the API can now verify a token. You, the visitor, never typed anything.

The Google sign-in modal still exists, but it gates upgrades, not entry. The first time you do something the anonymous tier is not allowed to do, like switching off Haiku to a paid model, the modal opens. For a single-file HTML project the modal never opens at all.

Side effect 3: the sandbox pool gets topped up

Quick mode (single file HTML) does not actually need a sandbox. But the same landing page is also the entry to VM mode, which boots a real Vite + React + TypeScript project inside an E2B sandbox. The pool top-up runs on every visit so the warm path is always warm. It is one fetch, dispatched fire-and-forget:

src/app/(landing)/page.tsx

The endpoint reads the current pool, counts ready and warming sandboxes, and spawns the deficit asynchronously. Each new sandbox boots from the E2B template, runs ACP initialize with a shared API key, sets the model to Haiku, and persists a ready entry in Firestore. The default pool size is one:

Pool target lookup

What the user does, what the system does

Same outcome, two divisions of labor. The left tab is the canonical "set up a single file HTML side project" workflow you have probably read elsewhere. The right tab is what mk0r does on your behalf.

Pick a code editor. Install VS Code or your editor of choice. Install a Live Server extension. Make a folder and a fresh index.html. Write the boilerplate by hand. Decide whether to put CSS in a <style> tag or its own file. Figure out where to host. Pick Netlify, GitHub Pages, or a friend's server. Wire up a deploy step you will only run once. Name your project even though it is a one-evening idea. Lose Saturday morning to setup.

  • An install step you have to repeat per machine
  • A hosting decision before you have a project
  • Time spent on the tooling, not the idea

Setup, sequenced

The order matters. The UUID exists before auth runs because the auth provider needs something stable to associate with the eventual user. The prewarm fires last because it is fire-and-forget and does not block anything else.

1

Visit one URL

There is no install. There is no clone, no npm install, no .env, no port to pick. You type mk0r.com or land here from a Reddit comment and the page renders.

2

A UUID gets written to localStorage

First visit only. The key is mk0r_session_key, the value is a v4 UUID generated by crypto.randomUUID(). On every subsequent visit the same key is read back and reused, so refreshing the tab does not lose your project.

3

An anonymous Firebase user signs in

Triggered by the first onAuthStateChanged callback noticing there is no user. This is the auth identity API endpoints attach work to. You do not see a modal, do not type an email, do not pick a password.

4

The VM pool gets topped up

The landing page POSTs /api/vm/prewarm fire-and-forget on mount. The server reads the current pool, counts how many sandboxes are ready or warming, and spawns the deficit asynchronously. By the time you finish typing your prompt, a sandbox is usually already claimed.

5

You type one sentence

First user input is a freeform description: 'tip splitter for three people, system fonts only, dark by default'. The Quick mode pipeline picks Haiku and starts streaming HTML into the iframe on the right.

The full stack of a zero-setup single file project

Five real components, each named in real code. None of them require anything from you during setup.

Browser tab

The whole interface. No desktop app, no terminal, no editor window. Chrome, Safari, Arc, Firefox all work because it is a Next.js page.

localStorage UUID

A v4 UUID under the key mk0r_session_key. Acts as the anonymous identifier the API uses to scope sessions, history, and edits.

Firebase Anonymous Auth

signInAnonymously(auth) at src/components/auth-provider.tsx line 68. No email, no password. The identity exists for one purpose: gate API calls without making the visitor sign up.

E2B sandbox pool

A Firestore-backed pool of pre-booted sandboxes. Default target size is one, set by VM_POOL_TARGET_SIZE in src/core/e2b.ts line 1852. Topped up on every page load.

Claude Haiku

FREE_MODEL = 'haiku' in src/app/api/chat/model/route.ts line 5. The model that streams the single self-contained .html into the iframe.

The numbers

Setup work, measured in things you do versus things the system does. The four metrics below are the literal counts.

0User-typed config files
0Install commands
0Background side effects
0URL to type

0 pre-booted sandbox waiting in the pool by default

Set by VM_POOL_TARGET_SIZE at src/core/e2b.ts line 1852

When self-setup actually wins

Hosted is not always right. Hand-rolling is the better answer when you want the file on disk to be the truth, not a render. Specifically: when you need the file in a Git repo from minute one, when you are teaching someone else how the language works and the friction is the lesson, when you do not have internet, when you are inside a corporate firewall that blocks third-party sandboxes, when the project will live on a shared server you control and the deploy is part of the artifact.

For everything else, the trade is real. You give up control of which editor renders the file in exchange for never seeing an install step. You give up the satisfaction of typing the boilerplate yourself in exchange for skipping straight to the part where the project does what you wanted. Neither path is wrong. They produce identical artifacts: a self-contained .html file you can email, USB-stick, attach to a Reddit comment, drag onto Netlify Drop.

The mistake is treating "real setup is a checklist of installs" as a moral position. It is not. It is a workflow choice. Pick the one that gets the file out of your head and into a tab fastest.

Want a real teardown of zero-setup builders?

Fifteen-minute call. Bring an idea, leave with a URL and an opinion on whether you should have built it yourself instead.

Frequently asked questions

What is the actual minimum setup for a single file HTML side project?

If you are typing it yourself, the literal minimum is one file with the right name. Create file.html, paste boilerplate (doctype, html, head, body), drop a <style> tag for CSS and a <script> tag for behavior, double-click the file. No editor required. No terminal required. The reason guides keep adding steps (install VS Code, install Node, install Live Server, set up a Git repo) is that each one is a comfort thing, not a requirement. The browser itself is the runtime, the editor is whatever opens .html files for editing on your machine.

Why does mk0r say setup takes zero seconds when there is clearly some setup happening?

There is setup, but you do not do it. The setup work runs in the browser tab and on the server while you are still reading the placeholder text. Three things specifically: (1) src/app/(landing)/page.tsx line 46 generates a v4 UUID with crypto.randomUUID() and writes it to localStorage under the key mk0r_session_key. (2) src/components/auth-provider.tsx line 68 calls signInAnonymously(auth) so the API gates have a Firebase user to attach work to. (3) src/app/(landing)/page.tsx line 64 POSTs /api/vm/prewarm fire-and-forget so a pre-booted sandbox is queued to be ready by the time you click. None of those are user actions. There is no form to fill, no email to verify, no node install to run.

Where is the file written when mk0r generates a single HTML file?

Quick mode streams the file directly into the iframe in the right pane. The bytes never touch your disk unless you click export. The model is fixed: src/app/api/chat/model/route.ts line 5 declares FREE_MODEL = 'haiku', so the free tier emits a self-contained HTML response from Claude Haiku, line by line, into the preview frame. When you save, the browser downloads a .html file built from the current iframe srcdoc. No mk0r runtime, no analytics beacon, no fetch back to our servers in the saved file.

Do I need an account, an email, or a credit card for the single file path?

No. The anonymous Firebase sign-in at src/components/auth-provider.tsx line 68 handles authentication for you. The session UUID at src/app/(landing)/page.tsx line 46 is what API endpoints hash against, not an email. You get a working .html out the other side without a sign-up modal in your face. You only see the Google sign-in modal when you try to do something that requires upgrading off the anonymous user (like switching off Haiku, or claiming an existing project from another tab).

What if my idea outgrows a single HTML file mid-build?

Stop and switch modes. Quick mode is the single-file path. VM mode boots a real Vite + React + TypeScript + Tailwind project inside an E2B sandbox, with HMR on port 5173 and Playwright MCP wired in. The default system prompt at src/core/e2b.ts line 148 is the one paragraph the agent reads to know it is in that environment. The cost of switching is one click. The cost of pretending a project is still single-file when it needs auth, a database, or secrets you cannot ship to the client is much higher: you end up with security holes pasted into a <script> tag.

Why is the VM pool size set to one by default?

Pragmatism. src/core/e2b.ts line 1852 reads parseInt(process.env.VM_POOL_TARGET_SIZE || '1', 10), so the default keeps exactly one ready sandbox per spec hash. The first user pays the boot cost and then immediately triggers a topup of the next one. After that the next visitor inherits a warm box. For higher concurrency you bump the env var; for staging you keep it at one because the failure mode of running the pool empty is a slower first request, not a broken first request.

What kinds of side projects fit a single HTML file and what do not?

Fit: anything where the state lives in the browser (URL hash, localStorage, in-memory), anything reading from a public no-key API, anything you can hand to one other person without a server. Calculators, single-screen games, stopwatch and timer tools, breathing exercises, kid-safe word drills, conference badges, RSVP pages, recipe scalers, beat tickers, e-ink homepages. Do not fit: anything with multi-user state, anything with a secret key you cannot put in the page source, anything with a database larger than a JSON literal you embed at the top, anything that needs auth beyond 'whoever has the URL'.

How do I host the file once mk0r generates it?

Drag onto Netlify Drop for an instant URL with no account. GitHub Gist plus a raw URL via githack works for a couple of friends. Cloudflare R2 with a public bucket alias if you own a domain and want it on a subdomain. None of them need a build step, because the file is the build. mk0r itself can publish on a *.mk0r.com subdomain if you want the simplest path: the preview is already a real running page, the publish action just maps a subdomain to the iframe contents.

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