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#
- You have Supabase account. If you don't, sign up at https://supabase.com/
- You have a Picket account. If you don't, sign up at https://picketapi.com/
- You've read the Picket Setup Guide
- Familiarity with React and Next.js
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.
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
.
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
astext
wallet_address
astext
completed
asbool
with the default valuefalse
created_at
as timestamptz with a default value ofnow()
Click Save
to create the new 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.
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 1PICKET_PROJECT_SECRET_KEY
=> Copy the secret key from the Picket project you created in the step 1NEXT_PUBLIC_SUPABASE_URL
=> You can find this URL under "Settings > API" in your Supabase projectNEXT_PUBLIC_SUPABASE_ANON_KEY
=> You can find this project API key under "Settings > API" in your Supabase projectSUAPBASE_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 →
</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
- Verifies server-side that the user is authenticated and if they are not redirects them to the homepage
- Checks to see if they already have
todos
. If so, it returns them. If not, it initializes them for the users - 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?#
- Explore the Picket documentation
- Play with the live Picket + Supabase demo
- Checkout Picket's example Github repositories
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