Examples
Three examples live in examples/. This page walks through what each one demonstrates and records the problems found in them while these docs were verified against the code. examples/static-site/ is an empty placeholder (.gitkeep only).
| Example | Shows | Works as written? |
|---|---|---|
examples/blog |
Public server-rendered pages, pattern routes, the database SDK, a config script, an admin extension | Handlers and config: yes. Tests: need path fixes. README migration timing: see below. |
examples/tasks |
The smallest schema-backed handler with microhost/db |
Handler: yes. Test: needs path fixes. |
examples/login |
Registration, login, sessions, logout | No, it has several defects (listed below). Use the login guide instead. |
Blog
A public journal ("Field Notes") with an admin extension to manage posts.
examples/blog/
microhost.config.js site definition: routes, sdk, permissions, admin extension
admin-pages.js page layouts for the extension (plain data, imported by the config)
private/migrations/0001_create_posts.sql
api/posts.js public handler: home, /posts, /posts/:slug, /api/posts
api/_admin/posts.js private extension handler (list, get, create, update, publish, ...)
lib/db.js data helpers over microhost/db
lib/{admin-auth,markdown-lite,slug}.js
public/ index.html, post.html, app.js, styles.css
build-zip.sh reproducible ZIP (sorted entries, fixed timestamps)
tests/, test-e2e.sh Node tests with a fake ctx; an admin-API smoke script
microhost.json kept only as a golden reference; the ZIP does not ship it
What to learn from it:
Config script.
microhost.config.jscallsdefineSite({ sdk: ["db"], schemaCompat: { min: 1, max: 1 }, routes: [...], permissionsRequested: { database: true }, admin: [blog] })and builds the extension withdefineExtensionandfield. See Config script.Routes.
/posts,/posts/:slugand/api/postsall go toapi/posts.js; the handler branches onrequest.params.slugandnew URL(request.url).pathname. The private route/_admin/posts(methodsGET,POST) goes toapi/_admin/posts.js. See Routing.Migrations and table objects. The schema is
0001_create_posts.sql(apoststable with aCHECK (status IN ('draft','published'))and indexes). Handlers useimport { posts } from "microhost/db"and never create tables or seed rows on a request. See Migrations and Database SDK.Querying.
posts.where({ status: "published" }).orderBy("published_at", "desc").orderBy("id", "desc").all()andposts.find({ status: "published", slug }). Slugs and ids reach SQL only as bound values.Optimistic concurrency with filters. The admin handler updates with a guard filter and treats an empty result as a conflict:
const changed = await posts.update({ id, status: "draft" }, { status: "published", updated_at: now() }); if (!changed.length) return reject(409, "post_changed");updateanddeletereturn the affected rows, so "zero rows" is a compare-and-set failure without a separate read.HTML output. Pages are built with template strings and an
escapeHtmlhelper;Response.htmlsets the content type andnosniffbut never escapes.Private handler.
api/_admin/posts.jsstarts withverifyAdminRequest, reads the JSON input, branches onidentity.operation, re-checks the role for writes, and answers JSON. See Admin extensions.
Deploy walkthrough:
./build-zip.sh(writesblog.zip), upload it withactivate=false.- Read the requested capabilities (
GET /admin/api/v1/sites/{id}/capabilities) and grantdatabasewithPATCHand the currentpolicy_version. - Activate the release (
POST .../releases/{rid}/activate). - In Website, Extensions, enable Blog (Viewer reads, Editor changes).
Known problems.
- The README says activation applies the migration. The deploy code applies pending migrations during the deploy itself, before the release is ready (Migrations). Either way the table exists after the upload.
tests/blog.test.jsandexamples/tasks/test.mjsloadsql.jsanddb.jsfromcrates/engine-quickjs-wasm/guest/, where they no longer are (they moved tosdk-js/runtime/v1/). Point the URLs at the new directory to run them. See Testing sites.
Tasks
The smallest example of table objects. private/migrations/0001_tasks.sql creates tasks (id INTEGER PRIMARY KEY, title, state CHECK (state IN ('open','done')), note NULL); microhost.json requests sdk: ["db", "sql"] and the database permission; api/tasks.js is the whole handler:
import { tasks } from "microhost/db";
export default async function handle(_request, ctx) {
const list = await tasks.where({ state: "open" }).orderBy("id").limit(50).all();
return Response.json({ tasks: list, databaseAvailable: Boolean(ctx.db) });
}
Served at /api/tasks by the convention route. sdk/db.d.ts is the matching hand-written type declaration (Task, TaskState union from the CHECK), and sdk/tests/db-types.ts type-checks it. The example's handler lets a missing grant surface as a thrown capability_denied (HTTP 500) because it never checks ctx.db before the query; real handlers should check first (see Quickstart).
Login
examples/login intends to show registration, login, a protected account endpoint, logout and log out everywhere with microhost/auth and microhost/session. As shipped it cannot work:
- Invalid manifest.
microhost.jsonis{ "version": 1, "entry": "api/index.js", "sdk": [...] }. The manifest requiresschemaVersion: 1and rejects unknown fields (version,entry), so the deploy would be refused. There is also nopermissionsRequested.database, and no explicit routes. - Routes never reach the handler. One script
api/index.jsserves/register,/login,/logout,/logout-everywhereand/account. Without routes, the only convention URL is/api/index, where none of those pathnames match. - Rows are arrays. The handler reads
result.rows[0]from rawctx.db.queryand then usesuser?.password_hash, but raw rows are arrays, so the property is alwaysundefined: every login verifies against the dummy hash and fails. - The dummy hash is not a valid platform hash. Its hash part is 29 bytes, not 32, so
verifyPasswordrejects it withinvalid_inputbefore doing any work. In a login handler this turns the "same answer for unknown users" defense into an error and a timing difference (reference). test.mjsonly checks the source files; it executes nothing. The full register, login and logout cycle was run against the dev VM on 2026-10-25 (see the README).
The login guide is a corrected, complete version of the same flow.