Avneesh Agarwal A fullstack web developer, designer, content creator, open source enthusiast, and student 🤓

How to create a DAO with Next.js and thirdweb

13 min read 3719

Editor’s note: This article was updated on 11 April 2022 to align with the release of thirdweb v2.

What is a DAO?

DAO stands for Decentralized Autonomous Organization. As it says in the name, a DAO is an organization without a single leader; instead, rules are encoded in the blockchain. Because of this, a DAO is completely transparent and everyone who participates has a stake. Large decisions are made via voting amongst those who own non-fungible tokens (NFTs) from the DAO, which grant membership.

Today, we are going to build our very own DAO using Next.js, thirdweb, MetaMask, and Alchemy. It will allow users to mint your DAO’s NFT, receive cryptocurrency via airdrops, and participate in the DAO’s polls. This tutorial will be written with just JavaScript, so you don’t need to know any Solidity.

Prerequisites

To understand and follow along with this tutorial, you should have the following:

  • Working knowledge of JavaScript, Next.js, and the blockchain
  • A MetaMask wallet
  • An account with Alchemy

Contents

Setup

We will begin by setting up a Next.js app with the following command:

npx create-next-app my-dao

Creating an Alchemy app

Next, head to Alchemy, sign in, click on Create App, and provide the required details. Make sure to use the same chain as the one you used in thirdweb – in our case, it is the Ethereum chain and the Rinkeby network.

Creating alchemy app

After the app is created, copy the HTTP API key.

Getting the wallet’s private key

In order to mint NFTs and perform certain scripts, we are going to need the wallet’s private key.

To access it, open the MetaMask browser extension and click on Account Details. You should see your private key here; export it and copy it somewhere safe.



Adding .env variables

Let’s add these variables in a .env file so we can access them later:

PRIVATE_KEY=<wallet_private_key>
ALCHEMY_API_URL=<alchemy_http_key>
WALLET_ADDRESS=<public_wallet_address>

Because we don’t want to push these to GitHub, be sure to add them in gitignore

Adding sign in functionality using MetaMask

In DApps, MetaMask is the most popular wallet used, so we will add MetaMask sign in with thirdweb.

We are going to need two packages from install:

npm i @thirdweb-dev/react @thirdweb-dev/sdk ethers # npm

yarn add @thirdweb-dev/react @thirdweb-dev/sdk ethers # yarn

Adding the thirdweb provider

We need to wrap our whole app in a thirdweb provider in order to access the login details and other information required for the components:

import "../styles/globals.css";
import {ThirdwebProvider } from "@thirdweb-dev/react";

function MyApp({ Component, pageProps }) {
  return (
    <ThirdwebProvider desiredChainId={activeChainId}>
      <Component {...pageProps} />
    </ThirdwebProvider>
  );
}
export default MyApp;

For authentication purposes, we also have to specify the type of authentication and the supported chain IDs. We are using MetaMask and the Rinkeby chain, so add the following as well:

import { ChainId, ThirdwebProvider } from "@thirdweb-dev/react";
const activeChainId = ChainId.Rinkeby;

Finally, pass these as props in the provider like so:

 <ThirdwebProvider desiredChainId={activeChainId}>
      <Component {...pageProps} />
  </ThirdwebProvider>

Adding the sign in component

Create a new folder called components in the root of the project and add a Login.js file to it:

import { useMetamask } from "@thirdweb-dev/react";
const Login = () => {
  const connectWithMetamask = useMetamask();
  return (
    <div>
      <button onClick={connectWithMetamask}>Sign in using MetaMask</button>
    </div>
  );
};
export default Login;

Thankfully, thirdweb provides a connectWallet function which we can use to add authentication!

Rendering the sign in component

Inside index.js, render the login screen if there is no address (if the user is not signed in):

const address = useAddress();
if (!address) {
  return ;
}

This will allow our users to sign in, but afterwards it just shows a blank screen. So, in the other return block, let’s show the user her address:


More great articles from LogRocket:


export default function Home() {
  const { address } = useWeb3();
  if (!address) {
    return <Login />;
  }
  return (
    <div className={styles.container}>
      <h2>You are signed in as {address}</h2>
    </div>
  );
}

The login works but it doesn’t look good right now. So, create a new file Login.module.css in the styles folder and add the following:

.container {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background-color: #7449bb;
}
.button {
  color: #7449bb;
  background-color: white;
  border: none;
  border-radius: 5px;
  padding: 10px;
  font-size: 16px;
  margin: 4px 2px;
  cursor: pointer;
  font-weight: 500;
}

Next, add the following classes to Login.js:

<div className={styles.container}>
  <button className={styles.button} onClick={() => connectWallet("injected")}>
    Sign in using MetaMask
  </button>
</div>

And finally, import the styles:

import styles from "../styles/Login.module.css";

This will give us a simple, but good-looking login screen.

metamask sign in

Initializing the thirdweb SDK

Now we need to initialize the thirdweb SDK for the various scripts we are going to run. Start by creating a new folder called scripts with an initialize-sdk.js file inside of it.

Add the following code to the file:

import { ThirdwebSDK } from "@thirdweb-dev/sdk";
import ethers from "ethers";
import dotenv from "dotenv";
dotenv.config();
if (!process.env.PRIVATE_KEY || process.env.PRIVATE_KEY === "") {
  console.log("🛑 Private key not found.");
}
if (!process.env.ALCHEMY_API_URL || process.env.ALCHEMY_API_URL === "") {
  console.log("🛑 Alchemy API URL not found.");
}
if (!process.env.WALLET_ADDRESS || process.env.WALLET_ADDRESS === "") {
  console.log("🛑 Wallet Address not found.");
}
const sdk = new ThirdwebSDK(
  new ethers.Wallet(
    process.env.PRIVATE_KEY,
    ethers.getDefaultProvider(process.env.ALCHEMY_API_URL)
  )
);
(async () => {
  try {
    const address = await sdk.getSigner().getAddress();
    console.log("SDK initialized by address:", address);
  } catch (err) {
    console.error("Failed to get the address", err);
    process.exit(1);
  }
})();
export default sdk;

This will initialize the thirdweb SDK, and as you can see, we need to install some packages:

npm i dotenv # npm

yarn add dotenv # yarn

We are using modular imports here, so create a new package.json file inside the scripts folder and simply add the following:

{
  "name": "scripts",
  "type": "module"
}

Finally, run the script:

node scripts/initialize-sdk.js

The script may take some time to run, but after some time you will get your app address.

We are going to need this in the next steps, so store it somewhere safe.

Adding features to mint an NFT

For this step, we are going to need some test ETH, so go to a faucet like this and get some.

Creating and configuring an NFT

Create a new file called deploy-drop.js inside the scripts folder. In here, add the following script:

import { ethers } from "ethers";
import sdk from "./initialize-sdk.js";

(async () => {
  try {
    const editionDropAddress = await sdk.deployer.deployEditionDrop({
      name: "LogRocket DAO", // Name of NFT Collection for DAO
      description: "A DAO for all the LogRocket readers.", // Description
      image: "image_Address", // PFP for NFT collection
      primary_sale_recipient: ethers.constants.AddressZero,
    });
    const editionDrop = sdk.getEditionDrop(editionDropAddress);
    const metadata = await editionDrop.metadata.get();
    console.log(
      "✅ Successfully deployed editionDrop contract, address:",
      editionDropAddress
    );
    console.log("✅ editionDrop metadata:", metadata);
  } catch (error) {
    console.log("failed to deploy editionDrop contract", error);
  }
})();

You will need to update a few things here:

  • Update the app address with the new app address that you got by running the previous script
  • Update the name of the NFT drop for the DAO, and its description
  • Add an image for the NFT drop by creating a new folder called assets, and adding the image for your NFT there

After you have updated the details, run the following script:

node scripts/deploy-drop.js

Wait for the script to run, and you should get an address and the metadata.

NFT address and metadata

This will create a new edition drop contract for us! You can even check out the transaction on Rinkeby Etherscan.

Let’s configure our NFT now! Create a new config-nft.js file inside the scripts folder and add the following:

import sdk from "./initialize-sdk.js";

const editionDrop = sdk.getEditionDrop("EDITION_DROP_ADDDRESS");

(async () => {
  try {
    await editionDrop.createBatch([
      {
        name: "LogRocket DAO", // Name of NFT Collection for DAO
        description: "A DAO for all the LogRocket readers.", // Description
        image: "image_address", // Image for NFT
      },
    ]);
    console.log("✅ Successfully created a new NFT in the drop!");
  } catch (error) {
    console.error("failed to create the new NFT", error);
  }
})();

You need to update the bundle drop address and the details in the object inside createBatch. These details are going to be used for the NFT!

Once, you have updated all of them, run the following script:

node scripts/config-nft.js

It should give you an output like this.

NFT-configs-output

If you see the module in the thirdweb dashboard, you will see that an NFT has been created! 🥳

Thirdweb dashboard

Finally, let’s add a claim condition to our NFT.

Setting a claim condition will allow us to set a limit for the NFTs and allow a specific max limit per transaction. We will set a claim condition from the dashboard itself, so click on the Settings button and create a new claim phase.

claim condition settings

After you are done updating, click Update claim phase and confirm the small transaction.

Checking if the user has an NFT

Before creating a mint button that allows the users to mint NFTs, let’s check if the user has an NFT already. We don’t want the users to mint multiple NFTs!

Start by adding two new variables, sdk and bundleDropModule, like this before our functional component:

  const editionDrop = useEditionDrop(
    "0x2f66A5A2BCB272FFC9EB873E3482A539BEB6f02a"
  );

You will also need to import useEditionDrop:

import { useAddress, useEditionDrop } from "@thirdweb-dev/react";

Now, let’s create a state for hasClaimedNFT:

const [hasClaimedNFT, setHasClaimedNFT] = useState(false);

We also need to create a useEffect Hook to check if the user has the NFT:

useEffect(() => {
    if (!address) {
      return;
    }
    const checkBalance = async () => {
      try {
        const balance = await editionDrop.balanceOf(address, 0);
        if (balance.gt(0)) {
          setHasClaimedNFT(true);
          console.log("🎉 You have an NFT!");
        } else {
          setHasClaimedNFT(false);
          console.log("🤷‍♂️ You don't have an NFT.");
        }
      } catch (error) {
        setHasClaimedNFT(false);
        console.error("Failed to get nft balance", error);
      }
    };
    checkBalance();
  }, [address, editionDrop]);

Firstly, it will check if the user is signed in. If the user is not signed in, it will return nothing. Then, this checks if the user has the NFT with the token ID 0 in the drop contract that we imported at the top.

If you, open the console in the website, it should show that you don’t have an NFT.

You dont have an NFT message

Creating a button to mint NFTs

Let’s create the button to mint NFTs! Create a new function called mintNft like so:

const mintNft = async () => {
  setIsClaiming(true);
  try {
    await bundleDropModule.claim("0", 1);
    setHasClaimedNFT(true);
    console.log("🌊 Successfully Minted the NFT!");
  } catch (error) {
    console.error("failed to claim", error);
  } finally {
    setIsClaiming(false);
  }
};

We will call this function when a button is clicked to mint the NFT to the user’s wallet. But first, let’s add the isClaiming state:

const [isClaiming, setIsClaiming] = useState(false);

Let’s create the button now! Inside the final return block add the following:

<div>
  <h1>Mint your free LogRocket DAO Membership NFT 💳</h1>
  <button disabled={isClaiming} onClick={() => mintNft()}>
    {isClaiming ? "Minting..." : "Mint your nft (FREE)"}
  </button>
</div>

Now, after we sign in, it should show us a screen like this.

Mint NFT screen button

If you try the Mint your nft (FREE) button, it should pop up your MetaMask screen to complete the transaction. In the console, should should see the following.

successfully minted nft

Finally, just above the final return block, add this check to see if the user has claimed the NFT already:

if (hasClaimedNFT) {
  return (
    <div>
      <h1>You have the DAO Membership NFT!</h1>
    </div>
  );
}

Adding styles

We have completed building the minting NFT functionality, but it looks ugly, so let’s add some basic stylings. Inside Home.module.css add the following:

.container {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background-color: #7449bb;
}
.container > h1 {
  font-size: 3rem;
  color: #fff;
  font-weight: bold;
}
.button {
  color: #7449bb;
  background-color: white;
  border: none;
  border-radius: 5px;
  padding: 10px;
  font-size: 16px;
  margin: 4px 2px;
  cursor: pointer;
  font-weight: 500;
}

We also need to add the classNames:

  if (hasClaimedNFT) {
    return (
      <div className={styles.container}>
        <h1>You have the DAO Membership NFT!</h1>
      </div>
    );
  }
  return (
    <div className={styles.container}>
      <h1>Mint your free LogRocket DAO Membership NFT 💳</h1>
      <button
        className={styles.button}
        disabled={isClaiming}
        onClick={() => mintNft()}
      >
        {isClaiming ? "Minting..." : "Mint your NFT (FREE)"}
      </button>
    </div>
  );
};

This gives us a better mint screen.

Styled NFT minting screen

Creating and deploying a governance token

Create a new file called deploy-token.js in the scripts folder. Add the following to it:

import { AddressZero } from "@ethersproject/constants";
import sdk from "./initialize-sdk.js";
(async () => {
  try {
    const tokenAddress = await sdk.deployer.deployToken({
      name: "LogRocket Token", // name of the token
      symbol: "LR", // symbol
      primary_sale_recipient: AddressZero, // 0x0000000000000000000000000000000000000000
    });
    console.log(
      "✅ Successfully deployed token module, address:",
      tokenAddress
    );
  } catch (error) {
    console.error("failed to deploy token module", error);
  }
})();

This script will create a new token module with a name and symbol. You will need to manually update the app address, token name, and symbol yourself.

After updating, run the script.

You can check this token by the address on Rinkeby Etherscan, and also add it on your MetaMask wallet by clicking Import tokens.

metamask import tokens

After importing, you should see the token under your assets.

Token count 0 LR

It is currently zero, so let’s mint some tokens!

Minting tokens

Create a new file called mint-token.js inside the scripts folder and add the following:

import { ethers } from "ethers";
import sdk from "./initialize-sdk.js";
const tokenModule = sdk.getTokenModule(
  "TOKEN_MODULE_ADDRESS"
);

(async () => {
  try {
    const amount = 1_000_000;
    const amountWith18Decimals = ethers.utils.parseUnits(amount.toString(), 18);
    await tokenModule.mint(amountWith18Decimals);
    const totalSupply = await tokenModule.totalSupply();
    console.log(
      "✅ There now is",
      ethers.utils.formatUnits(totalSupply, 18),
      "$LR in circulation"
    );
  } catch (error) {
    console.error("Failed to mint tokens", error);
  }
})();

Update the token module address with the address you got in the last script, and you can update the amount you want to mint.

After you are ready to mint, run the script:

node scripts/mint-token.js

You should now see the amount of tokens you minted on your MetaMask wallet! 🎉

token count 1 million

Airdropping tokens

We might want to airdrop the tokens to our NFT holders, so let’s build a script for that. Create a new airdrop.js file inside scripts and add the following:

import sdk from "./initialize-sdk.js";
const editionDrop = sdk.getEditionDrop(
  "EDITION_ADDRESS"
);
const token = sdk.getToken("TOKEN_ADDRESS");
(async () => {
  try {
    const walletAddresses = await editionDrop.history.getAllClaimerAddresses(0);
    if (walletAddresses.length === 0) {
      console.log(
        "No NFTs have been claimed yet, ask yourfriends to claim some free NFTs!"
      );
      process.exit(0);
    }
    const airdropTargets = walletAddresses.map((address) => {
      const randomAmount = Math.floor(
        Math.random() * (10000 - 1000 + 1) + 1000
      );
      console.log("✅ Going to airdrop", randomAmount, "tokens to", address);
      const airdropTarget = {
        toAddress: address,
        amount: randomAmount,
      };
      return airdropTarget;
    });
    console.log("🌈 Starting airdrop...");
    await token.transferBatch(airdropTargets);
    console.log(
      "✅ Successfully airdropped tokens to all the holders of the NFT!"
    );
  } catch (err) {
    console.error("Failed to airdrop tokens", err);
  }
})();

After you run the script you should get something like this.

Airdrop success message

Currently, only you have minted an NFT, so it won’t send the token to someone else. But this can be used to send it to other NFT holders later on.

Allowing users to vote

Create a new deploy-vote.js file in the scripts folder and add the following:

import sdk from "./initialize-sdk.js";
(async () => {
  try {
    const voteContractAddress = await sdk.deployer.deployVote({
      name: "LR Dao's Proposals",
      voting_token_address: "TOKEN_ADDRESS",
      voting_delay_in_blocks: 0,
      voting_period_in_blocks: 6570,
      voting_quorum_fraction: 0,
      proposal_token_threshold: 0,
    });
    console.log(
      "✅ Successfully deployed vote contract, address:",
      voteContractAddress
    );
  } catch (err) {
    console.error("Failed to deploy vote contract", err);
  }
})();

Update the app address, the name, and the voting token address, then run the script:

node scripts/deploy-vote.js

We also need to set up a vote module, so create a new script called setup-vote.js and add the following:

import sdk from "./initialize-sdk.js";
const vote = sdk.getVote("VOTE_ADDRESS");
const token = sdk.getToken("TOKEN_ADDRESS");

(async () => {
  try {
    await token.roles.grant("minter", vote.getAddress());
    console.log(
      "Successfully gave vote contract permissions to act on token contract"
    );
  } catch (error) {
    console.error(
      "failed to grant vote contract permissions on token contract",
      error
    );
    process.exit(1);
  }
  try {
    const ownedTokenBalance = await token.balanceOf(process.env.WALLET_ADDRESS);
    const ownedAmount = ownedTokenBalance.displayValue;
    const percent90 = (Number(ownedAmount) / 100) * 90;
    await token.transfer(vote.getAddress(), percent90);
    console.log(
      "✅ Successfully transferred " + percent90 + " tokens to vote contract"
    );
  } catch (err) {
    console.error("failed to transfer tokens to vote contract", err);
  }
})();

You will need to run this script to finish it up:

node scripts/setup-vote.js

Now that we have our vote module ready, let’s create some proposals!

Create a new file called vote-proposals.js inside the scripts folder and add the following:

import sdk from "./initialize-sdk.js";
import { ethers } from "ethers";
const vote = sdk.getVote("0x31c5840b31A1F97745bDCbB1E46954b686828E0F");
const token = sdk.getToken("0x6eefd78C9C73505AA71A13FeE31D9718775c9086");
(async () => {
  try {
    const amount = 420_000;
    const description =
      "Should the DAO mint an additional " +
      amount +
      " tokens into the treasury?";
    const executions = [
      {
        toAddress: token.getAddress(),
        nativeTokenValue: 0,
        transactionData: token.encoder.encode("mintTo", [
          vote.getAddress(),
          ethers.utils.parseUnits(amount.toString(), 18),
        ]),
      },
    ];
    await vote.propose(description, executions);
    console.log("✅ Successfully created proposal to mint tokens");
  } catch (error) {
    console.error("failed to create first proposal", error);
    process.exit(1);
  }
})();

You need to update the module addresses, and if you want to update the message of the proposal, you can update that as well.

Finally, run the script. It should give you something like this.

successful proposal to mint tokens message

If you now check the thirdweb dashboard, the proposal has been created. 🎉

thirdweb cleared proposal

Showing proposals on the website

First, import the token and vote module:

const token = useToken("TOKEN_ADDRESS");
const vote = useVote("VOTE_ADDRESS");

We are going to need three useStates, like so:

const [proposals, setProposals] = useState([]);
const [isVoting, setIsVoting] = useState(false);
const [hasVoted, setHasVoted] = useState(false); 

Getting the proposals

We need to get the proposals to display them on the screen, so create this useEffect:

useEffect(() => {
    if (!hasClaimedNFT) {
      return;
    }
    const getAllProposals = async () => {
      try {
        const proposals = await vote.getAll();
        setProposals(proposals);
        console.log("📋 Proposals:", proposals);
      } catch (error) {
        console.log("failed to get proposals", error);
      }
    };
    getAllProposals();
  }, [hasClaimedNFT, vote]);

Then, create a new handleFormSubmit function:

const handleFormSubmit = async (e) => {
    e.preventDefault();
    e.stopPropagation();
    setIsVoting(true);
    const votes = proposals.map((proposal) => {
      const voteResult = {
        proposalId: proposal.proposalId,
        vote: 2,
      };
      proposal.votes.forEach((vote) => {
        const elem = document.getElementById(
          proposal.proposalId + "-" + vote.type
        );
        if (elem.checked) {
          voteResult.vote = vote.type;
          return;
        }
      });
      return voteResult;
    });
    try {
      const delegation = await token.getDelegationOf(address);
      if (delegation === AddressZero) {
        await token.delegateTo(address);
      }
      try {
        await Promise.all(
          votes.map(async ({ proposalId, vote: _vote }) => {
            const proposal = await vote.get(proposalId);
            if (proposal.state === 1) {
              return vote.vote(proposalId, _vote);
            }
            return;
          })
        );
        try {
          await Promise.all(
            votes.map(async ({ proposalId }) => {
              const proposal = await vote.get(proposalId);
              if (proposal.state === 4) {
                return vote.execute(proposalId);
              }
            })
          );
          setHasVoted(true);
          console.log("successfully voted");
        } catch (err) {
          console.error("failed to execute votes", err);
        }
      } catch (err) {
        console.error("failed to vote", err);
      }
    } catch (err) {
      console.error("failed to delegate tokens");
    } finally {
      setIsVoting(false);
    }
  };

This function is going to collect the vote.

Rendering the proposals

Replace the if (hasClaimedNFT) block with this:

if (hasClaimedNFT) {
    return (
      <div className={styles.container}>
          <h2>Active Proposals</h2>
          <form onSubmit={handleFormSubmit}>
            {proposals.map((proposal) => (
              <Proposal
                key={proposal.proposalId}
                votes={proposal.votes}
                description={proposal.description}
                proposalId={proposal.proposalId}
              />
            ))}
            <button
              onClick={handleFormSubmit}
              type="submit"
              className={styles.button}
            >
              {isVoting
                ? "Voting..."
                "Submit Votes"}
            </button>
          </form>
        </div>
    );
  }

We are creating a separate component for the proposal to keep things clean. So, create a new file called Proposal.js in the components folder and add the following:

import styles from "../styles/Proposal.module.css";

const Proposal = ({ description, votes, proposalId }) => {
  return (
    <div className={styles.proposal}>
      <h5 className={styles.description}>{description}</h5>
      <div className={styles.options}>
        {votes.map((vote) => (
          <div key={vote.type}>
            <input
              type="radio"
              id={proposalId + "-" + vote.type}
              name={proposalId}
              value={vote.type}
              defaultChecked={vote.type === 2}
            />
            <label htmlFor={proposalId + "-" + vote.type}>{vote.label}</label>
          </div>
        ))}
      </div>
    </div>
  );
};
export default Proposal;

I also added basic styling, so create a new Proposal.module.css file in the styles folder:

.proposal {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background-color: #fafafa;
  border-radius: 10px;
  box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
  padding: 20px;
  margin: 20px;
}
.options {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: flex-start;
  width: 100%;
  margin-top: 1rem;
}

To center the items, I have added the following styles in Home.module.css as well:

.container > form {
  display: flex;
  flex-direction: column;
  align-items: center;
}

You will get to this screen where you can submit your votes. 🎉

Proposal vote screen

Finally, let’s make a function to check if the person has already voted.

First, create a new useEffect:

useEffect(() => {
    if (!hasClaimedNFT) {
      return;
    }
    if (!proposals.length) {
      return;
    }
    const checkIfUserHasVoted = async () => {
      try {
        const hasVoted = await vote.hasVoted(proposals[0].proposalId, address);
        setHasVoted(hasVoted);
      } catch (error) {
        console.error("Failed to check if wallet has voted", error);
      }
    };
    checkIfUserHasVoted();
  }, [hasClaimedNFT, proposals, address, vote]);

And replace the button with this:

 <button
  onClick={handleFormSubmit}
  type="submit"
  disabled={isVoting || hasVoted}
  className={styles.button}
>
  {isVoting ? "Voting..." : hasVoted ? "You Already Voted" : "Submit Votes"}
</button>

After you have voted, it should show the message You Already Voted:

You already voted message

Conclusion

This was it for this tutorial, hope you liked it and can use it to make your own DAO! You can always update the DAO and add more features if you like.✌️

You can find the GitHub repo for this project here.

LogRocket: Full visibility into production Next.js apps

Debugging Next applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Next app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your Next.js apps — .

Avneesh Agarwal A fullstack web developer, designer, content creator, open source enthusiast, and student 🤓

2 Replies to “How to create a DAO with Next.js and thirdweb”

  1. Sadly it seems like this tutorial is already outdated! Thirdweb moves fast…

    There’s no longer a getApps function – that was in v1 of the 3rdweb sdk and has been removed from v2. Plus, you’re no longer able to create projects. Which means your tutorial fails when you get to the initilise-script stage; getApps() returns an empty array.

    1. Thanks for the tip! You’re right, the tutorial did become outdated very quickly. We’ve updated the post to reflect the changes introduced with thirdweb v2.

Leave a Reply