Home

Picket

Picket is a developer-first, multi-chain web3 auth platform. With Picket, you can easily authenticate users via their wallets and token gate anything.

This guide steps through building a simple todo list Next.js application with Picket and Supabase. We use Picket to allow users to login into our app with their wallets and leverage Supabase's Row Level Security (RLS) to securely store user information off-chain.

Checkout a live demo of a Picket + Supabase integration

The code for this guide is based of this example repo.

Requirements#

Step 1: Create a Picket Project#

First, we'll create a new project in our Picket dashboard.

Click the Create New Project button at the top of the Projects section on your Picket dashboard. Edit the project to give it a memorable name.

Picket project settings

We're done for now! We'll revisit this project when we are setting up environment variables in our app.

Step 2: Create a Supabase Project#

From your Supabase dashboard, click New project.

Enter a Name for your Supabase project.

Enter a secure Database Password.

Select the any Region.

Click Create new project.

Supabase project settings

Step 3: Create new New Table with RLS in Supabase#

Create a todos Table#

From the sidebar menu in the Supabase dashboard, click Table editor, then New table.

Enter todos as the Name field.

Select Enable Row Level Security (RLS).

Create four columns:

  • name as text
  • wallet_address as text
  • completed as bool with the default value false
  • created_at as timestamptz with a default value of now()

Click Save to create the new table.

todos table

Setup Row Level Security (RLS)#

Now we want to make sure that only the todos owner, the user's wallet_address, can access their todos. The key component of the this RLS policy is the expression

((jwt() ->> 'walletAddress'::text) = wallet_address)

This expression checks that the wallet address in the requesting JWT access token is the same as the wallet_address in the todos table.

RLS policy

Step 4: Create a Next.js app#

Now, let's start building!

Create a new Typescript Next.js app

1npx create-next-app@latest --typescript

Create a .env.local file and enter the following values

  • NEXT_PUBLIC_PICKET_PUBLISHABLE_KEY => Copy the publishable key from the Picket project you created in step 1
  • PICKET_PROJECT_SECRET_KEY => Copy the secret key from the Picket project you created in the step 1
  • NEXT_PUBLIC_SUPABASE_URL => You can find this URL under "Settings > API" in your Supabase project
  • NEXT_PUBLIC_SUPABASE_ANON_KEY => You can find this project API key under "Settings > API" in your Supabase project
  • SUAPBASE_JWT_SECRET=> You can find this secret under "Settings > API" in your Supabase project
NEXT_PUBLIC_PICKET_PUBLISHABLE_KEY="YOUR_PICKET_PUBLISHABLE_KEY"
PICKET_PROJECT_SECRET_KEY="YOUR_PICKET_PROJECT_SECRET_KEY"
NEXT_PUBLIC_SUPABASE_URL="YOUR_SUPABASE_URL"
NEXT_PUBLIC_SUPABASE_ANON_KEY="YOUR_SUPABASE_ANON_KEY"
SUPABASE_JWT_SECRET="YOUR_SUPABASE_JWT_SECRET"

Step 5: Setup Picket for Wallet Login#

For more information on how to setup Picket in your Next.js app, checkout the Picket getting started guide After initializing our app, we can setup Picket.

Install the Picket React and Node libraries

1npm i @picketapi/picket-react @picketapi/picket-node

Update pages/_app.tsx to setup the PicketProvider

import '../styles/globals.css'
import type { AppProps } from 'next/app'

import { PicketProvider } from '@picketapi/picket-react'

export default function App({ Component, pageProps }: AppProps) {
  return (
    <PicketProvider apiKey={process.env.NEXT_PUBLIC_PICKET_PUBLISHABLE_KEY!}>
      <Component {...pageProps} />
    </PicketProvider>
  )
}

Update pages/index.tsx to let users log in and out with their wallet

import { GetServerSideProps } from 'next'
import { useRouter } from 'next/router'
import { useCallback } from 'react'

import styles from '../styles/Home.module.css'

import { usePicket } from '@picketapi/picket-react'
import { cookieName } from '../utils/supabase'

type Props = {
  loggedIn: boolean
}

export default function Home(props: Props) {
  const { loggedIn } = props
  const { login, logout, authState } = usePicket()
  const router = useRouter()

  const handleLogin = useCallback(async () => {
    let auth = authState
    // no need to re-login if they've already connected with Picket
    if (!auth) {
      // login with Picket
      auth = await login()
    }

    // login failed
    if (!auth) return

    // create a corresponding supabase access token
    await fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        accessToken: auth.accessToken,
      }),
    })
    // redirect to their todos page
    router.push('/todos')
  }, [authState, login, router])

  const handleLogout = useCallback(async () => {
    // clear both picket and supabase session
    await logout()
    await fetch('/api/logout', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
    })
    // refresh the page
    router.push('/')
  }, [logout, router])

  return (
    <div className={styles.container}>
      <main className={styles.main}>
        {loggedIn ? (
          <button onClick={handleLogout}>Log Out to Switch Wallets</button>
        ) : (
          <button onClick={handleLogin}>Log In with Your Wallet</button>
        )}
      </main>
    </div>
  )
}

export const getServerSideProps: GetServerSideProps<Props> = async ({ req }) => {
  // get supabase token server-side
  const accessToken = req.cookies[cookieName]

  if (!accessToken) {
    return {
      props: {
        loggedIn: false,
      },
    }
  }

  return {
    props: {
      loggedIn: true,
    },
  }
}

Step 6: Issue a Supabase JWT on Wallet Login#

Great, now we have setup a typical Picket Next.js app. Next, we need to implement the log in/out API routes to allow users to securely query our Supabase project.

First, install dependencies

1npm install @supabase/supabase-js jsonwebtoken cookie js-cookie

Create a utility function to create a Supabase client with a custom access token in utils/supabase.ts

import { createClient, SupabaseClientOptions } from '@supabase/supabase-js'

export const cookieName = 'sb-access-token'

const getSupabase = (accessToken: string) => {
  const options: SupabaseClientOptions<'public'> = {}

  if (accessToken) {
    options.global = {
      headers: {
        // This gives Supabase information about the user (wallet) making the request
        Authorization: `Bearer ${accessToken}`,
      },
    }
  }

  const supabase = createClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    options
  )

  return supabase
}

export { getSupabase }

Create new api route pages/api/login.ts. This route validates the Picket access token then issues another equivalent Supabase access token for us to use with the Supabase client.

import type { NextApiRequest, NextApiResponse } from 'next'
import jwt from 'jsonwebtoken'
import cookie from 'cookie'
import Picket from '@picketapi/picket-node'

import { cookieName } from '../../utils/supabase'

// create picket node client with your picket secret api key
const picket = new Picket(process.env.PICKET_PROJECT_SECRET_KEY!)

const expToExpiresIn = (exp: number) => exp - Math.floor(Date.now() / 1000)

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const { accessToken } = req.body
  // omit expiration time,.it will conflict with jwt.sign
  const { exp, ...payload } = await picket.validate(accessToken)
  const expiresIn = expToExpiresIn(exp)

  const supabaseJWT = jwt.sign(
    {
      ...payload,
    },
    process.env.SUPABASE_JWT_SECRET!,
    {
      expiresIn,
    }
  )

  // Set a new cookie with the name
  res.setHeader(
    'Set-Cookie',
    cookie.serialize(cookieName, supabaseJWT, {
      path: '/',
      secure: process.env.NODE_ENV !== 'development',
      // allow the cookie to be accessed client-side
      httpOnly: false,
      sameSite: 'strict',
      maxAge: expiresIn,
    })
  )
  res.status(200).json({})
}

And now create an equivalent logout api route /pages/api/logout.ts to delete the Supabase access token cookie.

import type { NextApiRequest, NextApiResponse } from 'next'
import cookie from 'cookie'

import { cookieName } from '../../utils/supabase'

export default async function handler(_req: NextApiRequest, res: NextApiResponse) {
  // Clear the supabase cookie
  res.setHeader(
    'Set-Cookie',
    cookie.serialize(cookieName, '', {
      path: '/',
      maxAge: -1,
    })
  )

  res.status(200).json({})
}

We can now login and logout to the app with our wallet!

Step 7: Interacting with Data in Supabase#

Now that we can login to the app, it's time to start interacting with Supabase. Let's make a todo list page for authenticated users.

Create a new file pages/todos.tsx

import { GetServerSideProps } from 'next'
import Head from 'next/head'
import Link from 'next/link'
import { useState, useMemo } from 'react'
import jwt from 'jsonwebtoken'
import Cookies from 'js-cookie'

import styles from '../styles/Home.module.css'

import { getSupabase, cookieName } from '../utils/supabase'

type Todo = {
  name: string
  completed: boolean
}

type Props = {
  walletAddress: string
  todos: Todo[]
}

const displayWalletAddress = (walletAddress: string) =>
  `${walletAddress.slice(0, 6)}...${walletAddress.slice(-4)}`

export default function Todos(props: Props) {
  const { walletAddress } = props
  const [todos, setTodos] = useState(props.todos)

  // avoid re-creating supabase client every render
  const supabase = useMemo(() => {
    const accessToken = Cookies.get(cookieName)
    return getSupabase(accessToken || '')
  }, [])

  return (
    <div className={styles.container}>
      <Head>
        <title>Picket 💜 Supabase</title>
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>Your Personal Todo List</h1>
        <div
          style={{
            maxWidth: '600px',
            textAlign: 'left',
            fontSize: '1.125rem',
            margin: '36px 0 24px 0',
          }}
        >
          <p>Welcome {displayWalletAddress(walletAddress)},</p>
          <p>
            Your todo list is stored in Supabase and are only accessible to you and your wallet
            address. Picket + Supabase makes it easy to build scalable, hybrid web2 and web3 apps.
            Use Supabase to store non-critical or private data off-chain like user app preferences
            or todo lists.
          </p>
        </div>
        <div
          style={{
            textAlign: 'left',
            fontSize: '1.125rem',
          }}
        >
          <h2>Todo List</h2>
          {todos.map((todo) => (
            <div
              key={todo.name}
              style={{
                margin: '8px 0',
                display: 'flex',
                alignItems: 'center',
              }}
            >
              <input
                type="checkbox"
                checked={todo.completed}
                onChange={async () => {
                  await supabase.from('todos').upsert({
                    wallet_address: walletAddress,
                    name: todo.name,
                    completed: !todo.completed,
                  })
                  setTodos((todos) =>
                    todos.map((t) => (t.name === todo.name ? { ...t, completed: !t.completed } : t))
                  )
                }}
              />
              <span
                style={{
                  margin: '0 0 0 8px',
                }}
              >
                {todo.name}
              </span>
            </div>
          ))}
          <div
            style={{
              margin: '24px 0',
            }}
          >
            <Link
              href={'/'}
              style={{
                textDecoration: 'underline',
              }}
            >
              Go back home &rarr;
            </Link>
          </div>
        </div>
      </main>
    </div>
  )
}

export const getServerSideProps: GetServerSideProps<Props> = async ({ req }) => {
  // example of fetching data server-side
  const accessToken = req.cookies[cookieName]

  // require authentication
  if (!accessToken) {
    return {
      redirect: {
        destination: '/',
      },
      props: {
        walletAddress: '',
        todos: [],
      },
    }
  }

  // check if logged in user has completed the tutorial
  const supabase = getSupabase(accessToken)
  const { walletAddress } = jwt.decode(accessToken) as {
    walletAddress: string
  }

  // get todos for the users
  // if none exist, create the default todos
  let { data } = await supabase.from('todos').select('*')

  if (!data || data.length === 0) {
    let error = null
    ;({ data, error } = await supabase
      .from('todos')
      .insert([
        {
          wallet_address: walletAddress,
          name: 'Complete the Picket + Supabase Tutorial',
          completed: true,
        },
        {
          wallet_address: walletAddress,
          name: 'Create a Picket Account (https://picketapi.com/)',
          completed: false,
        },
        {
          wallet_address: walletAddress,
          name: 'Read the Picket Docs (https://docs.picketapi.com/)',
          completed: false,
        },
        {
          wallet_address: walletAddress,
          name: 'Build an Awesome Web3 Experience',
          completed: false,
        },
      ])
      .select('*'))

    if (error) {
      // log error and redirect home
      console.error(error)
      return {
        redirect: {
          destination: '/',
        },
        props: {
          walletAddress: '',
          todos: [],
        },
      }
    }
  }

  return {
    props: {
      walletAddress,
      todos: data as Todo[],
    },
  }
}

This is a long file, but don't be intimidated. The page is actually straightforward. It

  1. Verifies server-side that the user is authenticated and if they are not redirects them to the homepage
  2. Checks to see if they already have todos . If so, it returns them. If not, it initializes them for the users
  3. We render the todos and when the user selects or deselects a todo, we update the data in the database

Step 8: Try it Out!#

And that's it. If you haven't already, run your app to test it out yourself

1# start the app
2npm run dev
3# open http://localhost:3000

What's Next?#

Common Use-Case for Picket + Supabase#

  • Account linking. Allow users to associate their wallet address(es) with their existing web2 account in your app
  • Leverage Supabase's awesome libraries and ecosystem while still enabling wallet login
  • Store app-specific data, like user preferences, about your user's wallet adress off-chain
  • Cache on-chain data to improve your DApp's performance
Need some help?

Not to worry, our specialist engineers are here to help. Submit a support ticket through the Dashboard.