DEV Community

Cover image for Mutable Content on IPFS: A Practical Guide to IPNS (with Real Examples)
Nacho Coll
Nacho Coll

Posted on

Mutable Content on IPFS: A Practical Guide to IPNS (with Real Examples)

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 tokenURI is 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:

  1. Go to Hosting → IPNS.
  2. Click Create name, give it a label (e.g. my-website).
  3. Copy the resulting k51... address — this is your permanent shareable handle.
  4. 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..."}'
Enter fullscreen mode Exit fullscreen mode

Resolve from any gateway:

https://ipfs.ninja/ipns/k51qzi5uqu5dlvj2bv6...
https://dweb.link/ipns/k51qzi5uqu5dlvj2bv6...
ipns://k51qzi5uqu5dlvj2bv6...
Enter fullscreen mode Exit fullscreen mode

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\"}"
Enter fullscreen mode Exit fullscreen mode

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"'"}'
Enter fullscreen mode Exit fullscreen mode

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..."
Enter fullscreen mode Exit fullscreen mode

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 })
});
Enter fullscreen mode Exit fullscreen mode

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..."
Enter fullscreen mode Exit fullscreen mode

Verify with dig:

dig +short TXT _dnslink.myapp.com
# "dnslink=/ipns/k51qzi5uqu5dlvj2bv6..."
Enter fullscreen mode Exit fullscreen mode

Then access via:

https://ipfs.ninja/ipns/myapp.com
ipns://myapp.com   (in IPFS-aware browsers like Brave)
Enter fullscreen mode Exit fullscreen mode

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

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)