Config script (microhost.config.js)
Instead of writing microhost.json by hand, you can ship a microhost.config.js at the root of your release. MicroHost runs it once per deploy, takes the object it exports, validates it with the same rules as microhost.json (Manifest), and seals the result into the release. You never write or see the resolved JSON; an administrator can view it read-only ("Effective manifest", GET /admin/api/v1/sites/{id}/manifest).
Use a script when the manifest is repetitive (many routes, a large admin extension), or when routes are derived from your content files. Use microhost.json for small static manifests. If both exist, the script wins and the deploy warns manifest_json_ignored.
Status. Implemented and used by
examples/blog. It runs in a separate, deterministic Wasm guest, so the server needs the build guest module; if it is missing, a release that containsmicrohost.config.jsis rejected withconfig_js_unavailable(releases with JSON or defaults still deploy).
Minimal example
// microhost.config.js
import { defineSite, route } from "microhost";
export default defineSite({
sdk: ["db"],
schemaCompat: { min: 1, max: 1 },
routes: [
route("/posts", "api/posts.js"),
route("/posts/:slug", "api/posts.js"),
route("/api/contact", "api/contact.js", { methods: ["POST"] }),
],
permissionsRequested: { database: true },
});
The build environment
The script runs in a capability-free guest:
- It can import only the built-in
microhostmodule and relative.jsmodules inside the staged release (for example./admin-pages.js). Other bare specifiers, absolute paths, traversal, hidden paths and dynamicimport()are refused. - There is no
ctx, no database, mail, storage, secrets or network, no timers, nofetch, nocrypto, noperformance.DateandMath.randomare replaced with functions that throw. The same input always produces the same output. - It has read-only access to the release files through
files.listandfiles.readonly. Nothing is writable. - The
microhostmodule is not available to request handlers, and handler modules (microhost/db, ...) are not available to the config script.
microhost exports
import { defineSite, route, defineExtension, field, files } from "microhost";
defineSite(value?)
Returns the manifest object. The default export of microhost.config.js must be this object (a plain object; a function, undefined or a BigInt is not serializable). Accepted keys are those of microhost.json plus two spellings:
| Key | Notes |
|---|---|
sdk |
List of module names, as in the manifest. |
schemaCompat |
{ min, max }. Omitted: the server's release default is used. |
static |
{ root, spaFallback } (recorded, not applied when serving; see Manifest). |
errorPages |
{ "404": "errors/not-found.html", "5xx": "errors/server.html", _: "errors/page.js" }; see Error pages. |
healthCheck |
{ path: "/status" }; see Health check. |
jobs |
{ cleanup: { script: "jobs/cleanup.js", schedule: "0 3 * * *" } }; see Scheduled jobs. |
routes |
Shorthand for entrypoints.routes; an array of route(...) values or plain { path, script, methods } objects. |
entrypoints |
The long form { routes: [...] }. |
permissionsRequested |
database, email, storage, secrets, externalHttp, workspaceDatabases. |
limitsRequested |
timeoutMs, memoryMb. |
admin |
An array of defineExtension(...) values (the shorthand), or the long form { schemaVersion: 1, extensions: [...] }. |
schemaVersion is set to 1. Any key that microhost.json would reject is rejected here as well. Omitted values get the defaults from Manifest. The convention /api/<name> routing is a runtime rule; it is not expanded into the route list.
route(path, script, { methods }?)
Returns { path, script, methods }. methods defaults to ["GET"], unlike a handwritten JSON route where it is required. Path grammar and precedence are in Routing.
files
files.list(prefix?)returns a sorted array of release-relative file paths underprefix. At most 1,000 files are listed.files.read(path)returns the content of one release file.- Paths are release-relative: no leading
/, no backslash, no.or..segments, and no segment starting with.. Symlinks are not followed. - Reads are bounded: 1 MiB per read and 1 MiB in total for the whole build (the config file itself and every module the build loads count toward it, and re-reading a file counts again).
Generate routes from content files:
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 },
});
Routes derived from data that changes at runtime still need a pattern route such as /posts/:slug and a handler; the config only sees files that are part of the release.
defineExtension(value) and field
These build admin extension declarations. defineExtension({ id, label, icon?, queries?, actions?, pages? }) fills defaults for operations:
| Property | Query default | Action default |
|---|---|---|
method |
GET |
POST |
minRole |
viewer |
editor |
timeoutMs |
5000 |
5000 |
write |
false |
true |
input |
{} |
{} |
output |
{ kind: "object", fields: {} } |
same |
route is required on every operation (there is no guessed handler), pages defaults to [], and explicit values always win. A contradictory override (for example a GET action) fails validation.
field.string, field.number, field.status, field.date, field.boolean, field.text, field.textarea are shorthands: field.string({ required: true, maxLength: 120 }) returns { type: "string", required: true, maxLength: 120 }, and values: [...] is spelled enum in the output. A field value can be reused in several places (for example one definition in an operation's input and another in output).
The builders do not translate keys per position. They emit their options as given, and each position has its own legal keys, so an illegal key makes the whole extension invalid (it is dropped with a warning). Keep to the allowed keys for the place you use the field: operation input accepts type (string, number, boolean, date), required, minLength, maxLength, minimum, maximum, enum; output fields accept type (string, number, boolean, date, status), maxLength, enum, nullable. Form field entries need id, type and label, so write form fields as plain objects (the blog does). Details in Admin extensions.
Build limits and errors
| Limit | Value |
|---|---|
| Wall time | 2 seconds |
| Fuel | 50,000,000 units |
| Guest memory | 32 MiB |
| Serialized manifest | 64 KiB |
| Files listed | 1,000 |
| File reads | 1 MiB per read and in total |
Exceeding any limit, throwing, or producing an invalid manifest fails the deploy; the active release stays unchanged, and nothing is created. Errors are bounded and safe (no host paths or stack traces) and look like code: file:line: message, for example syntax_error: microhost.config.js:7: unexpected token. Codes you can meet: syntax_error, import_error, runtime_error, missing_default_export, not_serializable, output_too_large, read_too_large, memory, fuel, timeout, config_js_unavailable, and invalid_manifest when the output fails manifest validation.
Equivalence and determinism
The builder produces canonical JSON with sorted keys, independent of the order you wrote keys in. Output that is semantically equal gives the same bytes. The extension descriptor hash is the SHA-256 of the canonical validated descriptor, so an equivalent config keeps the hash of an extension that an administrator already enabled. The blog example keeps microhost.json in the repository only as a golden reference for its tests; its ZIP ships just microhost.config.js (and the local admin-pages.js module it imports).
Types
sdk/microhost.d.ts declares defineSite, route, files, BuildSite and BuildRoute for editors. It does not yet declare defineExtension, field, or the sdk property of BuildSite, although they work.