Hero
back button

Back to all articles

Setup supabase for nextjs

16 min read
📝 1922 words
Setup supabase for nextjs

Supabase

This guide was last updated on March 1st, 2025. So if any changes please email or add comments below. Thanks ! p

Using brew, install supabase cli

$ brew install supabase/tap/supabase

Setup Supabase CLI

Initialize your supabase project, move to your current project's root directory and :

$ supabase init

Once initialized, login to supabase :

$ supabase login

This will create a new login link which you can use for authenticating with Supabase in your browser.

Once installed, login to supabase using cli:

Supabase local authentication

After logging in, We can link our remote supabase project/create new supabase project :

For new supabase project

If you don't have a remote supabase project (the project created at supabase.com), you can also create a new project straight from the cli, (You can get your organization ID from the Supabase dashboard) :

$ supabase projects create "Your Project Name" --org-id your-org-id

List your existing Supabase projects:

$ supabase projects list

Access your Supabase dashboard in the browser:

$ supabase dashboard

For local supabase project

Start your local Supabase instance:

$ supabase start

Link your local project to a remote Supabase project (if you have one):

$ supabase link --project-ref your-project-ref

This will pull all your remote configuration and create a config.toml file with the configuration settings:

It uses config as code, by which the cli will bootstrap the entire supabase stack on your local machine. It uses exactly the same infra as the hosted solution, you can read more about it

Config as code

For detailed configuration reference documentation, visit:

Supabase CLI config

Synchronize remote configurations to local :

To synchronize your configuration with the remote project, consider updating your local supabase/config.toml file. You have two options:

  1. Manually update

You can update the config.toml file locally and push your changes to remote

  1. Pull the remote configuration:

To pull the remote configuration, you need to have Docker daemon running, if you have installed docker, you can start docker and run the below command, If you haven't you have to setup Docker Desktop locally and start it, then run the below command, that should do it.

Supabase docker start

Even, if the docker is installed, docker is not running, we need to check that. You can check the docker version :

$ docker --version

If you try to run pull command it will throw error

Supabase docker error

Once docker is running :

supabase db pull

Supabase docker success

Note:

The auth and storage schemas were excluded from the pull. If you need these schemas, you can run:

supabase db pull --schema auth,storage

Supabase docker pull auth storage

Generate TypeScript types for type-safe database access:

$ supabase gen types typescript --local > src/types/supabase.ts

For better experience of generating types automatically, we can use supabase codegen.

Add the following commands in your package.json scripts of nextjs project:


"scripts": {
    "predev": "pnpm install && pnpm supabase-codegen",
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "prebuild": "pnpm supabase-codegen",
    "supabase-codegen": "supabase gen types typescript --project-id <project_id> --schema public > src/supabase/types.ts"
  },

  • predev - This script runs before each pnpm dev command, ensuring that our packages and generated TypeScript are always up to date before starting the development server.

  • prebuild - Functions similarly to predev, but it ensures that the most recent TypeScript files are generated during the build process on Vercel.

  • supabase-codegen - This is where the magic happens. This command instructs Supabase to generate TypeScript types based on a specific project-id. You can find this project id in the Supabase dashboard for your project. In the left navigation, click Project Settings -> General. Replace the project_id in the supabase-codegen with your project's reference id.

Database changes

If you do any changes in your local supabase database instance or in your schemas you have to push this changes to your remote as well, for every change you have create a migration file and apply this changes.

Migrations

Create migrations:

$ supabase migration new your_migration_name

**Push your local database changes to the remote project - Apply migrations ** (after linking):

$ supabase db push

Setup with Nextjs :

We now know how to use supabase, but let's now integrate supabase with our Nextjs project.

Project Setup

There are several initial setup steps required to make our Next.js app and Supabase work together. Let's begin with our .env.local file. environment variables can be found in various locations within the Supabase dashboard.

We will have to create the following env variables in the root directory .env.local file:


NEXT_PUBLIC_SUPABASE_ANON_KEY="eyJhbGci1iKIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl2ZHNhbWRpbGNyeWpoc3Zic2N0Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDgyNzkwMzIsImV4cCI6MjAyMzg1NTAzMn0.vHUHdPZ-npFD9zhnUIexdTw9rlYByDQQa1HW5RsKtZM"

NEXT_PUBLIC_SUPABASE_URL="https://yvdsamdilcryjhsvbsct.supabase.co"

SUPABASE_ACCESS_TOKEN="sbp_c83522caea12924355eb1270e4521850ac4d307a"

NEXT_PUBLIC_SUPABASE_ANON_KEY and NEXT_PUBLIC_SUPABASE_URL - these two values can be found in the Supabase dashboard. Note that they are marked as NEXT_PUBLIC and are perfectly safe to be exposed to client components.

SUPABASE_ACCESS_TOKEN - Unlike the ANON key, this access token is a secret and should not be exposed to client components. In fact, we will only use it as part of our build scripts to generate TypeScript files in the next step. To create one, go back to your Supabase project's main dashboard and, in the left navigation, click Account -> Access Tokens. Don't worry, I've already rotated my secret, so the one shared above is invalid.

Once added, you can restart the app in the terminal

Supabase client component

Starting with the createBrowserClient factory which is a bit more straightforward as there are no cookies to manage.

This code ensures that we always obtain a version of the Supabase client compatible with client components. It utilizes the two NEXT_PUBLIC environment variables we set earlier: the public URL and the anonymous key. The client is also typed to the Database type generated from our codegen, providing autocomplete functionality when attempting to query later.

Create a client.ts file inside supabase folder of the project.


import { createBrowserClient } from '@supabase/ssr'

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
export const supabase = createBrowserClient(supabaseUrl, supabaseAnonKey)

Supabase Server Component:

Our server client, which will be used in React Server Components, Server Actions, and middleware, is slightly more complex because it needs to manage the user's cookie state.

Similar to the client component, we are using the createServerClient factory function from @supabase/ssr. We also require the same environment variables as before. Finally, we need to configure "cookie methods." These methods determine how to get, set, and remove a cookie from the next/headers cookie store.

server.ts

import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!


export async function createClient() {
  const cookieStore = await cookies()

  return createServerClient(
     supabaseUrl,
     supabaseAnonKey,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll()
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            )
          } catch {
            // The `setAll` method was called from a Server Component.
            // This can be ignored if you have middleware refreshing
            // user sessions.
          }
        },
      },
    }
  )
}

Middleware

Next, we need to add a middleware.ts file to the root of the Next.js project (or the src directory if you're using that, like me). This middleware performs several important tasks, but its primary responsibility is to intercept every request and ensure that the Supabase user cookie is up-to-date in the cookie store. It also calls supabase.auth.getUser(), which refreshes the user's access token if it has become stale. Finally, it includes a matcher that only applies when the route is /protected-route, as most of my site is public.

middleware.ts

import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'

export async function updateSession(request: NextRequest) {
  let supabaseResponse = NextResponse.next({
    request,
  })

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll()
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value))
          supabaseResponse = NextResponse.next({
            request,
          })
          cookiesToSet.forEach(({ name, value, options }) =>
            supabaseResponse.cookies.set(name, value, options)
          )
        },
      },
    }
  )

  // IMPORTANT: Avoid writing any logic between createServerClient and
  // supabase.auth.getUser(). A simple mistake could make it very hard to debug
  // issues with users being randomly logged out.

  const {
    data: { user },
  } = await supabase.auth.getUser()
  console.log('user in the server >>>>', user);


  if (
    !user &&
    !request.nextUrl.pathname.startsWith('/login') &&
    !request.nextUrl.pathname.startsWith('/auth')
  ) {
    // no user, potentially respond by redirecting the user to the login page
    const url = request.nextUrl.clone()
    url.pathname = '/login'
    return NextResponse.redirect(url)
  }
}

Create Login Page


'use client'; // Mark as a Client Component

import { createClient } from '@/supabase/client;

export default function Login() {
  const handleLogin = async () => {
    const supabase = createClient();
    const { error } = await supabase.auth.signInWithOAuth({
      provider: 'github',
    });

    if (error) {
      console.error('Error logging in:', error.message);
    }
  };

  return (
    <button onClick={handleLogin} className="bg-blue-500 text-white px-4 py-2 rounded">
      Sign in with GitHub
    </button>
  );
}

Authenticated Route

Let's now create a authenticated route, in our case /protected-route

app/protected-route/page.tsx


import { createClient } from '@/supabase/server'
import { cookies } from 'next/headers'

export default async function Page() {

    const supabaseClient = createClient(cookies());

    // get the logged in user data
    const {
        data: { user }
    } = await supabaseClient.auth.getUser();

    const handleLogin = () => {
        const supabase = createClient();

        // handle signin process
        supabase.auth.signIn({
            provider: 'google', // choose your provider
            options: {
                redirectTo: '/auth/callback' // redirect url
            }
        });
    }

    const handleLogout = async () => {
        const supabase = createClient();

        // handle signout process
        const { error } = await supabase.auth.signOut();
        if (error) {
            console.error('Error logging out:', error.message);
        } else {
            window.location.reload();
        }
    };

    if (!user) {
        return (
            <div>
                <h1> Hello Welcome, please login to continue </h1>
                <button onclick={handleLogin}> Login </button>
            </div>
        )
    }

    return (
        <div>
            <h1> Hello Protected user ! </h1>
            <button onclick={handleLogout}> Logout </button>

            {
                user ? (
                  <p className="flex"> Hi {user.user_metadata.name} <div className="animate-waveMe animation-delay-100 px-4">👋</div> </p>
                )
            }
        </div>
    )
}

What we want to do is to protect this route or unable to access to anyone if the user is not logged in. If not logged in, we are going to ask the use to login to supabase and then redirect him to the protected-route. He should see the message Hello protected user ! otherwise he will be thrown out and he will see the login button.

Observe how we import the createServer factory method from earlier and pass the cookies() from next/headers to it. This enables Supabase to maintain the user's state across pages, layouts, RSCs, and server actions.

And once logged in you can then use the user data !!

Alright ! now you have got an overview on that.

Server Actions

Using supabase with server actions is simple too !

Suppose we have a form that has a input for taking the name of the user and then calls the greet action once the form is submitted

actions/greet.ts

'use server'

import {createClient} from '@/supabase/server';

export default function greet(formData) {

    const supabaseClient = createClient();
    const name = formData.get("name") as string;

    if (!name) {
        throw new Error('Please enter name');
    }

    const {
        data: {user},
    } = await supabaseClient.auth.getUser();

    if (!user) {
        throw new Error('you must be logged in');
    }

    const { user_name: username } = user.user_metadata;

    const greet_msg = `Hello ${username}`
    return {
        'message': greet_msg
    };
}

That's how you can use the server actions