Docs

Deploy a single HTML file and give it a real backend — database, files, and AI — with zero config and zero keys in your code. For humans and agents.

Deploy a site

Three ways to ship the same single HTML file — each gives you a live URL at name.myhtml.site.

  1. Dashboard — paste or drop your HTML in the New site box. You'll see a live name.myhtml.site preview and whether the name is free before you deploy.
  2. CLI — from any terminal:
npx myhtml-cli login                     # paste your API key once
npx myhtml-cli deploy ./index.html --name pong
# → https://pong.myhtml.site

3. REST API — one call with your API key:

curl -X POST https://myhtml.io/api/v1/sites \
  -H "Authorization: Bearer mh_live_xxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "slug": "pong", "html": "<!doctype html>…" }'

The MCP server is launching soon. Free sites publish via a quick safety scan (the “slow lane”); paid plans publish instantly. Pick a name that's taken and you'll get a clear error — names are first-come and never overwrite someone else's site.

The mh SDK

When the edge serves your page it injects a short-lived, origin-bound site token and the SDK, so window.mh is just there. The token scopes every call to your site, so there are no API keys in your HTML and nothing for a visitor to steal. The surface:

  • mh.db — a per-site JSON database (shared + per-visitor + key-value).
  • mh.files — file uploads served from your subdomain.
  • mh.ai — call a fast AI model from your page (opt-in; see below).
  • mh.visitor() — the current visitor's anonymous id.
  • mh.realtime — websockets + presence for multiplayer & live updates.
  • mh.auth — let visitors sign in to your site with a magic link.

mh.db — the database

Every site gets its own document database — no setup, no schema, no connection string. A collectionholds JSON documents you create, query, update, and delete. Behind the scenes it's stored as JSONB and scoped to your site; you just call the SDK.

// mh is auto-injected at serve time — no <script> config, no keys.
const scores = mh.db.collection('scores');

// Create a document (returns it with a generated id):
const doc = await scores.create({ player: 'alice', points: 42 });

// List — filter (shallow equality), sort ('-field' = desc), limit, cursor:
const { docs, nextCursor } = await scores.list({
  filter: { player: 'alice' },
  sort: '-points',
  limit: 10,
});

await scores.get(doc.id);                 // one doc
await scores.update(doc.id, { points: 99 }); // shallow-merge patch
await scores.delete(doc.id);

// Live — fires the instant anyone changes this collection (great for a
// leaderboard that updates itself). Returns an unsubscribe function.
const stop = scores.subscribe({
  onCreate: (d) => addRow(d),
  onUpdate: (d) => updateRow(d),
  onDelete: (id) => removeRow(id),
});

Shared vs. per-visitor

Collections and mh.db.set/get are shared — every visitor of your site reads the same data (great for leaderboards, guestbooks, galleries). mh.db.user is private to each visitor, keyed by an anonymous per-browser id (great for saved progress or preferences).

// Shared — every visitor of this site sees these:
await mh.db.collection('guestbook').create({ name: 'sam' });

// Site key-value sugar (also shared):
await mh.db.set('theme', 'dark');
const theme = await mh.db.get('theme');

// Per-visitor — private to each browser (anonymous visitor id):
await mh.db.user.set('progress', { level: 7 });
const me = await mh.db.user.get('progress');

list() filters are shallow equality ({ status: 'done' }); sort is '-field' for descending. Calls are rate-limited per site and per visitor, and you own all of it — wipe or delete the site anytime from your dashboard.

mh.files — uploads

Upload a File or Blob straight from the page and get back a URL served from your own subdomain at /_f/<key>.

const { url } = await mh.files.upload(file); // → https://<you>.myhtml.site/_f/<key>
await mh.files.list();
await mh.files.delete(path);

mh.ai — AI on a page

Live — enable it per site

mh.ai.chat()lets a page call a fast AI model (OpenAI's GPT by default) with no API key in your code — for a roast of a high score, a “make it vegan” button, an NPC, a summariser. mh.ai.image()generates a picture from a prompt — a logo, a sprite, NPC art — and hands you a ready-to-use image URL. They're off by default on every site. You turn AI on per site under Settings → AI, and choose who pays:

  • Off — pages can't use AI (the call returns a clear error).
  • On — you pay — visitors use AI on your credits, capped per day so a viral page can't drain your wallet.
  • On — visitor pays — visitors sign in (mh.auth) and spend their own AI credits, so a viral page costs you nothing. They buy credits with mh.ai.topUp().
// Turn AI On for the site first (Dashboard → site → Settings).
const res = await mh.ai.chat([
  { role: 'user', content: 'Roast my high score of 42.' },
]);
console.log(res.content);

// Generate an image too — drop it straight into an <img>:
const { url } = await mh.ai.image('a friendly robot mascot, flat vector', { size: '1:1' });
document.querySelector('img').src = url;   // url is a ready-to-use data: URL

Calls run through our metered proxy (a fast, cost-effective model by default), count toward your site's daily cap (chat and images share it), and are rate-limited per visitor. With visitor-pays, a signed-in visitor's credit balance is charged instead — mh.ai.credits() reads it, mh.ai.topUp() buys more, and an out-of-credits call throws insufficient_credits so you can prompt them.

mh.realtime — multiplayer

Live

mh.realtime.channel(name) opens a websocket to everyone else on the same channel of your site — for multiplayer games, live cursors, chat, and dashboards that update instantly. No server, no keys.

// Everyone on the same channel of your site is connected — live.
const room = mh.realtime.channel('lobby');

room.on('message', (data) => console.log('got', data));
room.on('join',  ({ vid }) => console.log(vid, 'joined'));
room.on('leave', ({ vid }) => console.log(vid, 'left'));

room.send({ x: 120, y: 40 });        // broadcast to everyone else
mh.realtime.channel('lobby').presence.list(); // who's here right now

See it live — move your mouse with everyone here →

send() broadcasts to the channel; on('message'|'join'|'leave') receives; presence.list()is who's here now. Everything is scoped to your site and rate-limited.

mh.auth — visitor sign-in

Live

mh.auth.signIn() lets your visitors sign in to your site with a one-time email link — no passwords, no OAuth setup, no backend. Each site gets its own visitors (a visitor of your site is separate from every other site), so you can build gated content, saved profiles, and personalised pages.

// Let visitors sign in to YOUR site (magic link — no passwords, no setup).
const me = await mh.auth.user();          // { id, email?, name?, signedIn }

if (!me.signedIn) {
  await mh.auth.signIn();                  // opens a popup, emails a one-time link
}

// Once signed in, per-visitor storage follows their account across devices:
await mh.db.user.set('profile', { theme: 'dark' });
await mh.auth.signOut();                   // forget them on this device

signIn() opens a popup and resolves once they click the emailed link (works across devices). After that, mh.db.user.* keys to their account instead of the browser — so their data follows them to any device. Their email is shared with you as the site owner; see the privacy policy.

Drop-in widgets

Live

Official, copy-paste building blocks built on the primitives above — a leaderboard, a guestbook, a sign-in button, live multiplayer cursors, and a drag-and-drop file dropzone. Drop a single <div> on any myhtml page — the widget script loads itself and picks up the mh.*that's already there. No config, no keys, no extra script tag to forget.

<!-- Official drop-in widgets. Just add the div — the matching script
     loads automatically (mh is already on your page). -->

<!-- 🏆 Live leaderboard from a collection, ranked by a field -->
<div data-mh-leaderboard="scores" data-sort="-points" data-limit="10"></div>

<!-- 📝 Guestbook / comment wall (posts + updates live) -->
<div data-mh-guestbook="guestbook"></div>

<!-- 🔑 Sign-in button (magic link, via mh.auth) -->
<div data-mh-signin></div>

<!-- 🖱 Live multiplayer cursors + "N here" presence (mh.realtime) -->
<div data-mh-cursors="cursors"></div>

<!-- 📤 Drag-and-drop file uploads with thumbnails (mh.files) -->
<div data-mh-dropzone></div>

See the widgets live →

Leaderboard data-sort ranks by any field (numbers sort numerically); the guestbook and leaderboard update live via collection.subscribe(); the sign-in button uses mh.auth. More widgets (presence cursors, file dropzone) are on the way.

REST API

Base URL https://myhtml.io/api/v1. Authenticate with a session cookie (dashboard) or an API key from Dashboard → API keys.

curl -X POST https://myhtml.io/api/v1/sites \
  -H "Authorization: Bearer mh_live_xxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "slug": "pong", "html": "<!doctype html>…" }'
MethodPathPurpose
POST/sitesCreate + first deploy (errors if name taken)
GET/sitesList your sites
GET/sites/check?slug=Is a name available?
POST/sites/:slug/deployRe-deploy an existing site
PATCH/sites/:slugUpdate settings (AI, visibility)
DELETE/sites/:slugDelete a site (storage purged)
GET/sites/:slug/data/:collectionRead a site's mh.db collection
GET / POST/keysList / create API keys
GET/usageUsage vs plan limits

Agents: the MCP server

Live

Wire myhtml into Claude (or any MCP client) in one line, and your agent can create_site, update_site, list_sites, read_site_data, and more — authenticated with your API key (sent as Authorization: Bearer mh_live_…).

claude mcp add --transport http myhtml https://mcp.myhtml.io/mcp

The free-site badge

Free sites carry a small badge that the edge injects automatically. Upgrade to a paid plan to remove it.

made with myhtml— links back to myhtml.io