Next.js 14 App with Clerk and Supabase

2024-01-26
By: O. Wolfson

This ariticle describes the process of building a Next.js 14 application with Clerk and Supabase. This app handles user authentication with Clerk and manages data with Supabase.

Demo:

My Example app, Depoloyed at Vercel.

My GitHub Repo


Official Documentation:

Clerk and Next.js Quickstart. Clerk Docs

Clerk and Supabase. Supabase Docs

Deploy to production. Clerk Docs


Step 1: Initializing the Project

  1. Create Next.js 14 App:
    sh
    npx create-next-app@latest my-app
    cd my-app
    
  2. Install Dependencies:
    sh
    npm install @clerk/nextjs @supabase/supabase-js
    

Step 2: Environment Setup

Set up the environment for your Next.js project by integrating Clerk and Supabase. This involves obtaining the necessary API keys from both Clerk and Supabase and configuring them in your project.

  1. Obtaining Clerk API Keys:

    • Go to the Clerk Dashboard.
    • Sign up or log in to your Clerk account.
    • Create a new Application or select an existing one.
    • Navigate to the 'API Keys' section in your Clerk dashboard.
  2. Obtaining Supabase API Keys:

    • Visit the Supabase Dashboard.

    • Sign in or create a new Supabase account.

    • Create a new project or select an existing one.

    • Once your project dashboard is open, go to the 'Settings' > 'API' section.

    • You will find two keys here: the anon public key and the service_role secret key. For most client-side operations, the anon key is what you'll use.

    • You will also need Supabase's jwt secret key. To obtain this key, go to the 'Settings' > 'API'. You will find the jwt secret key here under the Config section.

    In your Clerk application, use the lefthand menu to navigate to the JWT Templates page. Click on the button to create a new template based on Supabase. Name your template (I've named mine supabase, as in the example shown here in Supabase's Clerk Integration Docs) and click on the Create Template button. Enter your Supabase jwt secret key in the Signing Key input and apply changes. Once you have done this, you should be able to generate a token for your Supabase client.

    For more information check out the Supabase's Clerk Integration Docs.

  3. .env.local Configuration: After obtaining your API keys, the next step is to securely add them to your Next.js application using an environment file.

    • Create a file named .env.local in the root of your Next.js project (if it doesn't already exist).

add api keys to .env.local as follows:

text
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your-clerk-publishable-key
CLERK_SECRET_KEY=your-clerk-secret-key

NEXT_PUBLIC_SUPABASE_URL=your-supabase-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anonkey

# add these values for the Clerk URLs:
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/

This environment setup ensures that your application can communicate securely with Clerk and Supabase services. Remember, these keys should be kept secure and not exposed in client-side code beyond what is necessary for the functionalities provided by Clerk and Supabase.

Step 3: Clerk Integration

Integrating Clerk into your Next.js application involves setting up the ClerkProvider in your main layout, creating a navigation bar with authentication controls, and setting up dedicated pages for sign-in and sign-up. Here's how to go about it:

  1. Clerk Setup in layout.tsx: The ClerkProvider component is used to wrap your application's root layout. This ensures that Clerk's authentication functionalities are accessible throughout your app.

    • Open or create a layout.tsx file in your project.

    • Import ClerkProvider from @clerk/nextjs.

    • You'll also need to import the useRouter hook from next/router.

    • Wrap your layout component's content with ClerkProvider, passing in the Clerk Frontend API key from your environment variables.

    • Example implementation:

      tsx
      import type { Metadata } from "next";
      import { Inter } from "next/font/google";
      import "./globals.css";
      import { ClerkProvider } from "@clerk/nextjs";
      import NavBar from "@/components/nav/nav-bar";
      const inter = Inter({ subsets: ["latin"] });
      export const metadata: Metadata = {
        title: "Create Next App",
        description: "Generated by create next app",
      };
      export default function RootLayout({
        children,
      }: Readonly<{
        children: React.ReactNode;
      }>) {
        return (
          <ClerkProvider>
            <html lang="en">
              <body className={inter.className}>
                <div className="flex flex-col gap-8 px-20 py-12">
                  <NavBar />
                  {children}{" "}
                </div>
              </body>
            </html>
          </ClerkProvider>
        );
      }
      
  2. NavBar Component:

    The NavBar component serves as the main navigation header for your application, providing links and authentication controls available globally.

    • I recommend using Clerk's UserButton component to handle authentication controls like sign-out and account management. Here I have added the UserButton to an AuthButton component that also handles the case where the user is not authenticated and offers a link to the sign-in page. If the user is logged in the UserButton will be displayed.

      tsx
      import React from "react";
      import { UserButton } from "@clerk/nextjs";
      import Link from "next/link";
      import { auth } from "@clerk/nextjs/server";
      
      function AuthButton() {
        const { userId } = auth();
        // console.log("user id:", userId);
        return (
          <div>
            {userId ? (
              <div className="border-2 rounded-full w-[35px] h-[35px]">
                <UserButton afterSignOutUrl="/" />
              </div>
            ) : (
              <Link
                href="/sign-in"
                className="border-2 rounded-lg px-4 py-2 font-bold"
              >
                Log In
              </Link>
            )}
          </div>
        );
      }
      
      export default AuthButton;
      
  3. Creating Authentication Pages:

    Dedicated pages for sign-in and sign-up provide a focused area for users to manage their authentication state.

    • Create two new pages in your Next.js application, typically named sign-in.tsx and sign-up.tsx.

    • Use Clerk's provided components such as SignIn and SignUp to render the respective authentication forms.

    • Example for sign-in.tsx:

      tsx
      import { SignIn } from "@clerk/nextjs";
      
      const SignInPage = () => {
        return (
          <div>
            <SignIn />
            {/* Additional content or styling as needed */}
          </div>
        );
      };
      
      export default SignInPage;
      
    • Similarly, set up the sign-up.tsx page with the SignUp component.

By following these steps, you'll have integrated Clerk into your Next.js application, providing a seamless authentication experience for your users. Remember to style and customize your NavBar and authentication pages to match the look and feel of your application.

Step 4: Supabase Integration

Integrating Supabase into your Next.js application involves initializing the Supabase client and configuring row-level security to manage data access. Here’s a guide on how to set this up:

  1. Initializing Supabase Client: To interact with Supabase, you need to initialize the Supabase client. This is done by creating a standard client for general operations and a Clerk-authenticated client for operations that require user authentication.

Standard Supabase Client:

  • Create a file named lib/supabase.ts.

  • Import the createClient method from @supabase/supabase-js.

  • Initialize the Supabase client using the Supabase URL and ANON key from your environment variables.

  • Example:

    typescript
    import { createClient } from "@supabase/supabase-js";
    
    const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
    const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
    
    export const supabase = createClient(supabaseUrl, supabaseAnonKey);
    

Clerk-Authenticated Supabase Client:

Server-Side Supabase Client Setup

Server-side setup is crucial for operations that should be securely executed on the server, like direct database manipulations.

  1. Import Clerk's Server-Side Auth Module:

    javascript
    import { auth } from "@clerk/nextjs/server";
    
  2. Retrieve the Authentication Token:

    javascript
    const { getToken } = auth();
    const token = await getToken({ template: "supabase" });
    
  3. Create the Supabase Client:

    javascript
    import { createClient } from "@supabase/supabase-js";
    
    const supabaseServerClient = createClient(
      process.env.NEXT_PUBLIC_SUPABASE_URL,
      process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
      {
        global: {
          headers: {
            Authorization: `Bearer ${token}`,
          },
        },
      }
    );
    
  4. Usage:

    • Use supabaseServerClient for server-side database operations.

Client-Side Supabase Client Setup

The client-side setup is used for operations that require real-time user interaction, like form submissions.

  1. Import Clerk's Client-Side Auth Hook:

    javascript
    import { useAuth } from "@clerk/clerk-react";
    
  2. Initialize the Supabase Client Inside a React Component:

    javascript
    import { useState, useEffect } from "react";
    import { createClient } from "@supabase/supabase-js";
    
    function MyComponent() {
      const [supabaseClient, setSupabaseClient] = useState(null);
      const { getToken } = useAuth();
    
      useEffect(() => {
        const initializeSupabase = async () => {
          const token = await getToken({ template: "supabase" });
          const client = createClient(
            process.env.NEXT_PUBLIC_SUPABASE_URL,
            process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
            {
              global: {
                headers: {
                  Authorization: `Bearer ${token}`,
                },
              },
            }
          );
          setSupabaseClient(client);
        };
    
        initializeSupabase();
      }, []);
    
      // ... rest of your component
    }
    
  3. Usage:

    • Use supabaseClient within your React components for client-side interactions.
  4. Row-Level Security in Supabase: Row-level security (RLS) is a feature that allows you to control access to rows in a database table based on user attributes or other access policies. This is crucial for protecting sensitive data and ensuring that users can only access data they are permitted to see.

    • Setting Up RLS:

      • In your Supabase dashboard, go to the table you want to apply RLS to.
      • Enable Row-Level Security for the table.
      • Create policies that define the conditions under which rows in this table can be accessed, updated, or deleted. For example, you can create a policy that allows users to only read rows where the user ID column matches their own user ID.
      • Test these policies thoroughly to ensure they work as expected and do not inadvertently expose sensitive data.
    • Integrating RLS with Clerk:

      • When using Clerk for authentication, ensure that the user's unique ID from Clerk is stored in your Supabase tables where needed (e.g., as a column in each row).
      • Your RLS policies can then reference this ID to control access based on the authenticated user's ID.

By following these steps, you will successfully integrate Supabase into your Next.js application, complete with a standard client for general database operations and a Clerk-authenticated client for user-specific data access. Additionally, setting up row-level security ensures that your application's data access is secure and compliant with your access policies.