the killer demo

Build a login-protected CRUD app
in 10 minutes — no build step.

Magic-link auth, a per-user database, and a working CRUD UI. One command to a running app; the rest is just reading the ~40 lines that power it.

1 · Scaffold it (~30s)

npm create volt@latest tasks -- --template starter

The starter ships with auth + a per-user CRUD already wired (the Account and Notes tabs). No build tool, no config to hand-write.

2 · Run it (~1 min)

cd tasks && npm install && npm run dev

Open the app, click Account, enter your email. In dev the magic link is printed to your terminal — open it, confirm, and you're signed in. The Notes tab is now your login-protected, per-user CRUD. A working app, ~90 seconds in.

3 · The entire backend (it's this small)

Auth is on, so every route is one guard from login-protected. The whole CRUD:

import crypto from "node:crypto";

const tasks = store.collection("tasks");   // memory · Mongo · MySQL · Postgres
const guard = requireAuth(store);          // 401 unless signed in

app.get("/api/tasks", guard, async (req, res) =>
  res.json({ tasks: await tasks.find({ owner: req.user.email }) }));

app.post("/api/tasks", guard, async (req, res) => {
  const text = String(req.body.text || "").trim().slice(0, 500);
  const t = { id: crypto.randomBytes(8).toString("hex"),
              owner: req.user.email, text, done: false };
  await tasks.put(t.id, t);
  res.json({ ok: true, task: t });
});

app.delete("/api/tasks/:id", guard, async (req, res) => {
  const t = await tasks.get(req.params.id);
  if (t?.owner === req.user.email) await tasks.delete(req.params.id);
  res.json({ ok: true });
});

4 · The entire frontend (no build, no JSX)

import { signal, html, mount } from "/volt.js";

const tasks = signal([]), draft = signal("");
const load = async () => tasks((await (await fetch("/api/tasks")).json()).tasks);
const add  = async () => {
  await fetch("/api/tasks", { method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ text: draft() }) });
  draft(""); load();
};
const del = (id) => fetch("/api/tasks/" + id, { method: "DELETE" }).then(load);
load();

mount("#app", html`
  <input value=${draft} oninput=${(e) => draft(e.target.value)} placeholder="New task…" />
  <button onclick=${add}>Add</button>
  ${() => tasks().map((t) => html`<div>${t.text} <button onclick=${() => del(t.id)}>✕</button></div>`)}
`);

User input renders as escaped text nodes — no XSS to think about. Edit, save, it hot-reloads. No bundler ran.

5 · Make it production-grade (~2 min)

npm run dev -- --edit # pick Postgres / MySQL / Mongo, set the URL
npm run dev -- --studio # browse + edit your data, localhost-only

No code changes — the same store.collection("tasks") now talks to Postgres.

A login-protected, per-user CRUD app over a real database — no build step, ~40 lines you can read.

npm create volt@latest tasks -- --template starter