Pinata / IPFS
Our NFT metadata and media storage, including the storage target behind property NFT metadata.
Docs: https://docs.pinata.cloud
Environment
PINATA_JWT= # SECRET — the modern auth, use this
PINATA_API_KEY= # legacy pair
PINATA_SECRET_KEY= # SECRET, legacy
NEXT_PUBLIC_PINATA_GATEWAY= # your-gateway.mypinata.cloud
⚠️ The portfolio contains PINATA, PINATA_APIKEY, PINATASECRET and
PINATA_SECRET_KEY as separate names across repos. Standardise on PINATA_JWT alone —
the key/secret pair is legacy and the JWT replaces both.
Uploading
// Server-side only — the JWT can pin to your paid account.
const form = new FormData();
form.append("file", new Blob([buffer]), "house-42.png");
const res = await fetch("https://api.pinata.cloud/pinning/pinFileToIPFS", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.PINATA_JWT}` },
body: form,
});
const { IpfsHash } = await res.json(); // the CID
JSON metadata: POST https://api.pinata.cloud/pinning/pinJSONToIPFS.
Never expose the JWT to the browser. Anyone with it can pin arbitrary data to your billed account. Upload through a route handler that authorises the user first.
tokenURI
Store ipfs://<CID> onchain, not a gateway URL. Gateways change; the CID does not,
and a hardcoded gateway in an immutable contract is a permanent dependency on one vendor.
function tokenURI(uint256 id) public view override returns (string memory) {
return string.concat("ipfs://", _cid[id]);
}
Resolve to a gateway in the frontend:
export const ipfsToHttp = (uri: string) =>
uri.replace("ipfs://", `https://${process.env.NEXT_PUBLIC_PINATA_GATEWAY}/ipfs/`);
Use the dedicated gateway. The public gateway.pinata.cloud is heavily rate-limited
and slow enough that NFT images visibly fail to load in a grid.
Phased reveal
HouseNFT reveals metadata in phases as an auction progresses. The pattern:
- Pin the full metadata set up front, keep the CIDs private.
- Store only the current phase's CID onchain.
- Advance the phase with an owner-only setter as the auction moves.
Content on IPFS is public the moment it is pinned — the CID is the only thing hiding it, and CIDs are guessable if derived predictably. If a phase must be genuinely secret, do not pin it until reveal, and commit to a hash onchain instead.
Gotchas
- JWT in the browser — billable account takeover.
- Public gateway is too slow for a grid of images.
- Gateway URL onchain is a permanent vendor lock. Use
ipfs://. - Pinned ≠ private. Anything pinned is retrievable by CID.
- Unpinning does not delete — other nodes may still hold it.
- Large files need the resumable upload endpoint, not a single POST.
- Metadata JSON must match the ERC-721 schema (
name,description,image) or marketplaces render nothing.