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 starterThe 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 devOpen 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 URLnpm run dev -- --studio # browse + edit your data, localhost-onlyNo 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