Discover and enable the integrations you need to solve identityAuth0 Marketplace
Next.js

Using Next.js and Auth0 with Supabase

Learn how to integrate Auth0 authentication with database storage and row-level security authorization in Supabase.

November 17, 2021

Next.js

Using Next.js and Auth0 with Supabase

Learn how to integrate Auth0 authentication with database storage and row-level security authorization in Supabase.

November 17, 2021

Overview

In this article, we are going to explore using Next.js, Auth0, and Supabase to build a classic Todo app. Each user will only be able to see their own todos, so we will need to implement authentication, authorization, and a database.

This article will cover:

  • configuring Auth0, Next.js, and Supabase to work together seamlessly
  • using the nextjs-auth0 library for authentication
  • implementing Row Level Security (RLS) and policies for authorization
  • what a JWT is and how to sign our own
  • using PostgreSQL Functions to extract values from a JWT

The final version of the Todo app code can be found here.

Prerequisites

This article does not assume prior experience with any of these technologies. However, you will need Node.js installed to follow along. In addition, plan to have accounts for the managed services in the section below (Auth0 and Supabase). Both are free as of this writing and don't require a credit card.

Stack

Next.js is a React framework that makes building efficient web apps super easy. It also gives us the ability to write server-side logic — which we will need to ensure our application is secure — without needing to maintain our own server.

Auth0 is an authentication and authorization solution that makes managing users and securing applications a breeze. It is an extremely battle-tested and mature solution for auth.

Supabase is an open-source backend-as-a-service, which makes it possible to build an application in a weekend and scale to millions. It is a convenient wrapper around a collection of open source tools that enable database storage, file storage, authentication, authorization, and real-time subscriptions. While these are all great features, this article will only use database storage and authorization.

"Wait, if Supabase handles auth, why are we using Auth0?"

One of the real strengths of Supabase is the lack of vendor lockin. Maybe you already have users in Auth0, your company has a lot of experience with it, or you're interacting with other applications that use it. Any of the Supabase components can be swapped out for a similar service and hosted anywhere.

So let's do just that!

Auth0

The first thing we need to do is sign up for a free account with Auth0. Once at the dashboard, we need to create a new Tenant for our project.

A tenant is a way of isolating our users and settings from other applications we have with Auth0.

Click the name of your account in the top left, and then select Create tenant from the dropdown.

Create tenant from Auth0 dashboard

Give your tenant a unique Domain, set the Region closest to you, and leave the Environment Tag set to Development.

Auth0 tenant settings

In a production application, you want your region to be as close as possible to the majority of your users.

Next, we want to create an Application. Select Applications > Applications from the sidebar menu, and click + Create Application. We want to give it a name (this can be the same as the Tenant) and select Regular Web Applications. Click Create.

Auth0 application settings

From the application's page, you are redirected to select the Settings tab and scroll down to the Application URIs section.

Add the following:

  • Allowed Callback URLs: http://localhost:3000/api/auth/callback
  • Allowed Logout URLs: http://localhost:3000

Go to Advanced Settings > OAuth and confirm the JSON Web Token (JWT) Signature Algorithm is set to RS256 and that OIDC Conformant is enabled. Be sure to save your changes.

Awesome. We now have an Auth0 instance configured to handle authentication for our application. So let's build an app!

While we could use any web application framework for this example, I am going to use Next.js. It gives us a super-efficient React application and includes file-based routing out of the box. In addition, it allows us to run server-side logic while building our app with getStaticProps and (when the user requests a page) with the getServerSideProps function. We will need to do things like authentication server-side, but we don't want the hassle of setting up, maintaining, and paying for another server.

Next.js

The fastest way to create a Next.js application is using the create-next-app package:

npx create-next-app supabase-auth0

Replace the contents of pages/index.js with:

// pages/index.js

import styles from "../styles/Home.module.css";

const Index = () => {
  return <div className={styles.container}>Working!</div>;
};

export default Index;

Run the project in Development mode:

npm run dev

And confirm it is working at http://localhost:3000.

Authentication

Let's integrate the nextjs-auth0 package. This is a convenient wrapper around the Auth0 JS SDK but specifically built for Next.js:

npm i @auth0/nextjs-auth0

Create a new folder at pages/api/auth/ and add a file called [...auth0].js with the following content:

// pages/api/auth/[...auth0].js

import { handleAuth } from "@auth0/nextjs-auth0";

export default handleAuth();

The [...auth0].js is a catch all route. This means that any url that starts with /api/auth0 will load this component — /api/auth0, /api/auth0/login, /api/auth0/some/deeply/nested/url etc.

This is one of those awesome things nextjs-auth0 gives us for free! Calling handleAuth() automatically creates a collection of convenient routes — such as /login and /logout — and all the necessary logic for handling tokens and sessions. There're no extra steps required besides calling this method.

Replace the contents of pages/_app.js with:

// pages/_app.js

import React from "react";
import { UserProvider } from "@auth0/nextjs-auth0";

const App = ({ Component, pageProps }) => {
  return (
    <UserProvider>
      <Component {...pageProps} />
    </UserProvider>
  );
};

export default App;

Create a .env.local file in your root project folder and add:

AUTH0_SECRET=generate-this-below
AUTH0_BASE_URL=http://localhost:3000
AUTH0_ISSUER_BASE_URL=https://<name-of-your-tenant>.<region-you-selected>.auth0.com
AUTH0_CLIENT_ID=get-from-auth0-dashboard
AUTH0_CLIENT_SECRET=get-from-auth0-dashboard

See the Next.js documentation for Environment variables to learn more.

Generate a secure AUTH0_SECRET by running:

node -e "console.log(crypto.randomBytes(32).toString('hex'))"

AUTH0_CLIENT_ID and AUTH0_CLIENT_SECRET can be found at Applications > Settings > Basic Information in the Auth0 Dashboard.

You will need to quit the Next.js server and re-run the npm run dev command anytime new environment variables are added to the .env.local file

Let's update our pages/index.js file to add the ability to sign in and out:

// pages/index.js

import styles from "../styles/Home.module.css";
import { useUser } from "@auth0/nextjs-auth0";
import Link from "next/link";

const Index = () => {
  const { user } = useUser();

  return (
    <div className={styles.container}>
      {user ? (
        <p>
          Welcome {user.name}!{" "}
          <Link href="/api/auth/logout">
            <a>Logout</a>
          </Link>
        </p>
      ) : (
        <Link href="/api/auth/login">
          <a>Login</a>
        </Link>
      )}
    </div>
  );
};

export default Index;

We are using the useUser() hook to get the user object if they have signed in. If not, we are rendering a link to the Login page.

Next.js' Link component is being used to enable client-side routing rather than needing to reload the entire page from the server.

We can also add the ability to handle the loading and error states:

// pages/index.js

import styles from "../styles/Home.module.css";
import { useUser } from "@auth0/nextjs-auth0";
import Link from "next/link";

const Index = () => {
  const { user, error, isLoading } = useUser();

  if (isLoading) return <div className={styles.container}>Loading...</div>;
  if (error) return <div className={styles.container}>{error.message}</div>;

  // rest of component
};

export default Index;

We need our users to be signed in to see their todos or add a new todo. To accomplish this, we'll protect this route — requiring the user to be signed in. We'll also automatically redirect them to the /login route if they are not.

Thankfully, the nextjs-auth0 library makes this super simple with the withPageAuthRequired function. We can tell Next.js to call this function on the server before rendering our page by setting it to the getServerSideProps function.

Add the following to the pages/index.js file:

// other imports
import { withPageAuthRequired } from "@auth0/nextjs-auth0";

// rest of component

export const getServerSideProps = withPageAuthRequired();

// other export

This function checks if we have a user signed in and handles redirecting them to the Login page if not. If we have a user, it automatically passes the user object to our Index component as a prop. Since this is happening on the server before our component is rendered, we no longer need to handle loading, error states, or whether or not the user is logged in. This means we can significantly clean up our rendering logic.

This is what our entire file should look like:

// pages/index.js

import styles from "../styles/Home.module.css";
import { withPageAuthRequired } from "@auth0/nextjs-auth0";
import Link from "next/link";

const Index = ({ user }) => {
  return (
    <div className={styles.container}>
      <p>
        Welcome {user.name}!{" "}
        <Link href="/api/auth/logout">
          <a>Logout</a>
        </Link>
      </p>
    </div>
  );
};

export const getServerSideProps = withPageAuthRequired();

export default Index;

Very clean!

We want to display a list of todos as the landing page, but first, we need somewhere to store them.

Supabase

Head over to app.supabase.io and click Sign In to authenticate with GitHub. This will create a free Supabase account. From the dashboard, click New project and choose your Organization.

Enter a name, password and select a region geographically close to what you chose for your Auth0 region.

New Supabase project settings

Make sure you choose a secure password, as this will also apply to your PostgreSQL Database.

It will take a few minutes for Supabase to provision all the bits in the background, but this page conveniently displays all the values we need to get our Next.js app configured.

Supabase app URL and secrets

Add these values to the .env.local file:

NEXT_PUBLIC_SUPABASE_URL=your-url
NEXT_PUBLIC_SUPABASE_KEY=your-anon-public-key
SUPABASE_SIGNING_SECRET=your-jwt-secret

Prepending an environment variable with NEXT_PUBLIC_ makes it available in the Next.js client. All other values will only be available in getStaticProps, getServerSideProps and serverless functions in the pages/api/ directory.

Adding new values to the .env.local file requires a restart of the Next.js dev server.

Hopefully, that stalled you long enough and provisioning to complete, and your Supabase project is ready to go.

Click the Table editor icon in the sidebar menu and select + Create a new table.

Create new table button

Create a todo table and add columns for content, user_id and is_complete.

New table settings

  • content will be the text displayed for our todo.
  • user_id will be the user that owns the todo.
  • is_complete will signify whether the todo is done yet. We are setting the default value to false, as we would assume for a new todo.

Leave Row Level Security disabled for now. We will worry about this later.

Click Insert row to create some example todos.

New row settings

We can leave user_id blank and the default value of false for is_complete.

Let's head back to our Next.js application and install the supabase-js library:

npm i @supabase/supabase-js

Create a new folder called utils and add a file called supabase.js:

// utils/supabase.js

import { createClient } from "@supabase/supabase-js";

const getSupabase = () => {
  const supabase = createClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL,
    process.env.NEXT_PUBLIC_SUPABASE_KEY
  );

  return supabase;
};

export { getSupabase };

We are making this a function as we will need to extend it later.

This function uses the environment variables we declared earlier to create a new Supabase client. Let's use our new client to fetch todos in pages/index.js.

We can pass a configuration object to the withPageAuthRequired function and declare our own getServerSideProps function, which will only run if the user is signed in.

// pages/index.js

export const getServerSideProps = withPageAuthRequired({
  async getServerSideProps() {
    const supabase = getSupabase();

    const { data: todos } = await supabase.from("todo").select("*");

    return {
      props: { todos },
    };
  },
});

We need to remember to import the getSupabase function:

import { getSupabase } from "../utils/supabase";

And now we can iterate over our todos and display them in our component:

const Index = ({ user, todos }) => {
  return (
    <div className={styles.container}>
      <p>
        Welcome {user.name}!{" "}
        <Link href="/api/auth/logout">
          <a>Logout</a>
        </Link>
      </p>
      {todos.map((todo) => (
        <p key={todo.id}>{todo.content}</p>
      ))}
    </div>
  );
};

We can also handle the case where there are no todos to display:

const Index = ({ user, todos }) => {
  return (
    <div className={styles.container}>
      <p>
        Welcome {user.name}!{" "}
        <Link href="/api/auth/logout">
          <a>Logout</a>
        </Link>
      </p>
      {todos?.length > 0 ? (
        todos.map((todo) => <p key={todo.id}>{todo.content}</p>)
      ) : (
        <p>You have completed all todos!</p>
      )}
    </div>
  );
};

The todos?.length statement is using Optional Chaining. This is a fallback in case the todos prop is undefined or null.

Our whole component should look something like this:

// pages/index.js

import styles from "../styles/Home.module.css";
import { withPageAuthRequired } from "@auth0/nextjs-auth0";
import { getSupabase } from "../utils/supabase";
import Link from "next/link";

const Index = ({ user, todos }) => {
  return (
    <div className={styles.container}>
      <p>
        Welcome {user.name}!{" "}
        <Link href="/api/auth/logout">
          <a>Logout</a>
        </Link>
      </p>
      {todos?.length > 0 ? (
        todos.map((todo) => <p key={todo.id}>{todo.content}</p>)
      ) : (
        <p>You have completed all todos!</p>
      )}
    </div>
  );
};

export const getServerSideProps = withPageAuthRequired({
  async getServerSideProps() {
    const supabase = getSupabase();

    const { data: todos } = await supabase.from("todo").select("*");

    return {
      props: { todos },
    };
  },
});

export default Index;

Awesome! We should now be seeing our todos in our Next.js app.

List of todo items

But wait, we see all the todos.

We only want users to see their todos. For this, we need to implement authorization. Let's create a helper function in Postgres to extract the currently logged-in user from the request's JWT.

PostgreSQL Functions

Head back to the Supabase dashboard, click SQL in the side panel and select New query. This will create a snippet entitled new sql snippet. Add the following SQL blob and click RUN.

create or replace function auth.user_id() returns text as $$
  select nullif(current_setting('request.jwt.claim.userId', true), '')::text;
$$ language sql stable;

Okay, this one may look a little intimidating. Let's break down the bits we need to understand:

  1. We are creating a new function called user_id.
  2. The auth. part is just a way to namespace it as it is related to auth — also called a schema and is a convention in Postgres.
  3. This function will return a text value.
  4. The body of the function is fetching the value from the userId field of the jwt that came along with the request. We will talk about JWTs later in the article.
  5. If there is no request.jwt.claim.userId, we are just returning an empty string — ''.

Not that scary!

Check out this video on Postgres functions to learn more.

Let's enable Row Level Security and use our new function to ensure only users who own the todo can see it.

Row Level Security

Because Supabase is just a PostgreSQL Database under the hood, we can take advantage of a killer feature — Row Level Security (RLS). RLS allows us to write authorization rules in the database itself, which can be much more efficient, and much more secure!

Head back to the Supabase dashboard, and from the side panel, select Authentication > Policies and click Enable RLS for the todo table.

Supabase dashboard showing RLS enabled for todo table

Now, if we refresh our application, we will see the empty state message.

Empty todo list

Where did our todos go?

By default, RLS will deny access to all rows. If we want a user to see their todos, we need to write a policy.

Back in the Supabase dashboard, click New Policy, then Create a policy from scratch and fill in the following:

Policy settings for SELECT

This might look a little unfamiliar, so let's break it down.

  1. We are giving our policy a name. This can be anything.
  2. We declare which actions we would like to enable. Since we want users to be able to read their own todos, we are choosing SELECT.
  3. We need to specify a condition that can be true or false. If it evaluates to true, the action will be allowed. Otherwise, it will continue to be denied.
  4. We call our auth.user_id() function to get the currently logged-in user's id and compare it to the user_id column for this todo.

I recommend checking out this video to learn more about how awesome and powerful Row Level Security is in Supabase.

Click Review to see the SQL that is being generated for us.

Generated SQL for SELECT policy

Not even scary. It has just formatted the fields we entered into the correct SQL syntax.

While we're here, let's add a policy for creating new todos. This will be an INSERT action:

Policy settings for INSERT

While we probably want to permit users to perform all CRUD actions, it is good practice to specify separate policies rather than selecting ALL. This makes them easier to extend and remove in the future.

Our application does not yet need to be able to update or delete todos; therefore, we will not create policies for these actions.

It is good security practice to enable the minimum amount of permissions for the application to function. We can easily write policies for these actions in the future if we want to enable them.

Let's see if we can view our todos yet by refreshing our Next.js app.

Empty todo list

Still no todos 🙁 Did we mess something up?

No! We just need to add a little bit more glue to transform the JWT that Auth0 is giving our Next.js application to the format that Supabase is expecting.

What is a JWT?

To understand this problem, we must first understand what a JWT is. A JWT encodes a JSON object into a big string that we can use to send data between different services.

By default, the data in a JWT is not encrypted or private, just encoded.

Aditya Shukla wrote a great article about the differences between encoding, encryption, and hashing. Give it a read to learn more.

Screenshot from JWT.io showing the encoded and decoded values of a JWT

This is an example taken from jwt.io — a great tool for working with JWTs. On the left, we can see the JWT value. On the right, we can see what each part represents when decoded. We have some header information about how the JWT was encoded, the payload of user data, and a signature that can be used to verify the token.

Don't put secret things in a JWT!!

The reason we can trust JWTs for authentication is that we can sign it using a secret value. This value is run through an algorithm with the payload data, and a JWT string comes out the other side. We can use the signing secret to verify the JWT on our server. This ensures that an attacker hasn't tinkered with our token in transit. If they have, the value of the JWT will be different, and it will fail verification.

The only way it could be modified and pass the verify step is if someone has your signing secret. This would be bad. And this is why we can only sign JWTs with our server — or in the case of Next.js, the getStaticProps, getServerSideProps, or API routes in the pages/api directory.

Never expose the signing secret to the client!!

Okay, so now that we understand JWTs, what is the problem?

The signing secret used by Auth0 does not match Supabase's signing secret. While we're not using Supabase for authentication, it still uses the secret to verify the JWT each time we request data. Neither of these services makes the signing secret's value configurable.

To solve this problem, we can just grab the sub property — the user's ID — from Auth0 and sign a new token using the signing secret that Supabase is expecting.

Signing a JWT

When working with JWTs, we want to use a well-trusted library from a reputable source. Auth0 has a very widely used and trusted one called jsonwebtoken.

Let's install it:

npm i jsonwebtoken

We can use another hook that the @auth0/nextjs-auth0 library gives us to run some logic after the user signs in. This is called afterCallback and can be passed as a configuration to our handleAuth function. Let's replace the contents of the pages/api/auth/[...auth0].js file with:

// pages/api/auth/[...auth0].js

import { handleAuth, handleCallback } from "@auth0/nextjs-auth0";

const afterCallback = async (req, res, session) => {
  // do some stuff
  // modify the session

  return session;
};

export default handleAuth({
  async callback(req, res) {
    try {
      await handleCallback(req, res, { afterCallback });
    } catch (error) {
      res.status(error.status || 500).end(error.message);
    }
  },
});

In this function, we want to:

  1. get the user object from Auth0's session
  2. create a new payload
  3. sign a new token using Supabase's signing secret

Let's extend our afterCallback function to perform this logic:

// pages/api/auth/[...auth0].js

// other imports
import jwt from "jsonwebtoken";

const afterCallback = async (req, res, session) => {
  const payload = {
    userId: session.user.sub,
    exp: Math.floor(Date.now() / 1000) + 60 * 60,
  };

  session.user.accessToken = jwt.sign(
    payload,
    process.env.SUPABASE_SIGNING_SECRET
  );

  return session;
};

The sub field represents Auth0's unique ID for this user. Since this is the only value we actually need to tell Supabase who our user is, we can create a new payload that contains only this value.

It is good security practice to only give things the minimum amount of data and permissions they need to handle the task.

Additionally, we are setting an expiry for our token — 1 hour. This means that if someone does get a hold of our token, they do not have access to our database indefinitely.

Lastly, we are signing a new token with that payload, using the SUPABASE_SIGNING_SECRET.

Awesome! Our whole file should look something like this:

// pages/api/auth/[...auth0].js

import { handleAuth, handleCallback } from "@auth0/nextjs-auth0";
import jwt from "jsonwebtoken";

const afterCallback = async (req, res, session) => {
  const payload = {
    userId: session.user.sub,
    exp: Math.floor(Date.now() / 1000) + 60 * 60,
  };

  session.user.accessToken = jwt.sign(
    payload,
    process.env.SUPABASE_SIGNING_SECRET
  );

  return session;
};

export default handleAuth({
  async callback(req, res) {
    try {
      await handleCallback(req, res, { afterCallback });
    } catch (error) {
      res.status(error.status || 500).end(error.message);
    }
  },
});

Let's extend our getSupabase function in utils/supabase.js to accept an optional access_token parameter.

// utils/supabase.js

import { createClient } from "@supabase/supabase-js";

const getSupabase = (access_token) => {
  const supabase = createClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL,
    process.env.NEXT_PUBLIC_SUPABASE_KEY
  );

  if (access_token) {
    supabase.auth.session = () => ({
      access_token,
    });
  }

  return supabase;
};

export { getSupabase };

If we pass this function an access_token, it will attach it to the Supabase session, which will go along for the ride when we request data from Supabase.

Let's extend our getServerSideProps function in pages/index.js to get the accessToken from the session's user, and pass it to the getSupabase function:

// pages/index.js

export const getServerSideProps = withPageAuthRequired({
  async getServerSideProps({ req, res }) {
    const {
      user: { accessToken },
    } = await getSession(req, res);

    const supabase = getSupabase(accessToken);

    const { data: todos } = await supabase.from("todo").select("*");

    return {
      props: { todos },
    };
  },
});

We also need to remember to add getSession to the import from @auth0/nextjs-auth0:

import { withPageAuthRequired, getSession } from "@auth0/nextjs-auth0";

Our whole component should look something like this:

// pages/index.js

import styles from "../styles/Home.module.css";
import { withPageAuthRequired, getSession } from "@auth0/nextjs-auth0";
import { getSupabase } from "../utils/supabase";
import Link from "next/link";

const Index = ({ user, todos }) => {
  return (
    <div className={styles.container}>
      <p>
        Welcome {user.name}!{" "}
        <Link href="/api/auth/logout">
          <a>Logout</a>
        </Link>
      </p>
      {todos?.length > 0 ? (
        todos.map((todo) => <p key={todo.id}>{todo.content}</p>)
      ) : (
        <p>You have completed all todos!</p>
      )}
    </div>
  );
};

export const getServerSideProps = withPageAuthRequired({
  async getServerSideProps({ req, res }) {
    const {
      user: { accessToken },
    } = await getSession(req, res);

    const supabase = getSupabase(accessToken);

    const { data: todos } = await supabase.from("todo").select("*");

    return {
      props: { todos },
    };
  },
});

export default Index;

Since Auth0's afterCallback function runs after the user signs in, we need to log out of our application by clicking the Logout link on the landing page or manually navigating to http://localhost:3000/api/auth/logout.

This will sign us out and automatically redirect us to Auth0's sign-in page.

Sign back in, and we should have our new JWT attached to our Auth0 session's user.

Now when we refresh our application, we should finally see todos.

Empty todo list

Nope!

But we are very close. If we look at the Table editor in the Supabase dashboard. Who is the user that owns the todos?

User ID null in Supabase Table Editor

NULL!!

So we just need to find out what our current user_id is and add it to those rows.

Head back to the Auth0 dashboard, click User Management > Users in the sidebar, and select your user.

List of users in Auth0 dashboard

The user_id is displayed at the top of your user's details page.

User ID in Auth0 dashboard

Let's copy this value and paste it as the user_id for the todos.

User ID set to Auth0 user

Refresh our Next.js application.

Voilà! Todos!!!

List of todo items

The last thing we need to implement is the functionality to add a todo.

Let's add the form logic to our pages/index.js component.

// pages/index.js

// other imports
import { useState } from "react";

const Index = ({ user, todos }) => {
  const [content, setContent] = useState("");
  const [allTodos, setAllTodos] = useState([...todos]);

  const handleSubmit = async (e) => {
    e.preventDefault();
    const supabase = getSupabase(user.accessToken);
    const resp = await supabase
      .from("todo")
      .insert({ content, user_id: user.sub });

    setAllTodos([...todos, resp.data[0]]);
    setContent("");
  };

  return (
    <div className={styles.container}>
      <p>
        Welcome {user.name}!{" "}
        <Link href="/api/auth/logout">
          <a>Logout</a>
        </Link>
      </p>
      <form onSubmit={handleSubmit}>
        <input onChange={(e) => setContent(e.target.value)} value={content} />
        <button>Add</button>
      </form>
      {allTodos?.length > 0 ? (
        allTodos.map((todo) => <p key={todo.id}>{todo.content}</p>)
      ) : (
        <p>You have completed all todos!</p>
      )}
    </div>
  );
};

// exports

Our final component should look something like:

// pages/index.js

import styles from "../styles/Home.module.css";
import { withPageAuthRequired, getSession } from "@auth0/nextjs-auth0";
import { getSupabase } from "../utils/supabase";
import Link from "next/link";
import { useState } from "react";

const Index = ({ user, todos }) => {
  const [content, setContent] = useState("");
  const [allTodos, setAllTodos] = useState([...todos]);

  const handleSubmit = async (e) => {
    e.preventDefault();
    const supabase = getSupabase(user.accessToken);
    const resp = await supabase
      .from("todo")
      .insert({ content, user_id: user.sub });

    setAllTodos([...todos, resp.data[0]]);
    setContent("");
  };

  return (
    <div className={styles.container}>
      <p>
        Welcome {user.name}!{" "}
        <Link href="/api/auth/logout">
          <a>Logout</a>
        </Link>
      </p>
      <form onSubmit={handleSubmit}>
        <input onChange={(e) => setContent(e.target.value)} value={content} />
        <button>Add</button>
      </form>
      {allTodos?.length > 0 ? (
        allTodos.map((todo) => <p key={todo.id}>{todo.content}</p>)
      ) : (
        <p>You have completed all todos!</p>
      )}
    </div>
  );
};

export const getServerSideProps = withPageAuthRequired({
  async getServerSideProps({ req, res }) {
    const {
      user: { accessToken },
    } = await getSession(req, res);

    const supabase = getSupabase(accessToken);

    const { data: todos } = await supabase.from("todo").select("*");

    return {
      props: { todos },
    };
  },
});

export default Index;

We now have an input field above our list of todos. When we enter the content for our todo and click add, a new todo will be inserted into our DB. We are also setting the user_id column to the value of our user.sub, so we know who the todo belongs to.

We're introducing allTodos so that we can call setAllTodos after insertion of a new todo. It will trigger a partial re-render, displaying the newly inserted todo without a full page refresh.

Awesome! We now have a Next.js application using Auth0 for all things authentication and Supabase with row-level policies for authorization. We learned about JWTs and how to sign our own, as well as writing a Postgres function to lookup values in a JWT in our database.

If you liked this article, follow me on Twitter, subscribe to my YouTube channel and check out my blog.

For all things Supabase follow our Twitter, subscribe to our YouTube channel and check out our blog.

Thanks for reading!

  • Twitter icon
  • LinkedIn icon
  • Faceboook icon