How to Create a Web3 Creator Platform with a Web2 Backend

How to Create a Web3 Creator Platform with a Web2 Backend

In this guide, we'll show you how to create an application where users can Sign In With Ethereum to authenticate themselves, and interact with a web2 database from their connected wallet!

We'll show you how to:

  • Add sign in with ethereum to your application
  • Make server-side requests from an authenticated wallet
  • Restrict access to what operations a wallet can perform in a web2 database.

Before we get started, below are some helpful resources where you can learn more about the tools we will use in this guide.

Let's get started.

Quick Note

This guide is designed to be backend-agnostic; meaning we're going to skim over the setup of the database and leave that up to you. If you want to follow along with my setup, please follow the steps outlined in the project source code!

Setup

I am going to use the Next.js Typescript starter template for this guide.

If you are following along with the guide, you can create a project with the template using the thirdweb CLI:

npx thirdweb create --app --next --ts

If you already have a Next.js app you can simply follow these steps to get started:

  • Install @thirdweb-dev/react and @thirdweb-dev/sdk and ethers.

  • Add MetaMask authentication to the site. You can follow this guide to add metamask auth.

I have also setup a MongoDB Atlas cluster and prisma in the project. If you want to do the same you can set up the cluster yourself on MongoDB and you can have a look at the repository to see how I setup prisma.

Setting up Auth

Firstly, we need to install the thirdweb auth package:

npm i @thirdweb-dev/auth # npm

yarn add @thirdweb-dev/auth # yarn

Now, create a file called auth.config.ts and the following:

import { ThirdwebAuth } from "@thirdweb-dev/auth/next";

export const { ThirdwebAuthHandler, getUser } = ThirdwebAuth({
  privateKey: process.env.ADMIN_PRIVATE_KEY as string,
  domain: "example.org",
});
  • The domain is important since you don't want your users signing malicious signatures
  • Create a new .env.local file and add a new variable named ADMIN_PRIVATE_KEY. Learn how to export your private key from your wallet. Using private keys as an env variable is vulnerable to attacks and is not the best practice. We are doing it in this guide for the sake of brevity, but we strongly recommend using a secret manager to store your private key.

To configure the auth api, create a new folder inside pages/api called auth and [...thirdweb].ts file inside it! Here we need to export the ThirdwebAuthHandler that we made!

import { ThirdwebAuthHandler } from "../../../auth.config";

export default ThirdwebAuthHandler();

This will catch all the API requests that go to /api/auth/.... Behind the scenes, this handles three routes: /login, /logout, and /user.

Finally, inside the _app.tsx file, add the authConfig prop to ThirdwebProvider:

<ThirdwebProvider
  desiredChainId={activeChainId}
  authConfig={{
    authUrl: "/api/auth",
    domain: "example.org",
    loginRedirect: "/create",
  }}
>
  <Component {...pageProps} />
</ThirdwebProvider>

Creating /create page

Creating the frontend

Now, we will allow users to connect their wallets and sign in! We are going to handle the auth and user creation part on a route called /create so, create a new file in the pages folder called create.tsx.

Firstly, let's allow users to connect their wallets to our site if they aren't connected:

const address = useAddress();

if (!address) {
  return (
    <div className={styles.container}>
      <ConnectWallet accentColor="#F213A4" />
    </div>
  );
}

If the user has connected their wallet but isn't signed in, we will show the sign-in button, allowing them to sign a message to prove they own the wallet they are claiming to be.

const login = useLogin();
const { user: thirdwebUser } = useUser();

if (!thirdwebUser) {
  return (
    <div className={styles.container}>
      <button onClick={() => login()}>Login</button>
    </div>
  );
}

Once the user is both connected and signed in, we will show the create user form!

return (
  <div className={styles.container}>
    <Header />
    <h2>Create User</h2>
    <form className={styles.form} onSubmit={handleSubmit}>
      <input
        placeholder="Name"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <input
        placeholder="Bio"
        value={bio}
        onChange={(e) => setBio(e.target.value)}
      />
      <input
        placeholder="Avatar"
        value={avatar}
        onChange={(e) => setAvatar(e.target.value)}
      />
      <button onClick={() => handleSubmit()}>Create</button>
    </form>
  </div>
);

As you can see we need a handleSubmit function and some states to allow users to submit this data to our backend:

const [name, setName] = useState("");
const [bio, setBio] = useState("");
const [avatar, setAvatar] = useState("");

const handleSubmit = async (e?: React.FormEvent<HTMLFormElement>) => {
  if (e) {
    e.preventDefault();
  }
  try {
    await fetch("/api/user", {
      method: "POST",
      body: JSON.stringify({ name, bio, avatar }),
    });

    router.push(`/user/${thirdwebUser.address}`);
  } catch (error) {
    // Handle your error here
  }
};

Creating API

We will now set up an API for the CRUD functionality.

So, create a new file in pages/api called user.ts and add the following code.

First, we get the currently authenticated wallet using getUser from the configuration file we set up earlier.

Once the user is authenticated, we can perform CRUD operations based on the user's wallet address.

import { NextApiRequest, NextApiResponse } from "next";
import { createUser, deleteUser, getUser, updateUser } from "../../prisma/user";

import { getUser as getUserThirdweb } from "../../auth.config";

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  const thirdwebUser = await getUserThirdweb(req);

  if (!thirdwebUser) {
    return res.status(401).json({ message: "Unauthorized" });
  }

  try {
    if (req.method === "GET") {
      const user = await getUser({ address: thirdwebUser.address });
      return res.status(200).json(user);
    } else if (req.method === "POST") {
      const { name, bio, avatar } = JSON.parse(req.body);

      if (!name || !bio) {
        return res.status(400).json({ message: "Missing fields" });
      }

      const user = await createUser({
        name,
        bio,
        avatar,
        address: thirdwebUser.address,
      });

      return res.json(user);
    } else if (req.method === "PUT") {
      const { ...updateData } = JSON.parse(req.body);

      const user = await updateUser(thirdwebUser.address, updateData);
      return res.json(user);
    } else if (req.method === "DELETE") {
      const user = await deleteUser(thirdwebUser.address);
      return res.json(user);
    }

    return res.status(405).json({ message: "Method not allowed" });
  } catch (error) {
    return res.status(500).json(error);
  }
};

export default handler;

With this setup, we perform a two step process:

  1. Check the currently authenticated wallet
  2. Perform an operation in our web2 database with the knowledge that this request is coming from the wallet address in step 1.

This allows us to restrict what actions we perform based on who is sending the request! For example, I can't update anybody elses profile, since my wallet address is sent as part of the updateUser function.

if (req.method === "PUT") {
  const { ...updateData } = JSON.parse(req.body);

  const user = await updateUser(thirdwebUser.address, updateData);
  return res.json(user);
}

Creating a Dynamic user page

Now that users can perform authenticated CRUD operations on our web2 database, let's create a dynamic profile page for each user.

From this page, we will also allow users to edit/delete their accounts if they own the account they're currently looking at.

So, create a new folder inside the pages folder called user, and within that folder, create a [address].tsx file; the [] around the file name means that this is a dynamic page.

Firstly we need the user details, which we can get on the server-side:

export const getServerSideProps: GetServerSideProps = async (context) => {
  const user = await getUser({ address: context?.params?.address as string });

  if (!user) {
    return {
      notFound: true,
    };
  }

  return {
    props: {
      user,
    },
  };
};

This will pass the user as a prop to the component, allowing us to render some details of the user we just loaded from the database:

import type { GetServerSideProps } from "next";
import { getUser } from "../../prisma/user";
import styles from "../../styles/Theme.module.css";

const Address = ({ user }) => {
  const shortenAddress = (address: string) => {
    return `${address.slice(0, 6)}...${address.slice(-4)}`;
  };

  return (
    <div className={styles.container}>
      <div>
        <img
          className={styles.avatar}
          src={user.avatar.length > 0 ? user.avatar : "/avatar.svg"}
          alt={user.name}
        />

        <p>{user.name}</p>

        <p>{user.bio}</p>
        <a
          href={`https://goerli.etherscan.io/address/${user.address}`}
          target="_blank"
          rel="noreferrer"
        >
          {shortenAddress(user.address)}
        </a>
      </div>
    </div>
  );
};

export default Address;

If you go to a dynamic page with an address with which a user exists you will be able to see the details of that user! Now we will check if the user is the owner of this account:

const { user: thirdwebUser } = useUser();
const owner = thirdwebUser?.address === user.address;

If owner is true, we can show an "edit" button on the UI; which will allow users to update their profile. So add this state and the button:

const [editing, setEditing] = useState(false);
<div>
  {owner && editing && (
    <button onClick={() => setEditing(false)}>Cancel</button>
  )}

  {owner && (
    <button onClick={handleEditClick}>{editing ? "Save" : "Edit"}</button>
  )}
</div>

We will also create a handleEditClick function:

const handleEditClick = async () => {
  if (editing) {
    try {
      await fetch("/api/user", {
        method: "PUT",
        body: JSON.stringify({ name, bio, avatar }),
      });
      setEditing(false);
      alert("Profile updated!");
    } catch (error) {
      alert("Error updating profile");
    }
  }

  if (!editing) {
    setEditing(true);
  }
};

As you can see we need 3 states for name, bio, and avatar:

const [name, setName] = useState(user.name);
const [bio, setBio] = useState(user.bio);
const [avatar, setAvatar] = useState(user.avatar);

I am wrapping all the 3 details in a ternary individually and rendering a input based on the editing state but you can wrap it as a whole as well if you wish:

{
  editing ? (
    <input
      value={avatar}
      onChange={(e) => setAvatar(e.target.value)}
      placeholder="Avatar"
    />
  ) : (
    <img
      className={styles.avatar}
      src={user.avatar.length > 0 ? user.avatar : "/avatar.svg"}
      alt={user.name}
    />
  );
}

{
  editing ? (
    <input
      value={name}
      onChange={(e) => setName(e.target.value)}
      placeholder="Name"
    />
  ) : (
    <p>{user.name}</p>
  );
}
{
  editing ? (
    <input
      value={bio}
      onChange={(e) => setBio(e.target.value)}
      placeholder="Bio"
    />
  ) : (
    <p>{user.bio}</p>
  );
}

Finally, we will also add a delete acc button in a similar manner:

{
  owner && <button onClick={deleteAccount}>Delete</button>;
}

We also need the deleteAccount function:

const deleteAccount = async () => {
  try {
    await fetch("/api/user", {
      method: "DELETE",
    });
    alert("Account deleted");
    router.push("/");
  } catch (error) {
    alert("Error deleting account");
  }
};

If you now try out the app, everything works!

image.png

Conclusion

This guide taught us how to use thirdweb auth to connect web3 auth to a web2 backend by creating a fullstack web3 creator platform!

If you did as well, pat yourself on the back and share it with us on the thirdweb discord! If you want to take a look at the code, check out the GitHub Repository.