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
- 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
_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.
Step 4: Create a Next.js app
Now, let's start building!
Create a new Typescript Next.js app
_10npx 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
_10NEXT_PUBLIC_PICKET_PUBLISHABLE_KEY="YOUR_PICKET_PUBLISHABLE_KEY"_10PICKET_PROJECT_SECRET_KEY="YOUR_PICKET_PROJECT_SECRET_KEY"_10NEXT_PUBLIC_SUPABASE_URL="YOUR_SUPABASE_URL"_10NEXT_PUBLIC_SUPABASE_ANON_KEY="YOUR_SUPABASE_ANON_KEY"_10SUPABASE_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
_10npm i @picketapi/picket-react @picketapi/picket-node
Update pages/_app.tsx
to setup the PicketProvider
_12import '../styles/globals.css'_12import type { AppProps } from 'next/app'_12_12import { PicketProvider } from '@picketapi/picket-react'_12_12export 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
_85import { GetServerSideProps } from 'next'_85import { useRouter } from 'next/router'_85import { useCallback } from 'react'_85_85import styles from '../styles/Home.module.css'_85_85import { usePicket } from '@picketapi/picket-react'_85import { cookieName } from '../utils/supabase'_85_85type Props = {_85 loggedIn: boolean_85}_85_85export 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_85export 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
_10npm 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
_26import { createClient, SupabaseClientOptions } from '@supabase/supabase-js'_26_26export const cookieName = 'sb-access-token'_26_26const 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_26export { 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.
_42import type { NextApiRequest, NextApiResponse } from 'next'_42import jwt from 'jsonwebtoken'_42import cookie from 'cookie'_42import Picket from '@picketapi/picket-node'_42_42import { cookieName } from '../../utils/supabase'_42_42// create picket node client with your picket secret api key_42const picket = new Picket(process.env.PICKET_PROJECT_SECRET_KEY!)_42_42const expToExpiresIn = (exp: number) => exp - Math.floor(Date.now() / 1000)_42_42export 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.
_17import type { NextApiRequest, NextApiResponse } from 'next'_17import cookie from 'cookie'_17_17import { cookieName } from '../../utils/supabase'_17_17export 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
_194import { GetServerSideProps } from 'next'_194import Head from 'next/head'_194import Link from 'next/link'_194import { useState, useMemo } from 'react'_194import jwt from 'jsonwebtoken'_194import Cookies from 'js-cookie'_194_194import styles from '../styles/Home.module.css'_194_194import { getSupabase, cookieName } from '../utils/supabase'_194_194type Todo = {_194 name: string_194 completed: boolean_194}_194_194type Props = {_194 walletAddress: string_194 todos: Todo[]_194}_194_194const displayWalletAddress = (walletAddress: string) =>_194 `${walletAddress.slice(0, 6)}...${walletAddress.slice(-4)}`_194_194export 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 →_194 </Link>_194 </div>_194 </div>_194 </main>_194 </div>_194 )_194}_194_194export 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
- 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
_10# start the app_10npm run dev_10# 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
Details
Third-party integrations and docs are managed by Supabase partners.