MicroHost JavaScript SDK (API version 1)
Site code is an ES module. Its default export is an async handler:
export default async function handle(request, ctx) {
const { rows } = await ctx.db.query("SELECT 1 AS one");
return Response.json({ one: rows[0][0] });
}
This file is the compact SDK reference. The full developer documentation, with guides and examples, is in docs/developers/. Types for editors: microhost.d.ts.
The cross-engine conformance suite in crates/engine-api/conformance/*.json (48 scenarios) checks the handler contract, Request, Response, status and redirect rules, headers, URL, text encoding, randomness, import rules, limits, capability absence, microhost/auth and private admin-request verification (verifyAdminRequest, ctx.admin; five admin_verify_* scenarios). It has no scenarios yet for microhost/sql, microhost/db, microhost/session, fetch, mail or ctx.dbs. An engine is admitted only after it passes the suite; a feature marked "yes" below is one the suite exercises.
Supported
| Area | Surface | Conformance |
|---|---|---|
| Handler | export default async function (request, ctx) must resolve to a Response |
yes |
| Request | method, url (always https://<host><decoded path><query>), headers, immutable params, text(), json(), arrayBuffer() |
yes |
| Request body | Fully buffered, at most max_request_body_bytes (1 MiB). No streams. |
partly |
| Response | new Response(body, { status, headers }), body string, Uint8Array or null |
yes |
| Status | 200 to 599 for the constructor; other values throw RangeError |
yes |
| Response.json | Sets content-type: application/json |
yes |
| Response.html | Sets content-type: text/html; charset=utf-8 and x-content-type-options: nosniff |
yes |
| Response.redirect | Location header; status 301, 302, 303, 307 or 308 only |
yes |
| Headers | new Headers(init), get, has, set, append, delete; case-insensitive names; repeated Set-Cookie values are kept separate |
partly |
| URL | new URL(input, base?); href, origin, protocol, host, hostname, pathname, search, hash, toString() |
yes (pathname) |
| TextEncoder / TextDecoder | UTF-8 only | yes (encode length) |
| crypto.getRandomValues | Integer typed arrays, at most 65536 bytes per call | yes |
| console | log, info, warn, error; bounded to 64 KiB per execution; site log only; ctx.env values are redacted |
partly |
| fetch | https: URLs, method, headers, body (string or Uint8Array); resolves to status, ok, headers, text(), json(), arrayBuffer(). Exact allowed hosts, redirects re-checked. Implemented through the in-process egress broker; not verified end to end. |
no |
| ctx.db | query, execute, transaction; present only with the database permission |
yes |
| ctx.storage | get, put, append, delete, list, sendFile; present only with the storage permission |
partly |
| ctx.mail | send({ to, subject, text, html? }) returns { messageId } when accepted into the durable outbox. Delivered over the site's own SMTP server if the owner set one, else the platform SMTP server a platform administrator configures; without either a site cannot be granted mail. |
no |
| ctx.jobs | enqueue(name, payload?, { delaySeconds?, key? }) returns { runId }; present only when the site declares jobs and was granted jobs |
no |
| ctx.env | get(name); unknown or reserved names resolve to undefined; present only with the secrets permission |
no |
| ctx.dbs | get(name) handle with query, execute, transaction; declared in microhost.d.ts; integration unverified |
no |
| async / await | Full promise support inside the handler | yes |
| Imports | Static import with relative specifiers ending in .js, inside the release private namespace; reserved microhost/* modules; at most 256 modules and 8 MiB of source |
yes |
ctx.db details
- Parameters:
nullandundefinedbecome NULL; booleans become 0 or 1; safe integers become INTEGER; other finite numbers become REAL (NaNandInfinitythrowTypeError); bigints in the i64 range become INTEGER (others throwTypeError); strings become TEXT;Uint8ArrayandArrayBufferbecome BLOB; anything else throwsTypeError. - Results: INTEGER becomes a number when safe, otherwise a bigint; REAL a number; TEXT a string; BLOB a
Uint8Array; NULLnull. - Each result is
{ columns, rows, rowsAffected, lastInsertRowid };rowsare arrays. queryrefuses statements that modify data;executeruns writes and returns the rows ofRETURNING.- Tenant SQL is data-only: no DDL,
PRAGMA,ATTACH; only an allowlist of functions; tables named_mh_*are reserved. Schema changes are made by migrations.
Absence without permission
ctx.db, ctx.storage, ctx.mail, ctx.env, ctx.jobs and ctx.dbs are undefined unless the release requests the permission and an administrator granted it. Code must not assume they exist. Conformance scenarios *_absent_without_permission check this.
SDK modules
Four reserved, lazily loaded modules, requested with the manifest sdk list (auth, db, session, sql). Details: SDK modules.
| Import | Manifest name | Engines |
|---|---|---|
microhost/sql |
sql |
Wasm, native, Linux |
microhost/db (alias db) |
db (implies sql) |
Wasm, native, Linux |
microhost/auth |
auth |
Wasm, native, Linux |
microhost/session |
session |
Implemented once in the shared SDK layer for all engines. Verified end to end on Wasm (dev VM); native and Linux load the same module and dispatch, but a full cycle against real SQLite is not yet tested there. |
A manifest with no sdk key leaves every module importable; with an sdk list only the listed modules can be imported. An administrator can disable a module per site (GET/PATCH /admin/api/v1/sites/{id}/sdk-modules); the effective set is requested minus disabled. The switch is not authorization: ctx.db is governed only by the database grant.
Database modules (microhost/sql, microhost/db)
import { sql } from "microhost/sql";
import { all, one, run, tx, id, now, tasks, tables } from "microhost/db";
tasksandtablesare examples of generated table exports: they exist only for tables your migrations create and are not declared inmicrohost.d.ts; seedb.d.tsfor the per-release declaration shape.sqlis a tagged template: interpolated values become bound parameters, fragments compose, non-empty arrays expand to?, ?, ...(an empty array throws). There is no raw string interpolation.all,one,run,txaccept fragments only (a plain string throwsdb_use_sql_template).allreturns an array of row objects,onea row ornull(db_multiple_rowsfor more than one),runreturns{ rowsAffected, lastInsertRowid },txruns all fragments in one transaction.id()is a random UUID v4,now()a UTC ISO timestamp.- Table objects (
tasks,tables.tasks) are generated from the release's sealed migration schema snapshot. A table that no migration creates is not importable. A historical release without a snapshot must be redeployed to gain named table exports; rawctx.dbstill works for it. - Table methods:
get,find,where,count,insert(RETURNING *),update,delete(both return the affected rows and need a key or a non-empty filter), andstmt.insert/stmt.insertOrIgnore. Filters: equality, arrays asIN,nullasIS NULL. where()returns a lazy query:orderBy(key, "asc" | "desc")(key is a column or asqlfragment),limit()(default 100, maximum 1000),offset()(first page only),all()givingRows(an array withhasMore,first(),next()), andfor awaitin pages of 100. Paging is keyset based and needs a primary key. Each page is a separate database broker call (ctx.db.query): grants and row budgets are re-checked per page and every page counts as a host call; pages are not a snapshot.- Raw
ctx.dband all SDK calls share the same database grant and broker policy; the SDK adds no permission. - Error codes:
db_use_sql_template,db_multiple_rows,db_duplicate_column,db_no_such_column,db_where_required,db_invalid_direction,sdk_module_disabled,capability_denied. all,updateanddeletereturn plain arrays (typedT[]); onlywhere(...).all()returnsRows. See Database SDK.- Column-level types need a per-release declaration shaped like
db.d.ts. No tool generates it; it is a hand-written example. The fixture is type-checked withtsc --noEmit --strict --skipLibCheck --target ES2022 --module ESNext --moduleResolution Bundler --lib ES2022 sdk/tests/db-types.ts.
Passwords (microhost/auth)
hashPassword(password) returns a PHC string ($argon2id$v=19$m=19456,t=2,p=1$...); verifyPassword(password, storedHash) returns a boolean and accepts only that profile. Passwords are at most 1024 UTF-8 bytes, an execution may make at most 2 auth calls, at most 4 hashes run at once platform-wide, and one call has a 2 second deadline. Errors have lowercase codes (invalid_input, quota_exceeded, host_unavailable, ...). For unknown users, verify against a valid dummy hash of the same profile; that is author guidance. See Auth and sessions.
Sessions (microhost/session)
create(userId, { maxAgeSeconds, data }) resolves with a Set-Cookie value, get(request) with { userId, data } or null, destroy(request) with a clearing cookie value, destroyAll(userId) with nothing. Cookie: __Host-mh_session=<token>; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=<s>. The token is opaque (256 bits); only its SHA-256 is stored in the site_sessions table that you create with this migration:
CREATE TABLE site_sessions (
token_hash BLOB PRIMARY KEY CHECK (length(token_hash) = 32),
user_id TEXT NOT NULL,
data_json TEXT NOT NULL DEFAULT '{}',
expires_at INTEGER NOT NULL
);
CREATE INDEX site_sessions_user_id ON site_sessions(user_id);
Expiry uses the database clock. Limits: user id 256 bytes, data 1024 bytes of JSON, lifetime 1 to 2,592,000 seconds. Every call needs the database grant. Append the cookie with response.headers.append("Set-Cookie", cookie). CSRF checks (Origin, a per-session token) and login throttling are the author's responsibility. See Auth and sessions.
Errors
- An uncaught exception or rejected promise returns 500 with the generic body
Internal Server Error. The message and stack go to the site log only. - A handler that resolves to a non-
Response, or produces an invalid response (bad status, illegal header, oversizeSet-Cookie), returns 502Bad Gateway(ADR-0012). - A timeout returns 504
Gateway Timeout. Infinite loops are stopped at the deadline. The termination cannot be caught: nocatchorfinallyruns after it. - Exceeding the memory limit, or producing a response over the size limit, returns 500.
- A capability refusal at load time (an import outside the release, or a
public/file) returns 403Forbidden. - Host-call errors are thrown with stable codes:
CAPABILITY_DENIED,QUOTA_EXCEEDED,INVALID_INPUT(aTypeError),HOST_UNAVAILABLE,HOST_INTERNAL. SDK modules throw their own lowercase codes (see above).capability_deniedalso appears whenctx.dbis absent for a database module call. Messages are generic.
Response headers
The host removes hop-by-hop headers (for example connection) and content-length, rejects header names that are invalid and values containing CR or LF, limits a Set-Cookie value to 4096 bytes, and enforces the response size limit (4 MiB by default). A rejected header fails the request with InvalidResponse (502; ADR-0012). Multiple Set-Cookie headers are preserved.
Not supported
These are absent in v1. Code that uses them fails with a ReferenceError or undefined. Do not polyfill them with globals; the platform does not provide the capabilities behind them.
- Timers:
setTimeout,setInterval,setImmediate. - Node.js and browser host objects:
process,Buffer,require,module,global,window,document. - Network primitives other than
fetch:XMLHttpRequest,WebSocket,EventSource, raw sockets. AbortControllerandAbortSignal(including onfetch).- Streams:
ReadableStream,WritableStream, streamed bodies,Response.bodyas a stream. - A cookie API or
document.cookie; theCookieheader is an ordinary header (microhost/sessionparses its own cookie). URLSearchParams,FormData,Blob,File.crypto.subtleandcrypto.randomUUID.- Dynamic
import(),eval,new Function, and code generation from strings. - Bare module specifiers other than the four reserved
microhost/*names, URL imports, and imports ofpublic/files. - Interactive database transactions (
BEGINthroughCOMMITacross calls). Usectx.db.transaction. - Storage cursors and pagination;
listreturns at most 1000 names. - Mail attachments and custom senders.
- Writing to
public/or to any path outside the release private namespace. - Engine-specific APIs of QuickJS, Wasmtime or the native binding.
Versioning
SDK_API_VERSION is 1. A change that removes a supported item, or that changes its observable behaviour, requires a new version and new conformance cases. Adding an item requires conformance cases for it before an engine that provides it can be admitted. The version is pinned in each release record, and sdk-js/runtime/v1/ must not be edited once releases exist against it.
ctx.mail.send resolves with an outbox message id when the message is accepted. It does not confirm delivery; the host delivers queued mail asynchronously and may retry or expire it.
Route parameters
An explicit route may use whole-segment named parameters, such as /posts/:slug, or one terminal catch-all, such as /files/*rest. Captures are available as decoded strings in request.params; catch-all segments are joined with /. The object is immutable. Exact and convention routes receive an empty object. See Routing.
Config script (microhost.config.js)
Deployments may include microhost.config.js, which the server evaluates once per deploy in a capability-free build guest. It imports the server-provided microhost module and exports a manifest through defineSite. This module is not part of the request-handler API. The build has no ctx, database, mail, storage, secrets, network, clock or randomness. Relative imports are confined to the staged release. files.list(prefix) returns sorted safe release paths and files.read(path) reads bounded release content.
import { defineSite, route, files } from 'microhost';
const posts = files.list('content/posts/').filter(path => path.endsWith('.json'));
export default defineSite({
routes: posts.map(path => route(`/posts/${path.split('/').at(-1).slice(0, -5)}`, 'api/post.js')),
permissionsRequested: { database: true },
});
The module also exports defineExtension and field for admin extensions (all build exports are declared in microhost.d.ts; defineSite also accepts sdk, the list of requested SDK modules). route methods default to GET. Omitted values resolve to schema version 1, static root public, SPA fallback off, no explicit routes, no requested capabilities or limits, and no admin extensions; schemaCompat comes from the server's release default when omitted. Convention /api/* routing is a runtime rule and is not expanded into the resolved route list. The result is validated by the same schema-v1 manifest and admin descriptor validators as microhost.json, and stored as canonical JSON. If both microhost.config.js and microhost.json exist, the script wins and the deploy warns manifest_json_ignored. Build limits: 2 s, 50,000,000 fuel, 32 MiB, 64 KiB output, 1,000 files listed, 1 MiB of reads. See Config script.