is an open-source firebase alternative. Supabase's JWT-based authentication makes it easy to plug in additional authentication providers, like. 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
This guide will walk you through how to add Picket to a new Supabase app. By the end of this guide your users will be able to log into your Supabase app via their wallet of choice!
Getting Started
Requirements
You have a account. If you don't, sign up at
You've read the
Familiarity with and
You have account. If you don't, sign up at
Setup Picket
First, we'll create a new project in our Picket dashboard.
1. Go to your Picket Dashboard
2. Create a new Project
You'll see the Create New Project button at the top of the Projects section of your Picket dashboard. Alternatively, you can re-use an existing project. Feel free to 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.
Setup Supabase
1. Create a New Project
Enter a Name for your Supabase project.
Enter a secure Database Password.
Click Create new project.
2. Create a 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.
3. 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
npm 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,
},
};
};
Issue a Supabase JWT on Login
Great, now we have setup a typical Picket React app. Next, we need to implement the log in/out API routes to allow users to securely query our Supabase project.
Create a utility function to create a Supabase client 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!
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. Supabase + Picket 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
Try it Out!
And that's it. If you haven't already, run your app to test it out yourself
# start the app
npm run dev
# open http://localhost:3000
At this point, you have a Supabase-powered app that allows users to log in with their wallets. This works well for apps that only allow users to connect and login with their wallet. But what if you want allow users to log in with their wallet or traditional authentication mechanisms like email, phone, or OAuth 2.0.
This guide walks you through how to integrate wallet login into Supabase's native auth.users table. By leveraging Supabase's native auth table, users can log in via their preferred authentication method. Downstream of login, you can treat all users the same regardless of their login method by referencing their unique user id from the auth.users table.
Tailor to Your App's Requirements
Update Login Endpoint
First, we are going to update the login endpoint pages/api/login.ts to fetch the corresponding Supabase user for the wallet if the user exists. If the user doesn't exist, we are going to create a new user and save the user's wallet information to the app_metadata field.
import type { NextApiRequest, NextApiResponse } from "next";
import jwt from "jsonwebtoken";
import cookie from "cookie";
import Picket, { AuthenticatedUser } from "@picketapi/picket-node";
import { cookieName, getSupabaseAdminClient } 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);
const getOrCreateUser = async (payload: AuthenticatedUser) => {
const supabase = getSupabaseAdminClient();
// first we get the user by wallet address from user_wallet table
let { data: userWallet } = await supabase
.from("user_wallet")
.select("user_id")
.eq("wallet_address", payload.walletAddress)
.eq("chain", payload.chain)
.maybeSingle();
const userID = userWallet?.user_id;
if (!userID) {
let { data } = await supabase.auth.admin.createUser({
// @ts-ignore
email: payload.email,
app_metadata: {
provider: "picket",
providers: ["picket"],
// store authorization info in app_metadata
// because it cannot be modified by users
walletAddress: payload.walletAddress,
chain: payload.chain,
},
user_metadata: {
...payload,
},
});
return data?.user;
}
let { data } = await supabase.auth.admin.getUserById(userID);
return data?.user;
};
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 user = await getOrCreateUser(payload);
const supabaseJWT = jwt.sign(
{
...user,
},
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({});
}
The key changes are in the new getOrCreateUser function. The update logic is
For the given wallet, check to see if a user already exists. We use a new table user_wallet to lookup a user ID for a wallet address. We will create this table in the next step.
If the user doesn't exist, create a new user via the admin auth API. This requires a Supabase client that is created with your project's secret service role key
If the user does exist, fetch the user via their user ID. This requires a Supabase client that is created with your project's secret service role key
Create a Supabase JWT from the user info. This is the same JWT structure returned by other native Supabase login mechanism.
We use the app_metadata field instead of the user_metadata because user can update user_metadata. We do not want users to change their wallet address without verifying ownership, so we use the app_metadata field which is only modifiable by an admin role.
Create a Table for Mapping User ID to Wallet Address
There is no native Supabase SDK for filtering users based on app_metadata. To do this, we create a lookup table user_wallet that maps wallet addresses to user IDs.
Enter user_wallet as the Name field.
Select Enable Row Level Security (RLS).
Create two columns:
user_id as uuid with foreign key relationship to auth.user(id). Set this as the primary key.
wallet_address as text
Click Save to create the new table.
Alternative Approaches
We use a lookup table user_wallet to fetch a user_id for a given wallet address.
An alternative approach is to query auth.users directly. At the time of writing this, there is no native Supabase SDK method for query a user based on their app_metadata. The only way to query the raw_app_metadata field in auth.users is to connect and query Postgres directly or create a custom RPC.
Any of these approaches work. Choose the method that best meets your app's requirements.
Trigger Inserts on New User Creation
Now we have lookup a table, but how do we populate it? The recommended approach is to create an on insert trigger in Postgres. This trigger will automatically updates the mapping between user_id and wallet_address any time a new user is created in the auth.users table.
-- This trigger automatically creates a profile entry when a new user signs up via Supabase Auth.
-- See https://supabase.com/docs/guides/auth/managing-user-data#using-triggers for more details.
create function public.handle_new_user()
returns trigger as $$
begin
insert into public.user_wallet (user_id, wallet_address)
values (new.id, new.raw_app_meta_data->>'walletAddress');
return new;
end;
$$ language plpgsql security definer;
create trigger on_auth_user_created
after insert on auth.users
for each row execute procedure public.handle_new_user();
RLS
By leveraging Supabase's native auth.users table, we can write RLS policies based off user ID rather than the wallet address RLS policy we defined above. This keeps our logic consistent regardless of the user's login mechanism.
However, if we still want to restrict usage based off wallet address, then we can easily do so with the following RLS expression
You now have the foundation to build any application that supports wallet login along with any other Supabase native auth mechanism.
It's important to note that this guide is a template to help you get started. It is not meant to be prescriptive. Use the concepts in this guide and adjust the implementation to meet your applications specific requirements.
Next, we'll create and initialize an equivalent project in .
From your , click New project.
From the sidebar menu in the , click Table editor, then New table.
Create a
NEXT_PUBLIC_PICKET_PUBLISHABLE_KEY => Copy the publishable key from the Picket project you created in the
PICKET_PROJECT_SECRET_KEY => Copy the secret key from the Picket project you created in the
For more information on how to setup Picket in your React app, checkout the
Install the Picket and libraries
It's important to note that this guide is a template for using Picket to enable wallet login with Supabase's native auth.users table. It is not meant to be prescriptive. Treat this guide as a starting point for your application and customize it to meet your applications specific requirements. If you have any questions, don't hesitate to reach out to
We store the user's wallet information in the app_metadata field, so we can retrieve it client-side from the the JWT or .
From the sidebar menu in the , click Table editor, then New table.
If you have any questions, don't hesitate to reach out to . We're here to help!