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
| statice | Deploy 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 --name | The same, named after the folder. |
| statice --isolated | Write the cross-origin isolation marker into the folder, then deploy. |
| statice --verify | Re-upload every blob, then deploy. The way back when a site looks broken. |
| statice --dry-run | List exactly what would be published, and what name it would claim or update, then stop. Nothing leaves the machine; no key needed. |
| statice key | Store 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 names | Every 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
| .statice | The label, in your project. Public — commit it if you like. |
| ~/.statice/key | The 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/deployed | A 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
| host | Cache-Control |
|---|---|
| <root>.statice.app | public, max-age=31536000, immutable |
| <name>.statice.app | no-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 MB | one file. Sized so ffmpeg's core wasm (~31 MB) and a Godot export (~43 MB) fit whole. |
| 512 MB | one deploy, summed over distinct files. |
| 1,000 | files in one deploy. |
| 1,024 bytes | one 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.