Menu

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).

  1. POST /admin/api/v1/sites/{id}/uploads with JSON { "total_bytes": <size>, "sha256": "<64 hex>" } (sha256 optional; when given, the server checks it before the deploy and refuses a mismatch with 422 checksum_mismatch). Answers 201 { upload_id, chunk_bytes }.
  2. PUT .../uploads/{upload_id}?offset=<bytes sent so far> with Content-Type: application/octet-stream and the next chunk (at most chunk_bytes, 8 MiB or less), in order. Answers 200 { received }. A wrong offset is 409 upload_offset_mismatch; a chunk past total_bytes is 422 upload_too_large.
  3. POST .../uploads/{upload_id}/deploy?activate=false|true. Answers like the ZIP route (202 { deployment_id, status, release_id, activated, health, warnings }) and records a zip deployment. A deploy before every byte arrived is 409 upload_incomplete and 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

  1. Ingest and validate the archive or tree: safe names, no symlinks, size and file-count limits (Site layout).
  2. Resolve the manifest from microhost.config.js, else microhost.json, else defaults (Manifest, Config script). A failure rejects the deploy; nothing changes. Warning: manifest_json_ignored when both files exist.
  3. Validate migrations on an in-memory database and seal the schema snapshot (Migrations). A failure rejects the deploy.
  4. Create the release record and seal the resolved manifest and snapshot with it.
  5. 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.
  6. Mark the release ready, and if activate=true, activate it.
  7. 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:

  1. Look at what the release requests: GET /admin/api/v1/sites/{id}/capabilities (and the resolved manifest at GET .../manifest). Grant what is needed with PATCH .../capabilities and the current policy_version. A grant applies to the active release only and never activates anything by itself.
  2. Check or change the SDK module switches (GET/PATCH .../sdk-modules, see SDK modules).
  3. 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's schemaCompat.
  4. 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