Menu

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:

Deploy walkthrough:

  1. ./build-zip.sh (writes blog.zip), upload it with activate=false.
  2. Read the requested capabilities (GET /admin/api/v1/sites/{id}/capabilities) and grant database with PATCH and the current policy_version.
  3. Activate the release (POST .../releases/{rid}/activate).
  4. 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.js and examples/tasks/test.mjs load sql.js and db.js from crates/engine-quickjs-wasm/guest/, where they no longer are (they moved to sdk-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:

  1. Invalid manifest. microhost.json is { "version": 1, "entry": "api/index.js", "sdk": [...] }. The manifest requires schemaVersion: 1 and rejects unknown fields (version, entry), so the deploy would be refused. There is also no permissionsRequested.database, and no explicit routes.
  2. Routes never reach the handler. One script api/index.js serves /register, /login, /logout, /logout-everywhere and /account. Without routes, the only convention URL is /api/index, where none of those pathnames match.
  3. Rows are arrays. The handler reads result.rows[0] from raw ctx.db.query and then uses user?.password_hash, but raw rows are arrays, so the property is always undefined: every login verifies against the dummy hash and fails.
  4. The dummy hash is not a valid platform hash. Its hash part is 29 bytes, not 32, so verifyPassword rejects it with invalid_input before doing any work. In a login handler this turns the "same answer for unknown users" defense into an error and a timing difference (reference).
  5. test.mjs only 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.