Static API
Static hosts the front end — whatever your build step produces. Point it at that directory and it is live on a subdomain of its own, served from cache at the edge, with bandwidth that is not metered. Put your own name in front of it with a custom domain.
Deploying
Install the CLI and save an organization API key with write on the static instance — once per machine. In CI, set ALTENGINE_API_KEY instead. ALTENGINE_INSTANCE is read by every altengine command:
alt install altlimit/altengine # the CLI, if you do not have it
altengine login # paste the key; it is not echoed
export ALTENGINE_INSTANCE=marketing # the static instance's nameA deploy is then one command against your build output directory:
npm run build
altengine static deploy ./dist142 files, 3.1 MiB
uploading 7 files (135 already stored)
7/7
deployed 8f21c4a90b6e — 142 files, 3.1 MiB
https://marketing-web.altengine.app
live here now; everywhere within about a minuteFiles are hashed locally first and only the ones this site does not already have are uploaded, so a redeploy that changed one page uploads one page. Each upload link a deploy signs is billed as a write, alongside requests and stored bytes.
Deployments and rollback
A deploy creates a deployment and activates it. Activation is the only thing that changes what visitors see, and earlier builds are kept, so switching to one is instant and uploads nothing.
altengine static list # deploy history, * marks the live one
altengine static rollback <deployment-id> # switch to any of them
altengine static info # where it lives, which build is live| Flag | Does |
|---|---|
--message <text> | A label for the deployment, shown in list and kept as its message (up to 200 characters). Defaults to the directory's short git commit, with -dirty appended when the tree has uncommitted changes. |
--dry-run | Hashes the directory, prints the file count and total size, and stops. It does not contact the service, so it cannot say how many files are already stored. |
--no-activate | Uploads the build without publishing it. |
--json | Prints the result as one JSON document on stdout, with progress on stderr. list, rollback and info take it too. |
--no-activate lets a release be staged and switched on later:
altengine static deploy --no-activate ./dist # upload now
altengine static rollback <deployment-id> # publish when you are readyTo remove a build, delete it from the site's deployment list in the console, or with the static_delete_deployment MCP tool. The live build cannot be deleted; activate another first.
A URL for every build
Each deployment has a number, and every ready deployment is served at its own address, whichever build is live:
https://marketing--v7-web.altengine.app/ # deployment 7- Numbers are never reused. A deleted build's address stops serving within about a minute.
- Versioned responses send
x-robots-tag: noindex. - Versioned addresses work on the platform hostname only, not on a custom domain.
- A subdomain may not contain
--.
A visitor gets one build or the other, never a mix. A location already serving the old build can take about a minute to pick up the new one, so give a deploy that long before testing it from a machine that was just on the site.
How a path is resolved
In order, and the first match wins:
| Request | Serves |
|---|---|
/assets/app.js | That exact file. |
/, /docs/ | index.html in that directory. |
/docs | A 301 to /docs/, when /docs/index.html exists. |
/about | /about.html — clean URLs, on by default. |
| anything unmatched | /404.html with a 404. On a single-page app, a page request gets /index.html with a 200 instead. |
Everything in the directory is deployed, dotfiles included — .well-known/ is served like any other path.
Single-page apps
SPA mode serves /index.html with a 200 for an address that is not a file, so a deep link works on a cold load. It is turned on for you when a site's first deployment goes live and looks like a client-routed build: one page, at the root, and no 404.html. Set it yourself any time in the console — later deploys never change it back.
A missing script, stylesheet, image or fetch() still gets a 404. The shell goes only to requests the browser is loading as a page, so /edit/something.js typed into the address bar reaches your router, while a bundle that is not there fails as a missing file instead of arriving as a page of HTML.
Caching
Two headers, decided per file from its name:
| File | Sent |
|---|---|
app-4f2a91bc.js | max-age=31536000, immutable — a year in the visitor's browser. |
index.html | max-age=0, must-revalidate in the browser, cached at the edge. The browser asks on every view and gets a 304. |
A file is frozen for a year only when its name carries a content hash: a run of at least eight alphanumerics including a digit, before the extension. app-4f2a91bc.js and index.a1b2c3d4.css qualify; logo-2024.png is too short and about-something.html has no digit. If your assets are not being frozen, that is the naming to check in your build.
There is no purge step. A rollback lands on a build the edge still holds.
The ETag is the file's content hash, so a revalidation costs no read.
A response served from a deployment says which build answered and whether the edge had it:
x-ae-deployment: 8f21c4a90b6e… # the deployment id that served this response
x-ae-cache: HIT # HIT from the edge cache, MISS when read from storageWhen a deploy does not seem to have landed, curl -sI the page and compare x-ae-deployment with the id altengine static info reports as live.
Running locally
The same CLI serves a directory the way this service serves a deployment:
altengine dev --static ./dist
# → http://127.0.0.1:9191 API + console
# → http://127.0.0.1:9192 the siteThere is no local deploy step: the directory is read on every request, so a rebuild shows up on the next reload. The site listens on its own port, which keeps root-absolute URLs (/assets/app.js) working and puts the site and the API on different origins — the same split as production, so a fetch that CORS will refuse locally will refuse deployed.
Resolution, content types and ETags match this page. Two things differ: every response is no-cache rather than a year, and the trailing-slash redirect is a 302. The startup banner counts how many of your assets will be cached for a year once deployed.
--spa is detected from the directory when you do not pass it, by the same rule your first deployment uses, and the banner says what it decided. Pass --spa or --spa=false to set it yourself.
The API
The CLI is the intended interface, and the console deploys a dropped folder. For your own pipeline, a deploy is three calls — send a manifest, upload what is missing, activate:
| Endpoint | Does |
|---|---|
POST /v1/static/{instance}/deployments | Send {files: {"/index.html": {hash, size}, …}, message?}. Replies with the deployment id and signed upload links for only the files not already stored. |
GET /v1/static/{instance}/deployments/{id}/uploads?cursor=… | The next page of those links. A reply carries at most 100; page until cursor is null. |
POST /v1/static/{instance}/deployments/{id}/activate | Publish it. Verifies every file arrived first. |
GET /v1/static/{instance}/deployments | History, newest first. limit (25 by default, at most 200), cursor, and status (pending, ready or failed). Each deployment carries its number and versioned url. |
GET /v1/static/{instance}/deployments/{id} | One deployment, and whether it is live: {deployment, active}. |
GET /v1/static/{instance}/deployments/{id}/files | What is in one build. prefix, limit (100 by default, at most 200), cursor. |
GET /v1/static/{instance}/site | The URL, the live deployment, and totals. |
GET /v1/static/marketing/deployments
{ "deployments": [
{ "id": "c09d3e71a2f4…", "number": 8, "url": null, "status": "pending", … },
{ "id": "8f21c4a90b6e…", "number": 7, "url": "https://marketing--v7-web.altengine.app/",
"status": "ready", "file_count": 142, "total_bytes": 3251200, "message": null, … } ],
"cursor": null,
"active": "8f21c4a90b6e…",
"url": "https://marketing-web.altengine.app/" }url is null until a deployment is ready.
Hashes are SHA-256 of the file's bytes, hex. Upload links expire an hour after they are signed. Creating, uploading and activating need a write grant; reading history needs read. Every call takes an organization API key — an end-user identity token is refused.
Limits and settings
| Setting | What it does |
|---|---|
| SPA mode | Serve index.html with a 200 for unmatched page requests. Set from the shape of your first deployment. |
| Clean URLs | Resolve /about to /about.html. On by default. |
| 404 page | The path served for a miss when SPA mode is off. /404.html by default. |
| Maximum file size | The largest single file a build may contain. 25 MB by default, up to 100 MB. Anything larger belongs in Blob and should be linked to. |
| Files per deployment | 5,000, and not a setting. |
| Size per deployment | 1 GiB in total, and not a setting. |
| File list | The deployment's paths, hashes and content types, at most 5 MiB once serialized. Only very long paths reach it before the file count does. |
| Paths | At most 1,024 bytes each. A path containing .. or \, or ending in /, is refused. |
| Deployments to keep | keepDeployments: the newest ready deployments kept, 10 by default, 1–50. The live one is always kept, however old. Older ones are deleted after each deploy, with the files no remaining build references; lowering the setting deletes the extras when you save. |
| Rate limit | Bounds deploys and API calls: 600 a minute per instance by default, and a value you set can only lower it. Over it, a call returns 429 RATE_LIMITED. It does not apply to serving the site. |
What it costs
Requests, stored bytes, and uploads. Bandwidth is not metered.
Storage is deduplicated across builds — ten deployments of a 40 MB site that changed one page store about 40 MB — sampled hourly and averaged over the month. Every served request is billed, cache hit included: a million page views is about a dollar. A deploy is billed one upload per file it sends (at least one per deploy), and only files the instance does not already hold are sent. See pricing for the numbers.