HNS.ONE

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

A freshly published name serves over the gateway URL and answers NXDOMAIN natively

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

EndpointMethodWhat it does
/api/publish/targetsGET Which names this key or account may publish to, plus the current limits
/api/publishPOST 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.

There is no ready-made command-line publisher

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

LimitValue
Single file25 MiB
Total per publish50 MiB
Files per publish2,000
Relative path length400 characters
Free claims per TLD per owner5
Cost, control-key pathfree

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 50 MiB total is not reachable through the front door

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.

There is no unpublish, and nothing ever unpins

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.

Pin the CID somewhere you control

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.