statice

Reference

Commands, files, what gets published, and the rules the server follows. Everything here is what the CLI and the Worker actually do.

Getting a key

statice is not self-serve. Every call to the API carries an access key, keys are issued by hand, and there is no page anywhere that will mint you one. Email access@statice.run and ask, saying roughly what you want to publish.

statice key stores the key you were given in ~/.statice/key; it does not request one, and everything below assumes it is already set. The key is still not an account — see Two credentials.

Commands

staticeDeploy this folder to a hash URL. With a .statice file present, also update that name.
statice --name "…"Deploy, then claim a short name — or update it if this folder already has one.
statice --nameThe same, named after the folder.
statice --isolatedWrite the cross-origin isolation marker into the folder, then deploy.
statice --verifyRe-upload every blob, then deploy. The way back when a site looks broken.
statice --dry-runList exactly what would be published, and what name it would claim or update, then stop. Nothing leaves the machine; no key needed.
statice keyStore the access key you were issued on this machine. Prompts, or reads stdin.
statice claim <label>Adopt a name on a second machine, given its token.
statice namesEvery label with a token on this machine, and its folder.
statice retire [label]Take a label out of service. The record survives, so nobody else can take it — and the token stays on this machine, so statice claim <label> followed by a deploy revives it.

Files

.staticeThe label, in your project. Public — commit it if you like.
~/.statice/keyThe access key you were issued. One per machine.
~/.statice/slugs/<label>The name's token, and the folder it was claimed in. Losing it loses the name, permanently.
~/.statice/deployedA log of every deploy — and the only way back to a 52-character URL nobody can retype.

Everything under ~/.statice is 0600 inside a 0700 directory. STATICE_KEY and STATICE_TOKEN override the files, for machines with no durable home directory. STATICE_API points the CLI at a different API; it must be HTTPS, or plain HTTP on a loopback address only, since the key rides on every call.

Two credentials

The access key is admission: one per caller, on every API call, and handed out by a person at access@statice.run. The name token is possession: one per label, generated locally. The server stores only its SHA-256, which is what the claim sends; the token itself is sent over HTTPS with each update, check and retirement it authorizes, compared against that hash, and never written down server-side.

They are never merged. A key that also owned names would be an account, with every site linkable to every other and one leak losing all of them.

What gets published

The CLI walks the folder and admits files by extension. It prunes .git, node_modules and dot-directories — except a top-level .well-known — skips symlinks entirely, and drops CLAUDE.md and AGENTS.md at every depth: they are an admitted type, but they are written for an agent standing in the folder, never for the web.

Nothing leaves quietly. Every deploy names what the type table refused, and then, separately, what the walk dropped before the table saw it — each path beside the rule that dropped it, and a pruned tree named with a trailing slash and no count, because it is never opened.

Two things it does not do. It does not read .gitignore, or any other ignore file: what is in the folder is what is considered. And it drops dot-directories, not dot-files: a file like .env.local.json has an admitted extension and is published like any other. Source, JSON, sourcemaps and documents in admitted types all go up. statice --dry-run prints the exact list without sending anything, and the first deploy from a folder shows the same list before it asks.

Admitted: .html .htm .css .js .mjs .json .wasm .pck .data .map, the image set, the font set, the media set, plus .pdf .txt .xml .svg .webmanifest .vtt .md .obj.

Excluded, which is the more interesting half: .ts .tsx .jsx .yml .toml .sql .sh .py .pem .key .tfstate, and everything extensionless — except a fixed set of names directly under a top-level .well-known, which are admitted by name. That happens to stop a source tree being published — but it cannot stop a secret in an admitted type, and it was never a security control.

.map is admitted and flagged: every sourcemap is named back to you on every deploy in which it is new, because sourcemaps embed original source. Dropping .map from the table instead would break the production error reporting sourcemaps exist for.

Caching

hostCache-Control
<root>.statice.apppublic, max-age=31536000, immutable
<name>.statice.appno-cache

The hash host can promise a year because the root is in the name. Named hosts get no-cache plus a strong ETag — so a match is a 304 with no read, and a deploy is live the instant it lands. Not stale-while-revalidate, and not a small max-age: a deployer refreshing mid-write would otherwise get a page that is half old and half new, because assets resolve against whichever record their own request caught.

Limits

64 MBone file. Sized so ffmpeg's core wasm (~31 MB) and a Godot export (~43 MB) fit whole.
512 MBone deploy, summed over distinct files.
1,000files in one deploy.
1,024 bytesone path.

Ranges and media

One range per request is honored; every other shape of the header gets the full 200. If-Range is enforced against the strong ETag, so a client resuming yesterday's download never gets today's bytes spliced onto it. Safari probes <video> with Range: bytes=0-1 and will not play without a 206 — which is why the media set works at all.

Path resolution

A path ending in / tries index.html then index.htm. A path whose directory form exists gets a 301. There is no 404.html, no SPA fallback and no extensionless lookup. If you need client-side routing, serve it from index.html and route in the browser.

One cookie jar

Every hash URL and every name is its own origin, so localStorage, IndexedDB and service workers are separate per site. Cookies are not: statice.app is not on the Public Suffix List, so a page on any *.statice.app host can set a cookie with domain=.statice.app that every other site under it receives. Nothing served here can prevent that. Do not put anything cookie-authenticated on a statice.app host.

When something looks broken

A plain redeploy of unchanged bytes answers from a single operation without checking a single file — which is the point, and also means it will not repair a file that has been taken down. statice --verify is what re-uploads everything. Reach for it, not for a plain redeploy.

Hash URLs are permanent: manifests are never deleted and there is no garbage collector, so nothing you publish goes away on its own. A name can be retired with statice retire, which makes it 404 while keeping the record, so nobody else can serve their own content at a URL you already shared.