Docs · Publishing
Push a folder. Get a URL.
Publishing uploads your files, adds and pins them to IPFS, and writes a pointer record in your name's zone. Updates are instant and free, and the address of your site is the hash of your site — so nobody serving it, including us, can change a byte of it without the address changing.
What happens, stage by stage
Validate, then charge
Paths, extensions, sizes and the zip manifest are all checked before anything is written to disk or any credit is spent. A zip's declared sizes are re-checked against the real decompressed bytes.
Queue a job
The upload is stored and a
publish job is queued. A worker claims it with
SELECT … FOR UPDATE SKIP LOCKED, polling every few
seconds, retrying up to three times with backoff.
Add and pin
ipfs add -Q --cid-version
1 --pin — recursive for a folder. Add and pin are the same
command; pinning is the whole point, since IPFS keeps nothing alive on
its own.
Point the zone at it
Existing
A and TXT records at that label are removed
and replaced with the new pointer. The zone serial bumps and the zone
is re-signed.
The records you end up with
<label> TXT "ipfs=bafkrei...." # the content pointer
<label> A <gateway address> # only when a gateway IP is configured
There is no _dnslink record anywhere in this system — the
pointer is that TXT, in that form. Arweave publishes use
ar=<txid> in the same slot; when both exist,
ipfs= wins.
CIDs are v1 base32. A malformed pointer is ignored rather than guessed at.
The limitation you need to know before you build on this
What works: https://<name>.hns.one/ serves
the right bytes within a few seconds of publishing. Measured across
eight consecutive claim-and-publish runs on the live tenant plane:
claim 0.50–0.67 s, publish accepted 0.34–0.51 s, URL live
0.85–4.46 s. End to end 1.5–5.0 s, and the size of the site
barely moves it.
What does not work: the same names answer NXDOMAIN
through our own resolver — natively, in Wildroot, at
https://<name>/ — and still do twelve minutes
later.
What we have ruled out: it is not a DNSSEC lag — queries with checking disabled return NXDOMAIN too. It is not a missing zone merge — the records are present in the tenant zone file, whose modification time tracks each publish, and the nameserver is configured to read that file. An earlier version of this note guessed the merge was the cause; it was wrong, and it is corrected here rather than quietly deleted.
Where that leaves it: the gap is between the file and what the nameserver answers. The mechanism is not identified. We are not publishing a guess as a cause.
What it means for you: the deliverable of a publish today is a
working <name>.hns.one URL. If your reason for being
here is the native name resolving in a Handshake-aware client, this is
the open issue standing between you and that, and it is the top item on
our own issue list.
The API
| Endpoint | Method | What it does |
|---|---|---|
/api/publish/targets | GET | Which names this key or account may publish to, plus the current limits |
/api/publish | POST | Multipart upload; returns the FQDN, the native URL and the gateway URL |
/publish/ | POST | The HTML form. Session auth, works with no JavaScript, one file at a time |
Authentication
On app.hns.one the usable scheme is a signed Nostr
authorization event — NIP-98, which is a BIP-340 Schnorr signature over
secp256k1, made with your name's control key:
Authorization: Nostr <base64 of a kind-27235 event>
The event must carry a u tag equal to
https://app.hns.one plus the full request path, a method
tag matching the HTTP method, and a created_at within 60
seconds. The multipart body is not signed, so there is no
payload tag — which name you are publishing to is settled by
resolving the target against your key, not by the body.
Each authorization event is single-use. A replay is refused with
401 authorization event already used, so you sign per
request.
A name your key does not control returns 404, never
403 — confirming a name exists here would leak which names
other keys hold.
Request shape
# one file
-F name=alice.yourtld -F file=@index.html
# a zip, unpacked as a site
-F name=alice.yourtld -F file=@site.zip
# a folder, file by file
-F name=alice.yourtld \
-F files=@index.html -F files=@style.css \
-F paths_json='["index.html","style.css"]'
paths_json is a JSON array of relative paths in the same
order as the files parts. A site or zip publish must contain
an index.html at its root; a single-file publish need
not.
The API is curl-able, but you have to produce the signed header yourself — nothing in the published tooling mints a NIP-98 header for you, and each header works exactly once. In practice the shipped path is Wildroot's publish page, which does the signing in its main process so the secret key never crosses into page context.
If you are writing your own client: sign a kind-27235 event with the
tags above, base64 the JSON, and send it as the
Authorization header. That is the whole contract.
Limits
| Limit | Value |
|---|---|
| Single file | 25 MiB |
| Total per publish | 50 MiB |
| Files per publish | 2,000 |
| Relative path length | 400 characters |
| Free claims per TLD per owner | 5 |
| Cost, control-key path | free |
Allowed extensions are a fixed list of 32 web-safe types —
.html .htm .css .js .mjs .json .txt .md .markdown .xml .svg .png
.jpg .jpeg .gif .webp .avif .ico .pdf .woff .woff2 .ttf .otf .wasm
.webmanifest .map .mp3 .mp4 .webm .ogg .wav .vtt — matched by
extension, not by sniffing content. A .zip is handled
earlier, as "unpack me as a site".
Junk is skipped silently: dotfiles, __MACOSX,
Thumbs.db. Hostile paths — .. segments, control
characters, duplicates — are refused rather than sanitised.
The edge caps request bodies at 25 MB, so a publish between 25 and 50 MiB is rejected before the application sees it. Plan against 25 MB, not 50.
Updating, rolling back, and taking things down
Publishing again to the same name replaces the pointer records. The old content stays pinned, so an old CID keeps resolving; nothing in the product walks you back to it, but you can point a record at it by hand in the DNS editor.
The dashboard lists your ten most recent publications with their CIDs and timestamps.
No endpoint, no button, no job removes published content. The only way to stop serving a site is to delete its pointer record in the DNS editor, which asks you to confirm that you are removing the pointer to your published site.
Deleting the record stops us pointing at the content. It does not unpin it, here or on any other node that has fetched it. Content addressing cuts both ways: publish nothing you would need to be able to retract.
Durability
We pin on our own IPFS node at the moment of publishing, and garbage collection is not enabled — pins are held indefinitely, by design and by omission. Content also lives on any other node that has fetched it, including the one embedded in Wildroot.
We run no redundancy across independent pinning providers, and there
is no reconciliation between the several IPFS nodes in this system —
pushes only ever add. Your site survives exactly as long as some node
keeps it pinned. If the content matters, ipfs pin add
<cid> on a node of yours, or with a pinning service, and
you are no longer relying on us for durability at all.