Launch Week 12: Day 3

Learn more
Back
Picket

Picket

Picket

Overview

Web3 Authentication Made Easy. A single API call to authenticate wallets and token gate anything.

Picket's Supabase integration gives you the best of web2 and web3. Picket allows your users to log in with their wallet, but still leverage Supabase's awesome data management and security features.

Use Cases for Supabase + Picket

  • 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.

Documentation

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


_10
((auth.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


_10
npx 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

_10
NEXT_PUBLIC_PICKET_PUBLISHABLE_KEY="YOUR_PICKET_PUBLISHABLE_KEY"
_10
PICKET_PROJECT_SECRET_KEY="YOUR_PICKET_PROJECT_SECRET_KEY"
_10
NEXT_PUBLIC_SUPABASE_URL="YOUR_SUPABASE_URL"
_10
NEXT_PUBLIC_SUPABASE_ANON_KEY="YOUR_SUPABASE_ANON_KEY"
_10
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


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

Update pages/_app.tsx to setup the PicketProvider


_12
import '../styles/globals.css'
_12
import type { AppProps } from 'next/app'
_12
_12
import { PicketProvider } from '@picketapi/picket-react'
_12
_12
export default function App({ Component, pageProps }: AppProps) {
_12
return (
_12
<PicketProvider apiKey={process.env.NEXT_PUBLIC_PICKET_PUBLISHABLE_KEY!}>
_12
<Component {...pageProps} />
_12
</PicketProvider>
_12
)
_12
}

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


_85
import { GetServerSideProps } from 'next'
_85
import { useRouter } from 'next/router'
_85
import { useCallback } from 'react'
_85
_85
import styles from '../styles/Home.module.css'
_85
_85
import { usePicket } from '@picketapi/picket-react'
_85
import { cookieName } from '../utils/supabase'
_85
_85
type Props = {
_85
loggedIn: boolean
_85
}
_85
_85
export default function Home(props: Props) {
_85
const { loggedIn } = props
_85
const { login, logout, authState } = usePicket()
_85
const router = useRouter()
_85
_85
const handleLogin = useCallback(async () => {
_85
let auth = authState
_85
// no need to re-login if they've already connected with Picket
_85
if (!auth) {
_85
// login with Picket
_85
auth = await login()
_85
}
_85
_85
// login failed
_85
if (!auth) return
_85
_85
// create a corresponding supabase access token
_85
await fetch('/api/login', {
_85
method: 'POST',
_85
headers: { 'Content-Type': 'application/json' },
_85
body: JSON.stringify({
_85
accessToken: auth.accessToken,
_85
}),
_85
})
_85
// redirect to their todos page
_85
router.push('/todos')
_85
}, [authState, login, router])
_85
_85
const handleLogout = useCallback(async () => {
_85
// clear both picket and supabase session
_85
await logout()
_85
await fetch('/api/logout', {
_85
method: 'POST',
_85
headers: {
_85
'Content-Type': 'application/json',
_85
},
_85
})
_85
// refresh the page
_85
router.push('/')
_85
}, [logout, router])
_85
_85
return (
_85
<div className={styles.container}>
_85
<main className={styles.main}>
_85
{loggedIn ? (
_85
<button onClick={handleLogout}>Log Out to Switch Wallets</button>
_85
) : (
_85
<button onClick={handleLogin}>Log In with Your Wallet</button>
_85
)}
_85
</main>
_85
</div>
_85
)
_85
}
_85
_85
export const getServerSideProps: GetServerSideProps<Props> = async ({ req }) => {
_85
// get supabase token server-side
_85
const accessToken = req.cookies[cookieName]
_85
_85
if (!accessToken) {
_85
return {
_85
props: {
_85
loggedIn: false,
_85
},
_85
}
_85
}
_85
_85
return {
_85
props: {
_85
loggedIn: true,
_85
},
_85
}
_85
}

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


_10
npm 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


_26
import { createClient, SupabaseClientOptions } from '@supabase/supabase-js'
_26
_26
export const cookieName = 'sb-access-token'
_26
_26
const getSupabase = (accessToken: string) => {
_26
const options: SupabaseClientOptions<'public'> = {}
_26
_26
if (accessToken) {
_26
options.global = {
_26
headers: {
_26
// This gives Supabase information about the user (wallet) making the request
_26
Authorization: `Bearer ${accessToken}`,
_26
},
_26
}
_26
}
_26
_26
const supabase = createClient(
_26
process.env.NEXT_PUBLIC_SUPABASE_URL!,
_26
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
_26
options
_26
)
_26
_26
return supabase
_26
}
_26
_26
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.


_42
import type { NextApiRequest, NextApiResponse } from 'next'
_42
import jwt from 'jsonwebtoken'
_42
import cookie from 'cookie'
_42
import Picket from '@picketapi/picket-node'
_42
_42
import { cookieName } from '../../utils/supabase'
_42
_42
// create picket node client with your picket secret api key
_42
const picket = new Picket(process.env.PICKET_PROJECT_SECRET_KEY!)
_42
_42
const expToExpiresIn = (exp: number) => exp - Math.floor(Date.now() / 1000)
_42
_42
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
_42
const { accessToken } = req.body
_42
// omit expiration time,.it will conflict with jwt.sign
_42
const { exp, ...payload } = await picket.validate(accessToken)
_42
const expiresIn = expToExpiresIn(exp)
_42
_42
const supabaseJWT = jwt.sign(
_42
{
_42
...payload,
_42
},
_42
process.env.SUPABASE_JWT_SECRET!,
_42
{
_42
expiresIn,
_42
}
_42
)
_42
_42
// Set a new cookie with the name
_42
res.setHeader(
_42
'Set-Cookie',
_42
cookie.serialize(cookieName, supabaseJWT, {
_42
path: '/',
_42
secure: process.env.NODE_ENV !== 'development',
_42
// allow the cookie to be accessed client-side
_42
httpOnly: false,
_42
sameSite: 'strict',
_42
maxAge: expiresIn,
_42
})
_42
)
_42
res.status(200).json({})
_42
}

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


_17
import type { NextApiRequest, NextApiResponse } from 'next'
_17
import cookie from 'cookie'
_17
_17
import { cookieName } from '../../utils/supabase'
_17
_17
export default async function handler(_req: NextApiRequest, res: NextApiResponse) {
_17
// Clear the supabase cookie
_17
res.setHeader(
_17
'Set-Cookie',
_17
cookie.serialize(cookieName, '', {
_17
path: '/',
_17
maxAge: -1,
_17
})
_17
)
_17
_17
res.status(200).json({})
_17
}

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


_194
import { GetServerSideProps } from 'next'
_194
import Head from 'next/head'
_194
import Link from 'next/link'
_194
import { useState, useMemo } from 'react'
_194
import jwt from 'jsonwebtoken'
_194
import Cookies from 'js-cookie'
_194
_194
import styles from '../styles/Home.module.css'
_194
_194
import { getSupabase, cookieName } from '../utils/supabase'
_194
_194
type Todo = {
_194
name: string
_194
completed: boolean
_194
}
_194
_194
type Props = {
_194
walletAddress: string
_194
todos: Todo[]
_194
}
_194
_194
const displayWalletAddress = (walletAddress: string) =>
_194
`${walletAddress.slice(0, 6)}...${walletAddress.slice(-4)}`
_194
_194
export default function Todos(props: Props) {
_194
const { walletAddress } = props
_194
const [todos, setTodos] = useState(props.todos)
_194
_194
// avoid re-creating supabase client every render
_194
const supabase = useMemo(() => {
_194
const accessToken = Cookies.get(cookieName)
_194
return getSupabase(accessToken || '')
_194
}, [])
_194
_194
return (
_194
<div className={styles.container}>
_194
<Head>
_194
<title>Picket 💜 Supabase</title>
_194
</Head>
_194
_194
<main className={styles.main}>
_194
<h1 className={styles.title}>Your Personal Todo List</h1>
_194
<div
_194
style={{
_194
maxWidth: '600px',
_194
textAlign: 'left',
_194
fontSize: '1.125rem',
_194
margin: '36px 0 24px 0',
_194
}}
_194
>
_194
<p>Welcome {displayWalletAddress(walletAddress)},</p>
_194
<p>
_194
Your todo list is stored in Supabase and are only accessible to you and your wallet
_194
address. Picket + Supabase makes it easy to build scalable, hybrid web2 and web3 apps.
_194
Use Supabase to store non-critical or private data off-chain like user app preferences
_194
or todo lists.
_194
</p>
_194
</div>
_194
<div
_194
style={{
_194
textAlign: 'left',
_194
fontSize: '1.125rem',
_194
}}
_194
>
_194
<h2>Todo List</h2>
_194
{todos.map((todo) => (
_194
<div
_194
key={todo.name}
_194
style={{
_194
margin: '8px 0',
_194
display: 'flex',
_194
alignItems: 'center',
_194
}}
_194
>
_194
<input
_194
type="checkbox"
_194
checked={todo.completed}
_194
onChange={async () => {
_194
await supabase.from('todos').upsert({
_194
wallet_address: walletAddress,
_194
name: todo.name,
_194
completed: !todo.completed,
_194
})
_194
setTodos((todos) =>
_194
todos.map((t) => (t.name === todo.name ? { ...t, completed: !t.completed } : t))
_194
)
_194
}}
_194
/>
_194
<span
_194
style={{
_194
margin: '0 0 0 8px',
_194
}}
_194
>
_194
{todo.name}
_194
</span>
_194
</div>
_194
))}
_194
<div
_194
style={{
_194
margin: '24px 0',
_194
}}
_194
>
_194
<Link
_194
href={'/'}
_194
style={{
_194
textDecoration: 'underline',
_194
}}
_194
>
_194
Go back home &rarr;
_194
</Link>
_194
</div>
_194
</div>
_194
</main>
_194
</div>
_194
)
_194
}
_194
_194
export const getServerSideProps: GetServerSideProps<Props> = async ({ req }) => {
_194
// example of fetching data server-side
_194
const accessToken = req.cookies[cookieName]
_194
_194
// require authentication
_194
if (!accessToken) {
_194
return {
_194
redirect: {
_194
destination: '/',
_194
},
_194
props: {
_194
walletAddress: '',
_194
todos: [],
_194
},
_194
}
_194
}
_194
_194
// check if logged in user has completed the tutorial
_194
const supabase = getSupabase(accessToken)
_194
const { walletAddress } = jwt.decode(accessToken) as {
_194
walletAddress: string
_194
}
_194
_194
// get todos for the users
_194
// if none exist, create the default todos
_194
let { data } = await supabase.from('todos').select('*')
_194
_194
if (!data || data.length === 0) {
_194
let error = null
_194
;({ data, error } = await supabase
_194
.from('todos')
_194
.insert([
_194
{
_194
wallet_address: walletAddress,
_194
name: 'Complete the Picket + Supabase Tutorial',
_194
completed: true,
_194
},
_194
{
_194
wallet_address: walletAddress,
_194
name: 'Create a Picket Account (https://picketapi.com/)',
_194
completed: false,
_194
},
_194
{
_194
wallet_address: walletAddress,
_194
name: 'Read the Picket Docs (https://docs.picketapi.com/)',
_194
completed: false,
_194
},
_194
{
_194
wallet_address: walletAddress,
_194
name: 'Build an Awesome Web3 Experience',
_194
completed: false,
_194
},
_194
])
_194
.select('*'))
_194
_194
if (error) {
_194
// log error and redirect home
_194
console.error(error)
_194
return {
_194
redirect: {
_194
destination: '/',
_194
},
_194
props: {
_194
walletAddress: '',
_194
todos: [],
_194
},
_194
}
_194
}
_194
}
_194
_194
return {
_194
props: {
_194
walletAddress,
_194
todos: data as Todo[],
_194
},
_194
}
_194
}

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


_10
# start the app
_10
npm run dev
_10
# 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

Details

DeveloperPicket Labs
CategoryAuth
DocumentationLearn

Third-party integrations and docs are managed by Supabase partners.