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.

How fast a publish goes live

Measured across eight consecutive claim-and-publish runs on the live tenant plane, each one claiming a brand-new name first — the true cold start, not a re-publish:

StageRange
Claim a new name (/api/identity/claim)0.50 – 0.67 s
Publish accepted (/api/publish)0.34 – 0.51 s
Gateway URL serves the new page0.85 – 4.46 s
End to end1.5 – 5.0 s

Site size barely moves it: a 95-byte page and a five-file, 700 KB site land in the same band. The variance is the job worker's poll interval, not the upload or the IPFS add.

Which resolver you ask changes the answer

The authoritative nameservers for a delegated TLD — ns1/ns2.hns.one, which is what a Handshake-aware client reaches after reading the TLD's NS records from the chain — return the new TXT ipfs= pointer immediately after a publish. Verified directly against port 53.

Any resolver that sets the DNSSEC OK (DO) bit will be told the name does not exist. This is not caching and not a validation failure downstream — our own authoritative server produces it. When DO is set, nsd.py answers exclusively out of the signed zone file and returns a signed NXDOMAIN for anything absent from it, with no fall-through to the merged records. Tenant-written records are not in that file, because signing reads the operator zone only and the tenant plane holds no signing key.

Consequences, stated exactly: Wildroot resolves these names (its pointer query sets DO=0, and it proves the delegation against the chain instead), and the gateway URL works for everyone. A third-party DNSSEC-aware resolver will not see the name. Setting CD does not help — it suppresses validation at the recursive, and the denial was manufactured upstream of it.

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.