# Supabase

{% hint style="info" %}
**Live Demo**

Checkout a [live demo](https://picket-supabase-auth-example-v36z.vercel.app/) of a Supabase + Picket powered app
{% endhint %}

## Overview

[Supabase](https://supabase.com/) is an open-source firebase alternative. Supabase's JWT-based authentication makes it easy to plug in additional authentication providers, like[ Picket](https://picketapi.com/). 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 [Picket](https://picketapi.com/) account. If you don't, sign up at <https://picketapi.com/>
* You've read the [Setup Guide](https://docs.picketapi.com/picket-docs/quick-start-guides/quick-start-guides/start-here-setup)
* Familiarity with [React](https://reactjs.org/) and [Next.js](https://nextjs.org/)
* You have [Supabase](https://supabase.com/) account. If you don't, sign up at <https://supabase.com/>

### Setup Picket

First, we'll create a new project in our Picket dashboard.&#x20;

#### 1. Go to your Picket Dashboard

{% embed url="<https://picketapi.com/dashboard>" %}
Picket Dashboard
{% endembed %}

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

<figure><img src="https://3183040354-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FYuewee9BpHHd2p9bSo45%2Fuploads%2FNH2rf2B4Um3YY9yK9YbF%2FScreenshot%202022-11-15%20at%202.23.54%20PM.png?alt=media&#x26;token=b97f4c81-39cd-462d-a947-fb5c41319b27" alt=""><figcaption><p>Example Project w/  Publishable and Secret Key Redacted</p></figcaption></figure>

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

### Setup Supabase

Next, we'll create and initialize an equivalent project in [Supabase](https://supabase.com/).

#### 1. Create a New Project

From your [Supabase dashboard](https://app.supabase.com/), click `New project`.

Enter a `Name` for your Supabase project.

Enter a secure `Database Password`.

Click `Create new project`.

<figure><img src="https://3183040354-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FYuewee9BpHHd2p9bSo45%2Fuploads%2FoXqs76wN9WbYzigzyTnf%2FScreenshot%202022-11-17%20at%207.52.14%20AM.png?alt=media&#x26;token=d342f31e-5431-4a75-a77b-4de5b86c01ea" alt=""><figcaption><p>Create a new Supabase Project</p></figcaption></figure>

#### 2. Create a **New** Table

From the sidebar menu in the [Supabase dashboard](https://app.supabase.com/), 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.

<figure><img src="https://3183040354-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FYuewee9BpHHd2p9bSo45%2Fuploads%2FICQPgfznQ6LokDJjB2r8%2FScreenshot%202022-11-17%20at%202.37.47%20PM.png?alt=media&#x26;token=1e3ef756-624d-4867-a34d-96392e43c779" alt=""><figcaption></figcaption></figure>

#### **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

```sql
((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.

<figure><img src="https://3183040354-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FYuewee9BpHHd2p9bSo45%2Fuploads%2Fp8CJ17bwCxXHOzAU0I7i%2FScreenshot%202023-02-23%20at%207.21.38%20AM.png?alt=media&#x26;token=97889705-9ded-45d3-862a-b0408f7a061e" alt=""><figcaption><p><strong>Restrict Access to Todos to the Associated Wallet Address</strong></p></figcaption></figure>

### Create a Next.js App

Now, let's start building!&#x20;

Create a [new Typescript Next.js app](https://nextjs.org/docs/getting-started)

```bash
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 the [previous step](#setup-picket)
* &#x20;`PICKET_PROJECT_SECRET_KEY` => Copy the secret key from the Picket project you created in the [previous step](#setup-picket)
* `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"
```

### Setup Picket for Wallet Login

{% hint style="info" %}
**Picket React Quick Start Guide**

For more information on how to setup Picket in your React app, checkout the [getting started guide](https://docs.picketapi.com/picket-docs/quick-start-guides/quick-start-guides/wallet-login)
{% endhint %}

After initializing our app, we can setup Picket.

Install the Picket [React](https://docs.picketapi.com/picket-docs/reference/libraries-and-sdks/react-sdk-picket-react) and [Node](https://docs.picketapi.com/picket-docs/reference/libraries-and-sdks/node.js-library-picket-node) libraries

```bash
npm i @picketapi/picket-react @picketapi/picket-node
```

Update `pages/_app.tsx` to setup the `PicketProvider`

```tsx
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

```tsx
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.

First, install dependencies

```bash
npm install @supabase/supabase-js jsonwebtoken cookie js-cookie
```

Create a utility function to create a Supabase client `utils/supabase.ts`

```typescript
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.

```typescript
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.

```typescript
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`

```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 &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&#x20;

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&#x20;

### Try it Out!

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

```bash
# start the app
npm run dev
# open http://localhost:3000
```

## \[Advanced Guide] Supporting Multiple Login Methods

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.&#x20;

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.

{% hint style="info" %}
**Tailor to Your App's Requirements**

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 <team@picketapi.com>
{% endhint %}

### 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.&#x20;

```typescript
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

1. 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.&#x20;
2. 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
3. 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
4. Create a Supabase JWT from the user info. This is the same JWT structure returned by other native Supabase login mechanism.

We store the user's wallet information in the `app_metadata` field, so we can retrieve it client-side from the the JWT or [database-side in RLS policies](#rls).

{% hint style="success" %}
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.
{% endhint %}

### 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.&#x20;

From the sidebar menu in the [Supabase dashboard](https://app.supabase.com/), click `Table editor`, then `New table`.

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.&#x20;
* `wallet_address` as `text`

Click `Save` to create the new table.

<figure><img src="https://3183040354-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FYuewee9BpHHd2p9bSo45%2Fuploads%2FNxgDWMiECAOwETGYzQ7G%2FScreenshot%202023-02-06%20at%2011.48.45%20AM.png?alt=media&#x26;token=4a1bbd9a-1c91-4967-ad4a-b37da5350976" alt=""><figcaption><p>Create a lookup table for user_id to wallet_address</p></figcaption></figure>

{% hint style="warning" %}
**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.
{% endhint %}

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

```sql
-- 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.&#x20;

However, if we still want to restrict usage based off wallet address, then we can easily do so with the following RLS expression

```sql
auth.jwt()->'app_metadata'->>'walletAddress' = wallet_address
```

### Build Anything

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.&#x20;

If you have any questions, don't hesitate to reach out to <team@picketapi.com>. We're here to help!
