Menu

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.root and static.spaFallback (see below).

Where the manifest comes from

On every ZIP, Git or editor deploy the server picks exactly one source, in this order:

  1. microhost.config.js, if present. It wins. If microhost.json is also present, it is ignored and the deploy reports the warning manifest_json_ignored (JSON is never merged and never used as a fallback if the script fails).
  2. microhost.json, unchanged, if present.
  3. 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.

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:

Behavior.

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:

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:

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

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.