Cover photo

How To Add Farcaster Auth To Next.Js

Master Farcaster Auth with our Next.js tutorial and manage signer keys effortlessly to elevate your application's user authentication process.

Farcaster has grown exponentially in the last few months, and to truly scale and bear the load there needs to be more clients. Fetching casts and populating a feed isn’t too difficult, but writing to the Farcaster network is another issue. There have been several ways authentication has evolved in Farcaster, and the best approach so far has been signer keys. These are similar to API keys, where the user can approve or revoke them from posting on their behalf, and we’ve written about how you could do this yourself. The only downside is actually handling that signer key once it’s created.

One approach that’s been used before is storing it in local storage on the client, such as web browser cookies. This is actually how we approached it when building a photo client for Farcaster, and one of the biggest complaints was trying to sign in with different devices or having that cache cleared. When that happened, the user had to pay warps to sign in again, which made it less than desirable to use. Some people have opted to storing those keys on their own database, but that can come with its own risks.

Pinata saw this need and has recently just released Farcaster Auth, a service that manages signers for Farcaster Apps. With our API and FDK, adding auth and sending casts within your own Farcaster client is a breeze. In this tutorial, we’ll show you how to set it up in Next.js and send a “Hello World” cast at the end!

Setup

In order to follow this tutorial you will need a few things to get up and running.

Pinata Account

Farcaster Auth is a new paid feature starting on the Picnic plan, which also gives you access to all sorts of stuff you can use to build your client like image or video support with IPFS. If you don’t have a paid account, sign up today (and shoot me a message if you’re serious and want a discount 😉). Once you have an account set up, you’ll want to follow these steps to create an API key (specifically the JWT).

Farcaster Account

One of the things the FDK will be handling for you under the hood is creating and signing ED25519 keys. However, to sign those keys, you will need a Farcaster account - specifically the mnemonic phrase for the account and the FID. Usually, when you’re building a Farcaster app, the app itself will have its own account (e.g. @photocast). However, you do not necessarily have to do that and you could use your own Farcaster account. To find your mnemonic phrase, you can navigate to the Warpcast app, and go to Settings > Advance > Reveal recovery phrase. Keep in mind, that you’ll need to keep this phrase highly confidential; the Pinata team will never ask for this, and our FDK only uses it locally to sign the keys.

Stack

As stated earlier, we will be using Next.js to build this tutorial, so make sure you have Node.js installed along with a good text editor. Once you have everything mentioned beforehand ready to go, let’s run the following command to start up the project.

npx create-next-app@latest farcaster-auth

We’ll select all the default options, and once its done installing, we’ll cd into the repo and install some other dependencies.

cd farcaster-auth && npm install pinata-fdk @farcaster/auth-kit react-qrcode-logo

With everything installed, let’s create a new .env.local file at the root of the project and add the following variables.

# Your pinata API key 
JWT PINATA_JWT= 
# The App FID that the user is signing into 
APP_FID= 
# The App mnemonic phrase that the user is signing into 
DEVELOPER_MNEMONIC=

The Pinata JWT would be the API key we made earlier, and the App FID & Developer Mnemonic is from the Farcaster Account that will be signing the keys. With all of that set-up, we can now open the project and start building this!

Creating and Storing a Signer

Before we start creating a signer, we’re going to implement the Farcaster Authkit, provided by the team who built Farcaster. It’s a handy library that can let users scan a QR code and sign in with Farcaster. Now, that may sound like creating a signer, but in reality it’s just providing the app the user’s FID and other read-only info. Nevertheless, it is helpful to find someone by FID and make sure they are the owner of that FID when fetching a signer down the road.

To set it up, open the app/page.tsx file and make the following changes.

"use client";

import "@farcaster/auth-kit/styles.css";
import { AuthKitProvider, SignInButton } from "@farcaster/auth-kit";

const config = {
  rpcUrl: "https://mainnet.optimism.io",
  domain: "example.com",
  siweUri: "https://example.com/login",
};

export default function Home() {  

  return (
    <AuthKitProvider config={config}>
      <main className="flex min-h-screen flex-col items-center justify-center gap-4 p-24 w-full">
        <SignInButton
          onSuccess={({ fid, username }) =>
            console.log(
              `Hello, ${username}! Your fid is ${fid}.`
            )
          }
        />
      </main>
    </AuthKitProvider>
  );
}

If you run npm run dev in the terminal you should get the page with a button to sign in. Go ahead and try it out to see how it works. Now, with that done, we’ll make a new API route where we will create a signer. Create the path app/api/signer/route.ts and open the route.ts file, then put in the following code.

import { NextResponse, NextRequest } from "next/server";
import { PinataFDK } from "pinata-fdk";

const fdk = new PinataFDK({
  pinata_jwt: process.env.PINATA_JWT as string,
  pinata_gateway: "",
  app_fid: process.env.APP_FID as string,
  app_mnemonic: process.env.DEVELOPER_MNEMONIC,
});

export async function POST() {
  try {
    const res = await fdk.createSponsoredSigner();
    return NextResponse.json(res);
  } catch (error) {
    console.log(error);
    return NextResponse.json(error);
  }
}

As you can probably see, the FDK makes a lot of this really simple, abstracting pages of code for us. All we have to do is create the fdk instance with our pinata_jwt, the app_fid, and the app_mnemonic. Then we just make a POST endpoint where we call fdk.createSponsoredSigner() and return the results. We also have the option to use createSigner instead where the end user would have to pay warps, but with createSponsoredSigner that is simplified for the end user.

Back in app/page.tsx let’s make our client-side function to call the endpoint.

"use client";

import "@farcaster/auth-kit/styles.css";
import { AuthKitProvider, SignInButton } from "@farcaster/auth-kit";
import { QRCode } from "react-qrcode-logo";
import { useState } from "react";

const config = {
  rpcUrl: "https://mainnet.optimism.io",
  domain: "example.com",
  siweUri: "https://example.com/login",
};

export default function Home() {
  const [deepLink, setDeepLink]: any = useState();
  const [openQR, setOpenQR] = useState(false);
  const [fid, setFid]: any = useState();
  const [signerId, setSignerId]: any = useState();

  async function createSigner() {
    // start the process of creating a new signer
    try {
      // send API request to create signer with Pinata
      const signerReq = await fetch(`/api/signer`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
      });
      const signerRes = await signerReq.json();
      // Set deep link url for user to scan and open the qr code
      setDeepLink(signerRes.deep_link_url);
      setOpenQR(true);
    } catch (error) {
      console.log(error);
    }
  }

  return (
    <AuthKitProvider config={config}>
      <main className="flex min-h-screen flex-col items-center justify-center gap-4 p-24 w-full">
        <SignInButton
          onSuccess={({ fid, username }) =>
            console.log(
              `Hello, ${username}! Your fid is ${fid}.`,
              setFid(fid)
            )
          }
        />
        {!signerId && fid && (
          <button
            className="h-10 px-4 py-2 bg-black text-white hover:bg-black/90 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
            onClick={createSigner}
          >
            Create Signer
          </button>
        )}
        {openQR && (
          <QRCode
            value={deepLink}
            size={200}
            logoImage="https://dweb.mypinata.cloud/ipfs/QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng"
            logoWidth={50}
            logoHeight={50}
            logoPadding={5}
            logoPaddingStyle="square"
            qrStyle="dots"
            eyeRadius={15}
          />
        )}
      </main>
    </AuthKitProvider>
  );
}

Now, we’ve added several different things here, so let’s go over them. First, we added some react state to handle our QR code opening and deepLinkUrl, as well as the user’s fid and signerId when we get there. In our createSigner() function, it will make an API call to our signer’s endpoint, use the end result to set our deepLinkUrl, and open the QRCode/> component. What’s happened so far is that our FDK has created a keypair and signed it, and then sent a request to Pinata which then sends it to Warpcast for approval. At that point, we get that deepLinkUrl back which the user can open with their mobile device via the QR code. This will open Warpcast for the user to approve the signer or not. While this is all happening, we’re going to need to poll whether that user has approved the key or not. To do that, let’s create a new API route under app/api/poll/route.ts.

import { NextResponse, NextRequest } from "next/server";
import { PinataFDK } from "pinata-fdk";

const fdk = new PinataFDK({
  pinata_jwt: process.env.PINATA_JWT as string,
  pinata_gateway: "",
  app_fid: process.env.APP_FID as string,
  app_mnemonic: process.env.DEVELOPER_MNEMONIC,
});

export async function GET(request: NextRequest) {
  try {
    const searchParams = request.nextUrl.searchParams;
    const token: any = searchParams.get("token");
    const res = await fdk.pollSigner(token);
    return NextResponse.json(res);
  } catch (error) {
    console.log(error);
    return NextResponse.json(error);
  }
}

This one, again, is really simple. We just make our FDK instance and then use the search params to get the polling token. With that, we can use fdk.pollSigner() with the token and return the results. Back in our page.tsx file, we’ll make some additions to createSigner().

async function createSigner() {
    // start the process of creating a new signer
    try {
      // send API request to create signer with Pinata
      const signerReq = await fetch(`/api/signer`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
      });
      const signerRes = await signerReq.json();
      // Set deep link url for user to scan and open the qr code
      setDeepLink(signerRes.deep_link_url);
      setOpenQR(true);

      // While the user is polling up the approval in warpcast, poll the token and create a while loop to keep checking if its complete. Set a timeout in the even it fails
      const pollReq = await fetch(`/api/poll?token=${signerRes.token}`);
      const pollRes = await pollReq.json();
      const pollStartTime = Date.now();
      while (pollRes.state != "completed") {
        if (Date.now() - pollStartTime > 120000) {
          console.log("Polling timeout reached");
          alert("Request timed out");
          setOpenQR(false);
          break;
        }
        const pollReq = await fetch(`/api/poll?token=${signerRes.token}`);
        const pollRes = await pollReq.json();
        if (pollRes.state === "completed") {
          // If the approval is successful set the signerId and store it to local storage
          setDeepLink(null);
          setOpenQR(false);
          setSignerId(signerRes.signer_id);
          localStorage.setItem("signer_id", signerRes.signer_id);
          return pollRes;
        }
        await new Promise((resolve) => setTimeout(resolve, 2000));
      }
    } catch (error) {
      console.log(error);
    }
  }

While the user is working on opening the QR code, we’re going to immediately start polling. We’ll get an initial request sent, and then start a while loop with a delay. If the state is not completed, then we’ll continue to fetch our endpoint with the polling token, using a 2-second delay so we don’t overdo it. We’ll also set a time-out limit of 2 minutes, where if the user does not complete the approval, we’ll restart and close the modal out. If / when the user approves the signer, and it’s completed, we’ll close the QR code, set the signerId state in our app, and store the signerId in local storage. If you want more persistent auth methods - such as Next Auth - you could implement them here.

Persisting a Signer

Now all of this is great, but what if someone clears their browser cache or logs in from another device? This is where Farcaster Auth really shines. We’ll create a new GET function under app/api/signer/route.ts to fetch by FID a previously created signer.

import { NextResponse, NextRequest } from "next/server";
import { PinataFDK } from "pinata-fdk";

const fdk = new PinataFDK({
  pinata_jwt: process.env.PINATA_JWT as string,
  pinata_gateway: "",
  app_fid: process.env.APP_FID as string,
  app_mnemonic: process.env.DEVELOPER_MNEMONIC,
});

export async function POST() {
  try {
    const res = await fdk.createSponsoredSigner();
    return NextResponse.json(res);
  } catch (error) {
    console.log(error);
    return NextResponse.json(error);
  }
}

export async function GET(request: NextRequest) {
  try {
    const searchParams = request.nextUrl.searchParams;
    const fid: any = searchParams.get("fid");
    const res = await fdk.getSigners(fid);
    console.log(res)
    return NextResponse.json(res);
  } catch (error) {
    console.log(error);
    return NextResponse.json(error);
  }
}

Then, back on the client side, we’ll create a new function called checkStorage().

async function checkStorage() {
    // We an check if there happens to already be a signer in local storage, and if not we can fetch one we've already created via pinata
    try {
      if (typeof window != "undefined") {
        const signer = localStorage.getItem("signer_id");
        console.log("local storage: ", signer);
        if (signer != null) {
          setSignerId(signer);
        } else {
          const signerReq = await fetch(`/api/signer?fid=${fid}`);
          const signerRes = await signerReq.json();
          if (signerRes.signers.length > 0) {
            console.log("signer found and set")
            setSignerId(signerRes.signers[0].signer_uuid);
          } else {
            console.log("no signer found")
            return;
          }
        }
      }
    } catch (error) {
      console.log(error);
    }
  }

This is a function we’ll fire as soon as the user signs in with the auth kit. We’ll first check to see if there is a signer in local storage, and if there is, we’ll set it in the signerId state. If there isn’t one in local storage, we’ll use our API route to check with their FID if we have one saved with Pinata. If not, we’ll make them create a new one. Now, we can go into our markup to trigger this upon sign-in!

return (
  <AuthKitProvider config={config}>
    <main className="flex min-h-screen flex-col items-center justify-center gap-4 p-24 w-full">
      <SignInButton
        onSuccess={({ fid, username }) =>
          console.log(
            `Hello, ${username}! Your fid is ${fid}.`,
            setFid(fid),
            checkStorage(),
          )
        }
      />
      {!signerId && fid && (
        <button
          className="h-10 px-4 py-2 bg-black text-white hover:bg-black/90 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
          onClick={createSigner}
        >
          Create Signer
        </button>
      )}
      {openQR && (
        <QRCode
          value={deepLink}
          size={200}
          logoImage="https://dweb.mypinata.cloud/ipfs/QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng"
          logoWidth={50}
          logoHeight={50}
          logoPadding={5}
          logoPaddingStyle="square"
          qrStyle="dots"
          eyeRadius={15}
        />
      )}
    </main>
  </AuthKitProvider>
);

Sending a Cast

We’ve gotten this far, why not send a cast with our new signerId? You’ll be amazed at how simple it is. Let’s create a new API route called api/cast/route.ts.

import { NextResponse, NextRequest } from "next/server";
import { PinataFDK } from "pinata-fdk";

const fdk = new PinataFDK({
  pinata_jwt: process.env.PINATA_JWT as string,
  pinata_gateway: "",
  app_fid: process.env.APP_FID as string,
  app_mnemonic: process.env.DEVELOPER_MNEMONIC as string,
});

export async function POST(request: NextRequest) {
  try {
    const body = await request.json()
    const res = await fdk.sendCast({
      castAddBody: {
        text: "Hello World!",
        parentUrl: "https://warpcast.com/~/channel/pinata"
      },
      signerId: body.signerId
    });
    return NextResponse.json(res);
  } catch (error) {
    console.log(error);
    return NextResponse.json(error);
  }
}

Just like before, we’ll create an fdk instance with our environment variables, then make a POST function. In it, we’ll parse the body of the request, then call fdk.sendCast(). Inside that function, we’ll declare our castAddBody, which is all the contents of our cast, and include the signerId. There’s a lot you can add here, so definitely check out the docs here to see what you could cast instead!

Back on the client side, we’ll create a new function to send the cast and a button in the jsx to show if we have an approved signer.

async function sendCast() {
  // Using our already created signerId we can send a cast with our API route
  try {
    const castReq = await fetch(`/api/cast`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ signerId: signerId }),
    });
    const castRes = await castReq.json();
    console.log(castRes);
    alert("Cast sent!");
  } catch (error) {
    console.log(error);
  }
}

return (
  <AuthKitProvider config={config}>
    <main className="flex min-h-screen flex-col items-center justify-center gap-4 p-24 w-full">
      <SignInButton
        onSuccess={({ fid, username }) =>
          console.log(
            `Hello, ${username}! Your fid is ${fid}.`,
            setFid(fid),
            checkStorage(),
          )
        }
      />
      {!signerId && fid && (
        <button
          className="h-10 px-4 py-2 bg-black text-white hover:bg-black/90 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
          onClick={createSigner}
        >
          Create Signer
        </button>
      )}
      {openQR && (
        <QRCode
          value={deepLink}
          size={200}
          logoImage="https://dweb.mypinata.cloud/ipfs/QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng"
          logoWidth={50}
          logoHeight={50}
          logoPadding={5}
          logoPaddingStyle="square"
          qrStyle="dots"
          eyeRadius={15}
        />
      )}
      {signerId && (
        <button
          className="h-10 px-4 py-2 bg-black text-white hover:bg-black/90 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
          onClick={sendCast}
        >
          Cast "Hello World!" to /pinata
        </button>
      )}
    </main>
  </AuthKitProvider>
);

If you wait a few seconds, then you should be able to see the cast on Warpcast! If you want to view the entire code for this tutorial or use it as a template, then you can view it here.

Wrapping Up

Farcaster Auth provides the foundation needed for clients, but it doesn’t stop there - be sure to check out our docs on how you can use the Farcaster API to fetch casts and create feeds to build a fully-fledged Farcaster client. Then go the extra mile and add image or video uploads with IPFS by following this guide. Whatever you build, be sure to share it on the Pinata Farcaster Channel.

Happy Casting!

Pinata logo
Subscribe to Pinata and never miss a post.