Manifest
The manifest tells MicroHost what a release requests: SDK modules, routes, permissions, resource limits, database compatibility, and admin extensions. You can write it as a JSON file (microhost.json) or generate it with a script (microhost.config.js). Either way the server builds and validates the resolved manifest on every deploy; you normally never see it, and an administrator can view it read-only.
Status. Parsing, validation, request-versus-grant resolution, limit clamping, the sealed resolved manifest and the schema snapshot are implemented. Two fields are accepted and recorded but not applied when serving:
static.rootandstatic.spaFallback(see below).
Where the manifest comes from
On every ZIP, Git or editor deploy the server picks exactly one source, in this order:
microhost.config.js, if present. It wins. Ifmicrohost.jsonis also present, it is ignored and the deploy reports the warningmanifest_json_ignored(JSON is never merged and never used as a fallback if the script fails).microhost.json, unchanged, if present.- Built-in defaults.
The result is validated, written as canonical JSON, and sealed into the release (resolved-manifest.json plus resolved-manifest.meta.json with the source kind, hashes and SDK version). Those file names and db-schema.json are reserved: a deploy that contains them is rejected. A stored release is never rebuilt, even after a platform upgrade or a rollback. A validation failure rejects the whole deploy with a message like invalid_manifest: microhost.json: ... and leaves the active release unchanged.
With no manifest at all, the defaults apply: schema version 1, static root public, no explicit routes (the /api/<name> convention still works), no requested SDK modules list, no requested capabilities, no requested limits, no admin extensions. All dynamic capabilities are off, because a script's presence does not imply you want database access.
Fields
{
"schemaVersion": 1,
"sdk": ["db", "auth", "session"],
"schemaCompat": { "min": 1, "max": 3 },
"entrypoints": {
"routes": [
{ "path": "/api/contact", "script": "api/contact.js", "methods": ["POST"] },
{ "path": "/posts/:slug", "script": "api/post.js", "methods": ["GET"] }
]
},
"static": { "root": "public", "spaFallback": false },
"errorPages": { "404": "errors/not-found.html", "5xx": "errors/server.html", "_": "errors/page.js" },
"healthCheck": { "path": "/status" },
"jobs": { "cleanup": { "script": "jobs/cleanup.js", "schedule": "0 3 * * *" } },
"permissionsRequested": {
"database": true,
"storage": false,
"secrets": false,
"email": false,
"externalHttp": ["api.example.com"],
"workspaceDatabases": []
},
"limitsRequested": { "timeoutMs": 1000, "memoryMb": 32 },
"admin": { "schemaVersion": 1, "extensions": [] }
}
Unknown fields reject the file. The file is at most 64 KiB.
| Field | Meaning |
|---|---|
schemaVersion |
Required. Only 1 is accepted. |
sdk |
Optional list of SDK modules: auth, db, session, sql. Unique, known names only; stored sorted. See SDK modules. |
schemaCompat |
Optional inclusive range { min, max } of database user_version values this release works with (see below). Both are unsigned integers, min <= max. |
entrypoints.routes |
Optional explicit routes, at most 256. See Routing. |
static |
See below. |
errorPages |
Optional custom pages for errors the platform produces, keyed by status, class or _. See Error pages. At most 32 entries. |
healthCheck |
Optional { "path": "/status" }: the path the post-activation health check requests. See Health check. |
jobs |
Optional scheduled and background jobs, at most 32: { "name": { script, schedule, timezone, overlap, catchUp, timeoutMs, retries } }. See Scheduled jobs. Needs the jobs capability to run. |
permissionsRequested |
Optional booleans database, email, storage, secrets; externalHttp (list of hostnames, at most 32, exact names, duplicates removed); workspaceDatabases (list of names, at most 64, each 1 to 64 letters, digits, _ or -). |
limitsRequested |
Optional timeoutMs and memoryMb, positive integers. They can only lower the administrator's limits. |
admin |
Optional admin extension declarations. |
Routes
Each route has a canonical absolute path (at most 1,024 bytes, grammar in Routing), a script (a safe relative path inside the release: no leading /, no .., at most 1,024 bytes), and a non-empty methods list. Accepted methods: GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS; case is normalized. Routes are sorted by path. Two routes that could match the same request with equal specificity and a shared method are rejected as ambiguous; the same path may appear twice only with disjoint methods.
Static
static.root (a safe relative path, default public) and static.spaFallback (default false) are parsed, validated and recorded in the resolved manifest, but the serving path does not read them yet: static files always come from the release's public/ directory, and single-page-app fallback is not enabled for sites. Do not rely on either field to move your files or to get index.html fallback. A 404.html in public/ is served as the not-found page for GET and HEAD; for other errors and for your own page layout use errorPages.
Error pages
errorPages maps an error to the page that should answer it. Without it, visitors get the platform's short plain-text answer (and public/404.html for a missing page, see Routing).
"errorPages": {
"404": "errors/not-found.html",
"5xx": "errors/server.html",
"_": "errors/page.js"
}
Keys. A status from 400 to 599 ("404"), a class ("4xx" or "5xx"), or "_" for every other error. The most specific entry wins: the exact status, then the class, then _. Anything else (for example "200", "6xx") rejects the manifest.
Values. A safe relative path, no hidden segments (nothing starting with .) and not microhost.json.
- A path ending in
.jsis a script in the release, run like a route handler under the site's limits and grants. It reads the status fromrequest.params.status(a string such as"404").request.path,request.methodandrequest.queryare those of the original request, and the request body is empty. Scripts underpublic/orapi/_admin/are refused. - Any other path is a static file inside
public/."errors/not-found.html"ispublic/errors/not-found.html.
What it applies to. Errors the platform itself produced for a known site: 404 (no route, no file, no script), 405 (also for POST to a static file), 408, 413, 429 from the per-site budget and 5xx when a script fails, times out or runs out of memory. It does not apply to:
- a response your own code built, even with an error status (
new Response("nope", { status: 404 })stays as it is); - requests rejected before the site is known: a malformed path or Host (400), a target above 8 KiB (414), headers above the limit (431), the per-IP rate limit (429), or an unknown host;
- the
/_adminpaths, which are refused before the site is looked up; - API requests: a request whose
Content-Typeis JSON (application/jsonor any+json), or whoseAcceptasks for JSON and not fortext/html, keeps the plain-text answer. A client that sends JSON cannot use an HTML page, and a script page would run for every failed API call.
Behavior.
- The visitor always gets the original status. A page script that answers 200 still produces a 404; its headers and body are kept. Use the
Content-Typeheader of your script or file as usual. 405keeps itsAllowheader.- A configured
404wins over thepublic/404.htmlconvention. Without a404entry,public/404.htmlkeeps working for GET and HEAD. - If the page is missing, or the script fails or is refused, the platform's generic answer stays. An error page is never itself replaced by an error page, so there is no loop.
- A script page counts against the site's request budget like any dynamic request, and
429is only ever answered with a static file, never a script, so a flood cannot make the platform run more of your code. - A bad entry (unknown key, invalid path) fails the deploy with a manifest error, not the request.
A site that wants one dynamic page for every error needs only one entry:
// errors/page.js
export default function handler(request) {
const status = Number(request.params.status);
return Response.html(`<h1>${status}</h1><p>Page ${request.path} is not available.</p>`);
}
"errorPages": { "_": "errors/page.js" }
The page script may use the same granted capabilities as any handler (database, storage and so on), and it counts as one execution. Keep it small and make it robust: if it throws, visitors see the generic answer instead.
Health check
After a release is activated, the platform requests one path on the site's primary domain and expects a 2xx answer (see Deploying). By default that path is /. healthCheck.path chooses another one:
"healthCheck": { "path": "/status" }
The path may point at a static file, a declared route or a script route. Redirects and other non-2xx answers count as a failed check, so the path must answer directly. Rules:
- It starts with
/, is at most 256 bytes and contains only visible ASCII characters (no spaces or control characters). - No
..,?or#: there is no query string or fragment. - It must not start with
/_admin. healthCheckaccepts onlypath; unknown keys are rejected.
The check uses the path of the release being activated. Releases without healthCheck are checked on /, exactly as before. Config scripts set it with defineSite({ healthCheck: { path: "/status" } }).
schemaCompat and the database version
The site database's SQLite user_version is set to the number of the last applied migration. schemaCompat declares which versions your release can run against:
- A deploy whose migrations would leave the database at a version outside
schemaCompatfails (candidate release does not support resulting database schema N), and the release is marked failed. - Activating a release is refused when the site database's current version is outside its range.
- If you omit
schemaCompat, the host default applies. The production CLI default is 0 to 5, so a release with six or more migrations must declare a larger range. - Rollback needs the old release's range to still contain the current database version. A migration that moves the database beyond the previous release's
maxmakes that release ineligible for rollback. Migrations are forward-only; plan compatible changes (add columns, then drop later).
schemaCompat is a declaration, not a grant: it does not select a database or run SQL.
Requests and grants
permissionsRequested is a request. A grant is an administrator's decision, stored per site as policy:
effective = requested by the active release ∩ granted by site policy ∩ resource availability
- Missing grants do not block a deploy or an activation. They produce a visible reason (
not_requested,not_granted,database_not_granted,quota_unavailable,site_inactive) and leave the capability absent:ctx.dband friends areundefined. - Grants apply to the active release only and never activate an inactive release.
- A grant change applies to the next broker call of any execution, including one already running. A revoked grant makes the next call fail with
capability_denied; there is no grace period. An outbound request already handed to a provider cannot be recalled, and queued mail is re-checked before delivery. - Egress is exact hosts on port 443. Only a Super Admin can grant external hosts, public egress or mail.
- A workspace-level capability template can pre-seed the grants of new sites (Super Admin only; it defaults to deny, cannot enable mail or egress, and never changes existing sites).
- Never put secret values in a manifest.
Read the current state with GET /admin/api/v1/sites/{id}/capabilities (each capability reports requested, granted, effective and reasons). Change it with PATCH and the current policy_version. The resolved manifest of the active release is at GET /admin/api/v1/sites/{id}/manifest.
Limits are clamped the same way: limitsRequested values are intersected with the policy limit, never above it. See Limits and errors.
Common validation errors
| Cause | Result |
|---|---|
| Unknown field, invalid JSON, file over 64 KiB | The whole manifest is rejected. |
schemaVersion other than 1 |
Rejected. |
Unknown or duplicate sdk name |
sdk must contain unique known module names |
More than 256 routes, bad path, bad method, empty methods |
Rejected. |
| Ambiguous overlapping patterns | ambiguous route patterns overlap for a shared method |
schemaCompat.min > max |
Rejected. |
An invalid admin extension |
Not rejected: that extension is dropped and a safe warning code is recorded; the rest of the manifest is used. |
Deploy errors are reported as bounded messages with a stable prefix (for example invalid_manifest, syntax_error; see Config script). The validator is crates/core/src/manifest.rs.