Hi devs — Nacho here from the BWS (Blockchain Web Services) team. We just shipped IPFS.NINJA, a managed IPFS pinning service, and one of the most common questions we get is: “How do I update content on IPFS without changing the URL?”
The answer is IPNS (InterPlanetary Name System) — and it's one of those features that sounds confusing until you see it in action.
Full disclosure: I work on this product. This post is a transparent walkthrough from the team that built it, with concrete examples you can copy-paste.
The problem IPNS solves
IPFS is content-addressed: change a single byte in a file, and the CID changes. That's a feature — immutability and verifiability are the whole point.
But it's also a problem when you want to update something:
- Your dApp config evolves → new CID → every client needs the new link.
- Your NFT metadata levels up → new CID → your smart contract's
tokenURIis now pointing at stale content. - You redeploy your IPFS-hosted website → new CID → you have to tell users the new URL.
IPNS gives you a stable, shareable address that you control and can re-point at any time.
How it works (the short version)
When you create an IPNS name, an Ed25519 keypair is generated. The hash of the public key becomes your IPNS address (a string starting with k51...). To publish, you sign a record saying “this name points to CID X” and broadcast it to the IPFS DHT. To resolve, anyone can query the DHT for the latest signed record.
Key properties:
-
Stable: the
k51...address never changes. - Mutable: you can re-publish to point it at a new CID anytime.
- Owned: only the holder of the private key (you) can update it.
- Decentralized: any IPFS gateway can resolve it.
Use cases that actually matter
-
Websites on IPFS — redeploy gives a new CID, but
ipns://k51...always serves the latest. -
NFT metadata that evolves — set
tokenURI = ipns://k51...once; update metadata as the asset evolves. - App config files — update without redeploying the app.
- Data feeds / daily datasets — publish under a stable address consumers can poll.
-
DNSLink — connect a real domain (
yourdomain.com) to an IPNS name.
Setting up an IPNS name on IPFS.NINJA
IPNS is included on the Bodhi ($5/mo) and Nirvana ($29/mo) plans (3 names / 100 publishes per month, and 10 names / 1,000 publishes respectively). Records are auto-republished every 12 hours so they stay alive on the network.
From the dashboard:
- Go to Hosting → IPNS.
- Click Create name, give it a label (e.g.
my-website). - Copy the resulting
k51...address — this is your permanent shareable handle. - Click Publish, paste the CID you want it to point to, and you're live.
Or from the API:
# Create the IPNS key
curl -X POST https://api.ipfs.ninja/ipns/keys \
-H "X-Api-Key: bws_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"name": "my-website"}'
# Publish a CID to it
curl -X POST https://api.ipfs.ninja/ipns/publish \
-H "X-Api-Key: bws_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"ipnsName": "k51qzi5uqu5dlvj2bv6...", "cid": "bafybei..."}'
Resolve from any gateway:
https://ipfs.ninja/ipns/k51qzi5uqu5dlvj2bv6...
https://dweb.link/ipns/k51qzi5uqu5dlvj2bv6...
ipns://k51qzi5uqu5dlvj2bv6...
Example 1: Static site deployments
A tiny CI script that uploads dist/ and updates IPNS:
# Upload build output
CID=$(curl -s -X POST https://api.ipfs.ninja/upload/new \
-H "X-Api-Key: $IPFS_NINJA_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"content\": $(cat dist/index.html | base64 -w0 | jq -Rs .), \"description\": \"Website v2.1\"}" \
| jq -r '.cid')
# Re-point IPNS
curl -X POST https://api.ipfs.ninja/ipns/publish \
-H "X-Api-Key: $IPFS_NINJA_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"ipnsName\": \"k51qzi5uqu5dlvj2bv6...\", \"cid\": \"$CID\"}"
In GitHub Actions:
- name: Upload to IPFS and publish IPNS
run: |
CID=$(curl -s -X POST https://api.ipfs.ninja/upload/new \
-H "X-Api-Key: ${{ secrets.IPFS_NINJA_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"content": '"$(cat build/output.json)"', "description": "Deploy ${{ github.sha }}"}' \
| jq -r '.cid')
curl -X POST https://api.ipfs.ninja/ipns/publish \
-H "X-Api-Key: ${{ secrets.IPFS_NINJA_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"ipnsName": "${{ vars.IPNS_NAME }}", "cid": "'"$CID"'"}'
Your users always hit ipns://k51... (or your DNSLink domain). The deployment URL never changes.
Example 2: Mutable NFT metadata
This is the killer use case for game items, evolving art, dynamic profiles — anywhere on-chain assets need to change without redeploying the contract.
Your smart contract sets tokenURI to an IPNS address once:
tokenURI = "ipns://k51qzi5uqu5dlvj2bv6..."
When the NFT evolves (e.g. game item levels up):
const newMetadata = {
name: "Dragon Sword",
description: "A legendary weapon — Level 5",
image: "ipfs://QmNewImageCID...",
attributes: [
{ trait_type: "Level", value: 5 },
{ trait_type: "Damage", value: 150 }
]
};
// 1. Upload the new metadata
const uploadRes = await fetch("https://api.ipfs.ninja/upload/new", {
method: "POST",
headers: { "Content-Type": "application/json", "X-Api-Key": "bws_..." },
body: JSON.stringify({ content: newMetadata, description: "Dragon Sword v5" })
});
const { cid } = await uploadRes.json();
// 2. Re-point the IPNS name — tokenURI stays the same!
await fetch("https://api.ipfs.ninja/ipns/publish", {
method: "POST",
headers: { "Content-Type": "application/json", "X-Api-Key": "bws_..." },
body: JSON.stringify({ ipnsName: "k51qzi5uqu5dlvj2bv6...", cid })
});
No contract upgrade. No new CID to coordinate. Marketplaces and wallets that resolve IPNS will reflect the new state.
Example 3: Connect your domain with DNSLink
You can point a real domain at an IPNS name with a single TXT record:
_dnslink.yourdomain.com TXT "dnslink=/ipns/k51qzi5uqu5dlvj2bv6..."
Verify with dig:
dig +short TXT _dnslink.myapp.com
# "dnslink=/ipns/k51qzi5uqu5dlvj2bv6..."
Then access via:
https://ipfs.ninja/ipns/myapp.com
ipns://myapp.com (in IPFS-aware browsers like Brave)
Once the TXT record is set, you never have to touch DNS again. Just publish new CIDs to the IPNS name.
Honest gotchas
- Propagation isn't instant. Publishing to the DHT can take up to ~60 seconds.
- Records expire after 48h if not republished. We auto-republish every 12h to keep yours alive.
- Inactive names (no publishes for 90 days) stop being republished — publish anything once to reactivate.
- DNSLink DNS changes can take up to 24h to propagate globally. After that, switching CIDs is instant; you don't touch DNS again.
Try it
- IPNS docs: ipfs.ninja/docs/api/ipns
- Sign up: ipfs.ninja (Free Dharma plan to test, IPNS unlocks on Bodhi at $5/mo)
If you're using IPNS in a way I didn't cover — collaborative docs, evolving on-chain art, machine-readable status pages — I'd love to hear about it in the comments. We're actively shaping the roadmap based on real use cases.
— Nacho, BWS team
Top comments (0)