dhtml
DHTML, build without limits

One folder. One upload. Pages you can read.

dhtml is a language and the hosting that runs it. Write HTML with a few tags and get accounts, payments, AI, jobs, live pages and a database, safe by default. Zip the folder, upload it to dhtml.net, and it is online. No npm, no build step, nothing to install.

Hosted on dhtml.net · beta from January 2027 · MIT starters, your apps are yours

§ 00 · The folder

The whole project is a folder

From examples/notes. Everything outside public/ stays out of reach of the browser.

  • notes/
  • dhtml.configsettings: protected paths, mail, AI
  • public/static files, the only folder a browser can read
  • style.cssserved as written
  • server/pages/GET pages. The file path is the URL
  • index.dhtmlGET /
  • admin/index.dhtmlGET /admin (admins only, by config)
  • server/api/actions: forms, fragments, JSON
  • note-add.dhtmlPOST /api/note-add
  • server/layouts/page shells, with a slot
  • server/partials/fragments you include
  • server/migrations/plain SQL, applied in order
  • 001_notes.sqlCREATE TABLE notes (…)

§ 01 · Why dhtml

Why dhtml

  1. One language, one place to run it

    The language and the hosting are one thing. There is no framework to choose, no bundler, no node_modules, and no server to look after. You write the app, upload it, and dhtml.net runs it.

    Because the vocabulary is small and written down, the whole app is something you can understand end to end. So can an AI agent: a short guide and a ready-made skill give it the loop, the patterns and the common errors to start from.

  2. AI is native

    <d:ai> is a tag. Anthropic and OpenAI-compatible models, answers that stream as they are written, usage and history kept as data, and a rate limit required by design. An AI feature is a few lines in an action, not an SDK and a service.

  3. Safe by default

    Output is escaped for the place it lands. SQL values are bound parameters. Forms carry CSRF tokens. Outbound calls go only to the hosts you declare, and signed webhooks run each event once. There is no raw filter and no way to run a shell command. You don't switch safety on; you would have to work to take it out.

    How the model works →

  4. Pages you can read

    A page is HTML with a handful of tags. The file path is the URL, so routing needs no router. Components take named inputs that are checked before the page ever runs. A year later you open the file and see what it does.

  5. The speed of a single-page app

    Links and forms swap only what changed, components and small islands of client state handle the interactions, and live updates arrive as they happen. Every page is still a normal page with its own address, and navigation falls back to ordinary page loads without JavaScript.

  6. A real backend, built in

    Accounts, magic-link sign-in, sign-in with Google, API keys, Stripe payments, signed webhooks, scheduled and queued jobs with retries, outgoing mail, languages, uploads. Each is a tag or a few lines, written the same way as the rest of your app.

  7. Every upload is checked

    Before a new version goes live, dhtml.net compiles every page, route and setting, backs up your database and applies your migrations. A mistake is reported with its file, its line and a hint, and the version that is running is left as it was.

  8. Yours

    An app is a folder plus a database, and both are yours. The docs and the MIT starters are public.

§ 02 · Built with it

Whole apps, one folder each

A news site with an AI writer, and an AI product with credits and Stripe. See both →

§ 02 · A real page, a real action

Two files from the notes example

Nothing below is retyped. The code is read from examples/notes when this site is built, and the notes beside it are checked against the line they describe.

notes/server/pages/index.dhtmlGET /
<d:query as="notes">
SELECT n.id, n.user_id, n.body, n.created_at, u.name AS author
FROM notes n JOIN _dhtml_users u ON u.id = n.user_id
ORDER BY n.id DESC LIMIT 50
</d:query>
<d:layout src="main" title="Notes">
<h1>Notes</h1>
<d:auth>
<form method="post" action="/api/note-add">
<textarea name="body" rows="3" maxlength="500" placeholder="Write a note..." required></textarea>
<button>Add note</button>
</form>
<h2>Ask about the notes</h2>
<form method="post" action="/api/ask" data-stream-target="#answer">
<input name="question" placeholder="What are people writing about?" required>
<button>Ask</button>
</form>
<pre id="answer" class="answer"></pre>
<d:else>
<p>Sign in to write notes. <a href="/register">Register</a> if you do not have an account yet.</p>
</d:auth>
<ul class="notes">
<d:for each="n in notes"><d:include src="note" /><d:empty><li class="muted">No notes yet.</li></d:for>
</ul>
</d:layout>
notes/server/api/note-add.dhtmlPOST /api/note-add
<d:action auth ratelimit="30/minute">
<d:validate field="body" required max="500" />
<d:exec>INSERT INTO notes (user_id, body) VALUES ({{ user.id }}, {{ form.body }})</d:exec>
<d:redirect to="/" flash="Note added" />
<d:invalid>
<d:layout src="main" title="Notes">
<h1>That note cannot be saved</h1>
<p class="error">{{ errors.body }}</p>
<p><a href="/">Back</a></p>
</d:layout>
</d:invalid>
</d:action>

More, including a Stripe payment written in tags: the examples.

§ 03 · Hosting

Upload it, and it is online

StepWhat happens
Make a siteSign in at dhtml.net and make a site. It gets its own address, NAME.dhtml.net, with HTTPS.
UploadZip the folder and upload it on the site's page, or from a script or an AI agent with a deploy key.
CheckedPages, routes, settings and migrations are checked, the database is backed up and the migrations run. A mistake is reported with its file and line, and the running version is left alone.
SecretsAPI keys and passwords are set on the site's page, never in the zip, and named env:NAME in dhtml.config.
Your domainPoint your own domain at the site.

Get started →

§ 04 · Built for real apps

Any web app, from a landing page to a paid AI product

dhtml does not decide what you can build. The server side is SQL, outbound calls, mail, AI, jobs, uploads, webhooks and live streams. The browser side is HTML, components, islands of client state, and any JavaScript files you put in public/, talking to JSON actions. LaunchSaaS is a whole product in one folder: credits, plans, Stripe, a text studio and an image studio, sign-in by link, a control centre, a blog, docs and a changelog. LaunchBlog is a news site with authors, editors and an AI writer. Download either, change it and upload it to your site.

  • dhtml.config and secrets
  • File-based routing
  • Security headers and request limits
  • Template engine
  • SQLite and migrations
  • Actions, validation, CSRF, uploads
  • AI calls and streaming
  • Accounts and sessions
  • Outgoing mail (SMTP)
  • Computation: <d:set> and arithmetic filters
  • Outbound calls: [http] and <d:http>
  • Signed webhooks with dedupe
  • Stripe payments (examples/shop)
  • Jobs: scheduled and queued work
  • API keys, bearer tokens and CORS
  • AI as data: usage and history
  • Fast navigation (SPA-style page changes)
  • Magic-link sign-in
  • Saving pictures from services: <d:save>
  • Banning accounts and the account log
  • Telling the site owner by mail: <d:notify>
  • Answers that are not HTML: <d:respond type>
  • Components: <d:component> and <d:props>
  • Client state: islands (/_dhtml/state.js)
  • Live updates: <d:publish>, [channels], /_dhtml/events
  • Board example (examples/board)
  • LaunchSaaS starter (examples/launchsaas)
  • Sign in with Google: [oauth]
  • Mail to the visitor: <d:mail />
  • Compression and long caching
  • Guide for AI agents
  • Mail to many rows: <d:mail for=>
  • HTML mail with templates and a live preview
  • Reusable steps: <d:use>
  • request.host: many sites in one app, by name
  • Numbers and dates: fixed, thousands, date layouts, date and time rules
  • Zip files as an upload type
  • Starters: LaunchBlog, LaunchSaaS
  • LaunchBlog starter (examples/launchblog)
  • Signing in actions: hmac filters
  • Following redirects: <d:http follow>
  • Languages: [lang], the t filter, mail in the recipient's language
  • Deploy by API with a deploy key

Everything built, tested and in progress →

§ 05 · Status

See exactly where it stands

Every feature, with what is built, tested and in progress, is on one page generated from one list, so it agrees with every other page.

The status page →

§ 06 · License

One paragraph

The documentation, the starters (MIT) and this website are public and free. What you build is yours: your pages, actions, migrations, styles and data, under any license you like. The whole story →