Deploying
Every deploy creates a new immutable release. Releases are created ready but not necessarily active: you can stage one, check its grants, and activate it later, and you can roll back by activating an older ready release. Platform-level setup (domains, TLS, configuration) is in the operations guide.
Three ways to deploy
| Method | How | Notes |
|---|---|---|
| ZIP upload | Site admin UI, or POST /admin/api/v1/sites/{id}/deployments/zip |
The archive root holds public/, api/, private/, the manifest source. Default limit 32 MiB in one request (see Site layout); larger archives, up to 512 MiB by default, are sent in chunks (below). The admin UI switches to chunks above 24 MiB. |
| Git | POST /admin/api/v1/sites/{id}/deployments/git, or a push webhook (POST .../webhooks/git, token from POST .../webhooks/git/token) |
The site's git source is set with GET/PATCH .../git-source. The sample platform policy allows only github.com and bitbucket.org over HTTPS, and requires a branch or commit. |
| In-browser editor | Admin UI, Files | Edits are saved to a draft. Publishing (POST .../deployments/draft, with If-Match set to the draft's tree ETag) turns the exact draft tree into a release. Draft files can also be imported from an existing release (POST .../draft/import). |
| Uploading local files | Admin UI, Files | New → Upload files… or Upload folder… (also in the right-click menu of the tree and of a folder, which uploads into that folder), or drag files and folders onto the tree. Folders keep their structure; hidden files and folders (names starting with a dot) are skipped, and so are empty folders. Each file is written with PUT .../files/{path} and If-None-Match: *, four at a time, with a progress bar per file and overall; Cancel upload stops everything still running or waiting. A file that already exists is not overwritten until you choose Replace (then the write carries If-Match with the file's current ETag); Skip existing leaves it alone. Limits: 16 MiB per file, 1,000 files per drop or folder choice, and the draft's own size and file-count limits. Uploading changes only the draft; publish to deploy it. |
All three go through the same server-side pipeline. The upload calls take ?activate=true or ?activate=false. They answer 202 with { deployment_id, status, release_id, activated, health, warnings }. Admin API calls need an authenticated session and CSRF token, and support idempotency keys; see the OpenAPI document served by the control plane.
Large ZIP archives
A ZIP posted in one request to .../deployments/zip (Content-Type: application/zip) is streamed to the server's disk as it arrives, so curl -T site.zip works for archives up to deploy.max_upload_bytes (default 512 MiB) without holding them in memory. Clients that cannot send one long request (a proxy with a short timeout, an unreliable link) can upload in chunks instead: the archive is then assembled on the server's disk. The limit is deploy.max_upload_bytes (default 512 MiB). Permissions are those of the ZIP route: deploy.create and code.write (Developer or Site admin).
POST /admin/api/v1/sites/{id}/uploadswith JSON{ "total_bytes": <size>, "sha256": "<64 hex>" }(sha256optional; when given, the server checks it before the deploy and refuses a mismatch with422 checksum_mismatch). Answers201 { upload_id, chunk_bytes }.PUT .../uploads/{upload_id}?offset=<bytes sent so far>withContent-Type: application/octet-streamand the next chunk (at mostchunk_bytes, 8 MiB or less), in order. Answers200 { received }. A wrong offset is409 upload_offset_mismatch; a chunk pasttotal_bytesis422 upload_too_large.POST .../uploads/{upload_id}/deploy?activate=false|true. Answers like the ZIP route (202 { deployment_id, status, release_id, activated, health, warnings }) and records azipdeployment. A deploy before every byte arrived is409 upload_incompleteand keeps the upload. Otherwise the upload is consumed: the server removes its data whether the deploy succeeds or fails.
DELETE .../uploads/{upload_id} cancels an upload (204). An upload without a chunk for an hour expires, a restart removes every open upload, and a site can have two open uploads at once (409 too_many_uploads). A server without chunked upload support answers 503 uploads_unavailable.
With curl (cookie jar from a login and the CSRF token in $CSRF):
API=https://admin.example.com/admin/api/v1
SITE=my-site
ZIP=site.zip
SIZE=$(stat -c %s "$ZIP")
SUM=$(sha256sum "$ZIP" | cut -d' ' -f1)
CHUNK=$((8 * 1024 * 1024))
ID=$(curl -sf -b cookies.txt -H "x-csrf-token: $CSRF" -H 'content-type: application/json' \
-d "{\"total_bytes\": $SIZE, \"sha256\": \"$SUM\"}" "$API/sites/$SITE/uploads" | jq -r .upload_id)
OFFSET=0
while [ "$OFFSET" -lt "$SIZE" ]; do
tail -c +$((OFFSET + 1)) "$ZIP" | head -c "$CHUNK" > chunk.part
curl -sf -b cookies.txt -H "x-csrf-token: $CSRF" -H 'content-type: application/octet-stream' \
-X PUT --data-binary @chunk.part "$API/sites/$SITE/uploads/$ID?offset=$OFFSET" > /dev/null \
|| { curl -s -b cookies.txt -H "x-csrf-token: $CSRF" -X DELETE "$API/sites/$SITE/uploads/$ID"; exit 1; }
OFFSET=$((OFFSET + $(stat -c %s chunk.part)))
done
rm -f chunk.part
curl -sf -b cookies.txt -H "x-csrf-token: $CSRF" -X POST "$API/sites/$SITE/uploads/$ID/deploy?activate=false"
Use the chunk_bytes from the first answer if it is smaller than 8 MiB (a server with a small deploy.max_zip_bytes).
What a deploy does
- Ingest and validate the archive or tree: safe names, no symlinks, size and file-count limits (Site layout).
- Resolve the manifest from
microhost.config.js, elsemicrohost.json, else defaults (Manifest, Config script). A failure rejects the deploy; nothing changes. Warning:manifest_json_ignoredwhen both files exist. - Validate migrations on an in-memory database and seal the schema snapshot (Migrations). A failure rejects the deploy.
- Create the release record and seal the resolved manifest and snapshot with it.
- Apply pending migrations to the live site database, if the release has any. The migration runs here, not at activation; a failure marks the release failed. See Migrations.
- Mark the release ready, and if
activate=true, activate it. - Garbage-collect old releases beyond the retention count (default 5; the active release is always kept).
Staging, grants and activation
Upload with activate=false to stage a release. Then:
- Look at what the release requests:
GET /admin/api/v1/sites/{id}/capabilities(and the resolved manifest atGET .../manifest). Grant what is needed withPATCH .../capabilitiesand the currentpolicy_version. A grant applies to the active release only and never activates anything by itself. - Check or change the SDK module switches (
GET/PATCH .../sdk-modules, see SDK modules). - Activate:
POST /admin/api/v1/sites/{id}/releases/{rid}/activate. Activation switches the site's active release with a compare-and-set on the current one, evicts cached routes, and requires the site database version to be inside the release'sschemaCompat. - If the release declares admin extensions, an administrator enables them (and re-enables them when their descriptor changes).
Inspect a staged release's files with GET /admin/api/v1/sites/{id}/releases/{rid}/files and .../files/{path}. There is no preview URL for a staged release: only the active release answers requests on the site's hostnames. The configured preview hostname is a stub that answers 404, and the development service (microhost serve --dev) serves static files only and answers 501 for scripts.
After activation, the platform runs a health probe against the site's primary hostname (GET / or the manifest's healthCheck.path, up to 3 attempts, 5 s each). A failed probe is reported as the warning health_check_failed; the new release stays active and nothing is rolled back automatically. A site with no primary domain has no probe.
Rollback
POST /admin/api/v1/sites/{id}/releases/{rid}/activate with an older ready release is the rollback; the same call and the same checks apply. Remember that data is not rolled back. Migrations are forward-only, so the older code runs against the newer database schema, and the old release's schemaCompat must still contain the current database version.
Engines
Which engine runs a site (Wasm, native or Linux worker) is site policy, set by an operator, not by the manifest. Untrusted sites cannot select the native engine. Behaviour differences are in Engine differences and status.
After the first deploy
- The database grant, any other capability you need, and the extension switch are separate admin steps (above).
- Secrets for
ctx.envare set by an administrator (PUT /admin/api/v1/sites/{id}/secrets/{name}); secret values are never shown again. - Operators can view requests, logs and mail outbox state in the site pages of the admin UI; handler
consoleoutput appears in the site log.