Bundlr + Arweave + NextJS: Permanent and Decentralized Storage
Blockchain technology has had a major impact on data storage and access. Arweave, a decentralized data storage platform built on…

Bundlr + Arweave + NextJS: Permanent and Decentralized Storage
Blockchain technology has had a major impact on data storage and access. Arweave, a decentralized data storage platform built on blockchain, offers a secure and permanent solution for storing digital assets such as images, PDFs, and websites.
Arweave utilizes a wallet and token system for uploading data to its permanent storage, which is designed to be more robust than IPFS’s caching mechanism. However, the requirement of using an Arweave wallet may limit its user base, as many individuals in the web3 space prefer to choose their own wallet depending on the blockchain it’s built on.
Thankfully, we have a solution for that:
Introducing Bundlr Protocol
Bundlr is a protocol or, as some refer to it, an adapter for Arweave. It enables you to connect any blockchain of your choice, whether it be a Layer 1 or Layer 2 blockchain such as Solana, Polygon, or Ethereum, with any token you desire, whether it be native or non-native. With Bundlr, you no longer have to worry about limiting your user base by requiring them to use an Arweave wallet.
Thanks to its JavaScript library, developers can easily upload data to Arweave without needing an Arweave wallet, making it more accessible and user-friendly for a wider range of individuals.
How Does it Work?
Bundlr operates based on an architecture that allows users create a “Bundlr node” associated to their wallet address. In this Node users can transfer their funds from their existing wallet (such as MetaMask or Phantom) to their Bundlr Node before they can start using it.
With this, you can tell Bundlr to expect funds from a blockchain and the token you want and will also persist the user wallet address in order to fetch it funds in the future.
Let’s start coding
I did a small code example with Nextjs (I’m using Next 12)
import { WebBundlr } from "@bundlr-network/client";
import { useState, useEffect, useRef } from "react";
function MyApp({ Component, pageProps }) {
const [bundlrInstance, setBundlrInstance] = useState();
const [balance, setBalance] = useState(0);
const bundlrRef = useRef();
useEffect(() => {
initialiseBundlr();
}, []);
async function initialiseBundlr() {
await ethereum.request({ method: "eth_accounts" });
const provider = new providers.Web3Provider(window.ethereum);
await provider._ready();
/* here i can choose to use the blockchain I need,
it can be a devnet or a mainnet */
const bundlr = new WebBundlr(
"<https://devnet.bundlr.network>",
"matic",
provider,
{ providerUrl: "<https://matic-mumbai.chainstacklabs.com>" }
);
await bundlr.ready();
setBundlrInstance(bundlr);
bundlrRef.current = bundlr;
fetchBalance();
}
async function fetchBalance() {
const bal = await bundlrRef.current.getLoadedBalance();
console.log("bal: ", utils.formatEther(bal.toString()));
setBalance(utils.formatEther(bal.toString()));
}
return <Component {...pageProps} />;
}
export default MyApp;
In this example, I created a new Bundlr instance; this instance, or node, will be used to process transactions on the Bundlr network.
You can see that after I create the Bundlr Instance, I fetch the balance, but you might be thinking “But we are creating a new instance”, and you’d be right. Bundlr linked your wallet to the Node when creating the Bundlr instance, so every time you “create” a new node, Bundlr looks for any Node that has your wallet address linked and uses that one.
Funding a Node
For funding a Node, we gotta tweak a little bit our _app.js to pass the bundlrInstance through child components, for this, i will use React Context just to keep it simple
import { WebBundlr } from "@bundlr-network/client";
import { useState, useEffect, useRef } from "react";
import { MainContext } from "context";
function MyApp({ Component, pageProps }) {
const [bundlrInstance, setBundlrInstance] = useState();
const [balance, setBalance] = useState(0);
const bundlrRef = useRef();
useEffect(() => {
initialiseBundlr();
}, []);
async function initialiseBundlr() {
await ethereum.request({ method: "eth_accounts" });
const provider = new providers.Web3Provider(window.ethereum);
await provider._ready();
const bundlr = new WebBundlr(
"<https://devnet.bundlr.network>",
"matic",
provider,
{ providerUrl: "<https://matic-mumbai.chainstacklabs.com>" }
);
await bundlr.ready();
setBundlrInstance(bundlr);
bundlrRef.current = bundlr;
fetchBalance();
}
async function fetchBalance() {
const bal = await bundlrRef.current.getLoadedBalance();
console.log("bal: ", utils.formatEther(bal.toString()));
setBalance(utils.formatEther(bal.toString()));
}
return (
<MainContext.Provider
value={{
initialiseBundlr,
bundlrInstance,
balance,
fetchBalance,
}}
>
<Component {...pageProps} />
</MainContext.Provider>
);
}
export default MyApp;
We this, We can now create our Fund component
import { useState, useContext, useEffect } from "react";
import BigNumber from 'bignumber.js'
import { MainContext } from "context";
const Funds = () => {
const { balance, bundlrInstance, fetchBalance } = useContext(MainContext);
const [amount, setAmount] = useState()
function parseInput (input) {
const conv = new BigNumber(input).multipliedBy(bundlrInstance.currencyConfig.base[1])
if (conv.isLessThan(1)) {
console.log('error: value too small')
return
} else {
return conv
}
}
async function fundWallet() {
if (!amount) return
const amountParsed = parseInput(amount)
try {
await bundlrInstance.fund(amountParsed)
fetchBalance()
} catch (err) {
console.log('Error funding wallet: ', err)
}
}
useEffect(() => {
fetchBalance()
}, [balance])
return (
<div>
<div>
<p>Total Balance</p>
<h3>{Math.round(balance * 100) / 100} MATIC</h3>
</div>
<div>
<p>Fund Wallet</p>
<input
placeholder="amount"
className={`text-primary ${inputStyle}`}
onChange={(e) => setAmount(e.target.value)}
/>
<button onClick={fundWallet}>
Send transaction
</button>
</div>
</div>
);
};
export default Funds;
As you can see on fundWallet() We first need to parse the amount We want to fund to BigNumber, and to do a proper conversion, we gotta use the currencyConfig by bundlerInstance
As simple as it is, you can now use those funds to upload a file to Arweave through Bundlr, let’s get that done!
For uploading a file, We need to first calculate the cost of the file, for this, Bundlr has a function getPrice(bytes) that expects the file size in bytes, this will give if a cost in BigNumber, the function will look a little bit like this:
async function checkUploadCost(bytes) {
if (bytes && bundlrInstance) {
const cost = await bundlrInstance.getPrice(bytes);
const formattedCost = utils.formatEther(cost.toString());
setFileCost(formattedCost);
}
}
Notice that, i’m only parsing the cost for showing it to the user, but getPrice will return the price in the currency you set when you create the Bundlr instance.
So let’s wrap up our upload file component:
import { useState, useContext, useEffect } from "react";
import { MainContext } from "context";
import { utils } from "ethers";
import Funds from "../components/Funds";
export default function UploadFile() {
const { bundlrInstance } = useContext(MainContext);
const [file, setFile] = useState();
const [localVideo, setLocalVideo] = useState();
const [fileCost, setFileCost] = useState();
const [fileSize, setFileSize] = useState(0);
const [URI, setURI] = useState();
function onFileChange(e) {
const file = e.target.files[0];
if (!file) return;
setFileSize(file.size);
if (file) {
const video = URL.createObjectURL(file);
setLocalVideo(video);
let reader = new FileReader();
reader.onload = function (e) {
if (reader.result) {
setFile(Buffer.from(reader.result));
}
};
reader.readAsArrayBuffer(file);
}
}
useEffect(() => {
checkUploadCost(fileSize);
}, [fileSize]);
async function checkUploadCost(bytes) {
if (bytes && bundlrInstance) {
const cost = await bundlrInstance.getPrice(bytes);
const formattedCost = utils.formatEther(cost.toString());
setFileCost(formattedCost);
}
}
async function saveVideo() {
const tags = [{ name: "Content-Type", value: "text/plain" }];
const videoTags = [{ name: "Content-Type", value: "video/mp4" }];
try {
let txVideo = await bundlrInstance.uploader.upload(file, videoTags);
try {
setURI(`http://arweave.net/${txVideo.data.id}`);
} catch (err) {
console.log("error uploading video with metadata: ", err);
}
} catch (err) {
console.log("Error uploading video: ", err);
}
}
return (
<div>
<div>
<p>Add Video</p>
<div>
<input type="file" onChange={onFileChange} />
</div>
{localVideo && (
<video key={localVideo} width="520" controls className={videoStyle}>
<source src={localVideo} type="video/mp4" />
</video>
)}
{fileCost && (
<h4>Cost to upload: {Math.round(fileCost * 1000) / 1000} MATIC</h4>
)}
{URI && (
<div>
<p>
<a href={URI}>{URI}</a>
</p>
</div>
)}
<button onClick={saveVideo}>
Save Video
</button>
</div>
<Funds />
</div>
);
}
Ok, this is a BIG component, most of it is just input components and handlers, which isn’t a big deal, but I want to focus on saveVideo()
This function does exactly what it says it does: it saves the video to Arweave. To do so, we must utilize the upload() function from the bundlrInstance. As the name implies, it uploads the file to Arweave via Bundlr and returns the file's id in Arweave, as simple as that, and then we simply generate the url using that id and that's it! We successfully uploaded a file to long-term storage.
Take that, AWS! 🎉🎉🎉
Conclusion
In conclusion, the Bundlr protocol and the Arweave network offer numerous benefits to users who are looking for a decentralized and permanent solution for data storage. With Bundlr, users are no longer limited by the requirement of using an Arweave wallet, making it easier and more accessible to a wider range of individuals.
Furthermore, the permanent and decentralized nature of Arweave provides the added benefit of protecting users’ data from censorship and government interference, ensuring that their digital assets are safe and secure for years to come. In an era where privacy and security are becoming increasingly important, the Bundlr protocol and Arweave network provide a valuable solution for those looking to store and protect their digital assets.
Happy coding :)
메타데이터
- post_id
- 54775d6bdee9
- slug
- bundlr-arweave-nextjs-permanent-and-decentralized-storage-54775d6bdee9
- url
- https://medium.com/@aguiarjesus/bundlr-arweave-nextjs-permanent-and-decentralized-storage-54775d6bdee9
- canonical_url
- https://medium.com/@aguiarjesus/bundlr-arweave-nextjs-permanent-and-decentralized-storage-54775d6bdee9
- author_url
- https://medium.com/@aguiarjesus
- status
- ok
- fetched_at
- 2026-07-26 03:39:34