Supabase vs Clerk - Choose the Right Auth for Your SaaS

October 28, 2024
563 views

This is the fourth post in a series of awesome developer tools I intend to write about. In the last 3 posts I talk about Neon, Supabase, Postgres and MongoDB it’s worth revisiting:

  1. Neon Postgres vs. Supabase
  2. MongoDB vs. PostgreSQL
  3. State of Databases for Serverless in 2024

Today I want to talk about auth which is one of the most critical component of any app. Whether you’re building a simple web app or a full-scale app, ensuring that users can securely log in and manage their accounts is essential.

And based on all the feedback—and a good amount of time spent in developer communities, Twitter and Reddit — I’ve picked out 2 popular options for authentication: Supabase and Clerk.

In this article, let's take a detailed look at Supabase Auth and Clerk Auth, I'll focus on their offerings, and how you can implement them with a React frontend and .NET backend. By the end, you’ll have a clear understanding of which solution may work best for you.

If you don’t have time to read and want to jump straight to the code, check out the demo.

What is Clerk

Clerk is a developer-first authentication platform, designed for apps where user management and complex workflows are required. Clerk goes beyond simple user auth, offering features like user profile management, session management, and fine-grained permissions.

What is Supabase

Supabase is an open-source alternative to Firebase, offering various backend services, including authentication, real-time databases, and storage. Supabase’s authentication system is built on PostgreSQL and integrates well with the rest of the Supabase ecosystem.

On Features

Supabase

Key Features of Supabase Auth:

  • Email/Password Authentication - Supabase provides traditional email/password auth with verification links.

  • OAuth Providers - Support for social login through Google, Facebook, GitHub, and other OAuth providers.

  • Magic Links - Passwordless auth using email-based magic links.

  • Secure Session Management - Supabase securely handles sessions with access tokens and refresh tokens.

  • Role-Based Access Control (RBAC) - Users can be assigned roles, and permissions can be managed using policies in the PostgreSQL database.

  • Multi-Factor Authentication (MFA) - With third-party integrations, MFA can be enabled for added security.

Clerk

Key Features of Clerk:

  • Email/Password Authentication - Similar to Supabase, Clerk supports traditional email/password authentication.

  • Social Login - Supports a wide variety of OAuth providers, including Google, Facebook, Twitter, GitHub, and more.

  • Magic Links and Passwordless Login - Clerk allows users to log in with magic links, providing a smooth, passwordless experience.

  • Session Management - Clerk offers built-in session management, with support for multiple active sessions and session revocation.

  • User Profile Management - Clerk provides APIs for managing detailed user profiles, including avatars, email addresses, and phone numbers.

  • Multi-Factor Authentication (MFA) - Clerk natively supports MFA for enhanced security.

  • Customizable User Flows - Clerk gives developers full control over the user onboarding experience, from sign-ups to custom redirect flows.

  • Clerk SDKs - Clerk has official SDKs for React, Next.js, and other frontend frameworks, as well as backend integration for Node.js, .NET, and others.

Next, let’s set them up in local and see a live demo.

On Setup

Supabase - You can sign up via GitHub or SSO.

Supabase

Clerk - You can sign up via GitHub, Google or an Existing email/password combination.

Clerk-Auth

In this article, I've used my GitHub account to sign up to both Supabase and Clerk.

Supabase

Once you sign up and authorize Supabase to use the GitHub account, You will be asked to create an organization.

Supabase-Org
  • Name of the Org - can be a valid company/individual name, I have provided the name as “MyXYZCompany”.

  • Type of Org - Personal for learning purposes

  • Plan - Free

Once you set up the org, you'll get a prompt to add a project - here's what that looks like:

Supabase-Create-Account
  • Organization - The current org will be pre-populated

  • Project Name - Provide a meaningful project name

  • Database Password - Either provide a strong password, or click on Generate a password, make sure to copy this password somewhere secure, this is used to access the PostgreSQL DB

  • Region - Select the region closest to you

Just give it a minute to set up, and boom - your Dashboard will pop up.

Supabase-Dashboard

You can see the API keys and settings at Project Settings -> Configuration -> API

These are important URLs and keys that you are going to use further in the demo.

Supabase-API-Keys

Next, let's setup Clerk.

Clerk

After you sign up to Clerk, You get the below screen to create an app.

  • Provide an `app name, by default Clerk provides Email and Google providers.

When you add Clerk auth to your app, you'll get a simple sign-in screen with Email and Google as default options. Don't worry - you can always add more sign-in methods later if you want.

Let’s use the default options and create the app.

Clerk-Login-Page

Great - You've created your Clerk app and now you'll see your shiny new Dashboard.

Clerk-Auth-Dashboard

You'll find all your settings, keys, and other important stuff in the Configure screen.

Clerk-Auth-Configure-Screen

And there you have it - you're all set up with Clerk. 🎉

On User Creation / OnBoarding

Next, let's create users in both Supabase and Clerk to start setting up the Auth components.

I'll use my email (thewritingwallah@gmail.com) to create accounts on both platforms to show you how it works.

Supabase

  • Go to Authentication tab from the left sidebar -> There are 2 options available in the Add User dropdown

    • Send Invitation

    • Create new user

At the moment for default Email provider scenario for Send Invitation option, Supabase has restricted the authentication emails to org specific email addresses. So if we use the Send Invitation option for our test gmail user, it is going to fail.

Supabase-default-emails-provider

more info on this GitHub Discussion

so hit the Create new user and fill in the details.

Supabase-create-new-users

You can see user in the dashboard.

Supabase-new-user-dashboard

Next, let’s see creation/onboarding of users in Clerk.

Clerk

let’s head over to the Users page.

You’ll see we can either Create a user right from the dashboard or Invite one.

Clerk-Create-Users

Let’s create a user -> fill in the details -> Check the Ignore password policies checkbox for simplicity purposes.

Clerk-How-to-create-a-user

The user gets added to the platform instantly.

Clerk-user-dashboard

Both Clerk and Supabase make adding users simple and quick.

On Front End Integration - React

Next, let's integrate both Supabase and Clerk Auth in a React app.

Supabase

Let’s create a new vite React app.

1npm create vite@latest ./

Supabase provides an auth library specifically for React - check out the docs here.

Let’s install the required supabase libraries in the React app

1npm install @supabase/supabase-js @supabase/auth-ui-react @supabase/auth-ui-shared

Let’s create routes - Home, Login, Success, hook up the react router and create a basic app.

App.jsx

1import React from "react";
2import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
3import Login from "./components/login";
4import Success from "./components/success";
5import Home from "./components/home";
6
7const App = () => {
8 return (
9 <>
10 <Router>
11 <Routes>
12 <Route path="/" element={<Home />} />
13 <Route path="/login" element={<Login />} />
14 <Route path="/success" element={<Success />} />
15 </Routes>
16 </Router>
17 </>
18 );
19};
20
21export default App;

Grab the Supabase Project URL and Project API Key from your Supabase dashboard, and drop them into the .env.local file in your React app. This will set up the Supabase client, which we’ll use to handle user authentication.

These are available at Project Settings -> Configuration -> API

Now, let’s add the authentication in the Login component:

Supabase-API-Configuration

Next, add auth in the Login component.

1import React, { useEffect, useState } from "react";
2import { createClient } from "@supabase/supabase-js";
3import { Auth } from "@supabase/auth-ui-react";
4import { ThemeSupa } from "@supabase/auth-ui-shared";
5import { useNavigate } from "react-router-dom";
6
7const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
8const supabasePublicKey = import.meta.env.VITE_SUPABASE_KEY;
9const supabase = createClient(supabaseUrl, supabasePublicKey);
10
11const Login = () => {
12 const [event, setEvent] = useState("");
13 const navigate = useNavigate();
14
15 useEffect(() => {
16 supabase.auth.onAuthStateChange(async (event) => {
17 console.log(event);
18 if (event === "SIGNED_IN") {
19 setEvent(event);
20 navigate("/success");
21 } else {
22 setEvent(event);
23 navigate("/login");
24 }
25 });
26 }, [event]);
27
28 return (
29 <>
30 <div className="container">
31 <h1 className="center">Login</h1>
32 <Auth
33 supabaseClient={supabase}
34 appearance={{ theme: ThemeSupa }}
35 providers={["github"]}
36 theme="dark"
37 />
38 </div>
39 </>
40 );
41};
42
43export default Login;

Let’s break down what’s happening in the Login component:

  • First, we import createClient, Auth, and ThemeSupa from the Supabase libraries we installed earlier.

  • createClient needs the project URL and project public key to initialize.

  • In the useEffect hook, we check if the user is logged in using supabase.auth.onAuthStateChange() and navigate to the appropriate routes accordingly.

  • Supabase offers a built-in <Auth /> component, which accepts supabaseClient, appearance, providers, and theme as props.

  • supabaseClient is the object we get from calling createClient.

  • appearance lets us set a Supabase theme for the UI.

  • providers is an array where we can configure OAuth providers like GitHub, Google, Twitter, and more.

Alright, let’s run the app.

1cd supabse-react
2npm run dev

and click on login button and you'll see a supabase theme page.

Supabase-React-App-Local

The Supbase’s pre-built <Auth /> provides the look and feel, and we don’t need to configure any styles for it.

Let’s create the Success component that we are going to navigate to post successful login.

1import { useNavigate } from "react-router-dom";
2import { useEffect, useState } from "react";
3import { createClient } from "@supabase/supabase-js";
4
5const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
6const supabasePublicKey = import.meta.env.VITE_SUPABASE_KEY;
7const supabase = createClient(supabaseUrl, supabasePublicKey);
8
9const Success = () => {
10 const navigate = useNavigate();
11 const [user, setUser] = useState("");
12
13 useEffect(() => {
14 async function userData() {
15 const { data } = await supabase.auth.getUser();
16 if (data?.user) {
17 setUser(data.user);
18 }
19 }
20 userData();
21 }, []);
22
23 async function logOut() {
24 await supabase.auth.signOut();
25 navigate("/login");
26 }
27
28 return (
29 <>
30 {Object.keys(user).length !== 0 ? (
31 <>
32 <h1>Success</h1>
33 <button onClick={logOut}>LogOut</button>
34 </>
35 ) : (
36 <>
37 <h1>User is not logged in</h1>
38 <button onClick={() => navigate("/")}>Go back to home</button>
39 </>
40 )}
41 </>
42 );
43};
44
45export default Success;

Let’s walk through what’s going on in the Success component:

  • In the useEffect hook, we are trying to set the authenticated user details so that we can find whether he/she has been authenticated or not.

  • If the user details exist, we will display the Success header, along with a logout button, else we will display “user is not logged in” message, along with the “Go back to home” link. This check will also prevent unauthenticated browsing of the success URL directly from the browser.

  • In the logout function, we call the supabase.auth.signout() function and it logouts the user.

Let’s login with the test user (thewritingwallah@gmail.com) that we had created earlier.

Supabase-Local-React-APp

The Supabase authentication with React works great and is super simple.

Now, let’s get started with setting up Clerk authentication in our React app.

Clerk

Let’s create another React app to integrate Clerk Auth.

Clerk provides us with built in components like <SignInButton />, <UserButton /> <SignedIn />, <SignedOut /> etc to help in the integration.

We need to grab the Publishable Key from the Clerk dashboard and add it to our .env.local file for authentication.

You can find this key under Configure -> Developers -> API Keys.

Clerk-API-Keys-Dashboard

Importing Clerk Dependencies

Let’s import the Clerk dependencies in the app.

You can find the details about the required dependencies in the documentation.

1npm install @clerk/clerk-react

Now, we need to add the reference to the publishable key in the main component and wrap the code inside a <ClerkProvider /> component.

ClerkProvider expects the publishableKey and afterSignOutUrl as props.

1import { StrictMode } from "react";
2import { createRoot } from "react-dom/client";
3import App from "./App.jsx";
4import "./index.css";
5import { ClerkProvider } from "@clerk/clerk-react";
6
7const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;
8if (!PUBLISHABLE_KEY) {
9 throw new Error("Missing Publishable Key");
10}
11
12createRoot(document.getElementById("root")).render(
13 <StrictMode>
14 <ClerkProvider publishableKey={PUBLISHABLE_KEY} afterSignOutUrl="/">
15 <App />
16 </ClerkProvider>
17 </StrictMode>
18);

Now, let’s hook up the auth in the App component.

1import "./App.css";
2import {
3 SignedIn,
4 SignedOut,
5 SignInButton,
6 UserButton,
7} from "@clerk/clerk-react";
8
9import Success from "./components/success";
10function App() {
11 return (
12 <>
13 <h1>Clerk Auth</h1>
14 <header>
15 <SignedOut>
16 <SignInButton />
17 </SignedOut>
18 <SignedIn>
19 <UserButton />
20 <Success />
21 </SignedIn>
22 </header>
23 </>
24 );
25}
26
27export default App;

Let’s break down what’s happening in the code:

  • We import the pre-built SignedIn, SignedOut, UserButton, SignInButton from clerk React library.

  • We wrap SignInButtton inside SignedOut component, which means it will show the sign-in button only when the user isn’t logged in.

Now, let’s run the app and click on the SignIn button.

Clerk-SignIn-Page

Let’s enter the test user we created on the Clerk dashboard: thewritingwallah@gmail.com, and then click Continue. After that, enter the password and click Continue. If everything goes well, you’ll see a success message and be taken to the next page.

Clerk-Auth-Page-Success

The <UserButton /> component shows up, and when you click on it, you can see the basic details of your account.

Clerk authentication with React is super smooth and really easy to set up.

On Backend Integration - .NET 8 Web APIs

We’ve learned how to integrate our React apps with both Supabase and Clerk.

But just the front end isn’t enough; we always need a backend to support a data-driven app. So, let's look at how Supabase and Clerk fit into the backend.

In this section, let's build a sample .NET 8 Web APIs, secure them with Supabase and Clerk auth, and call those protected APIs from our secure React front end.

Let’s keep building.

Supabase

Let’s create a new .NET 8 Web API app.

1dotnet new webapi

We’ll be using the below Nuget package to work with the Supabase JWT tokens.

1dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 8.0.8

Let’s open the Program.cs file of the web api solution and add support for JWT auth.

1public class Program
2 {
3 public static void Main(string[] args)
4 {
5 var builder = WebApplication.CreateBuilder(args);
6 var configuration = builder.Configuration;
7
8 // Add services to the container.
9
10 builder.Services.AddControllers();
11 // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
12 builder.Services.AddEndpointsApiExplorer();
13 builder.Services.AddSwaggerGen();
14
15 builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
16 .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
17 {
18 options.TokenValidationParameters = new TokenValidationParameters
19 {
20 ValidateAudience = true,
21 ValidateIssuer = true,
22 ValidateIssuerSigningKey = true,
23 ValidIssuer = configuration["SupabaseSettings:Issuer"],
24 ValidAudience = configuration["SupabaseSettings:Audience"],
25 IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["SupabaseSettings:SecretKey"]))
26 };
27 });
28
29 builder.Services.AddCors(options =>
30 {
31 options.AddPolicy("Cors", p =>
32 {
33 p.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin();
34 });
35 });
36
37 var app = builder.Build();
38
39 // Configure the HTTP request pipeline.
40 if (app.Environment.IsDevelopment())
41 {
42 app.UseSwagger();
43 app.UseSwaggerUI();
44 }
45
46 app.UseHttpsRedirection();
47 app.UseCors("Cors");
48
49 app.UseAuthentication();
50 app.UseAuthorization();
51
52
53 app.MapControllers();
54
55 app.Run();
56 }
57 }

Here’s what we’ve done:

  • We’ve added support for JWT authentication using builder.Services.AddAuthentication().

  • The token validation now includes all the important parameters:

  • ValidateAudience is set to true.

  • ValidateIssuer is set to true.

  • ValidateIssuerSigningKey is set to true.

  • We’ve also specified the ValidIssuer, ValidAudience, and IssuerSigningKey.

  • For ValidIssuer, we have “SupabaseProjectURL/auth/v1”.

  • The ValidAudience is set to “authenticated”.

  • For IssuerSigningKey, we use the JWT secret from the Supabase API dashboard.

Just a heads-up - this is a sensitive key, so keep it safe and don’t share it or check it into source control.

Now, let’s add these settings in the appsettings.json file and use the Configuration pattern to get them in the Program.cs file.

1"SupabaseSettings": {
2 "Issuer": "https://vcripehfbbgogiwvzbgo.supabase.co/auth/v1",
3 "Audience": "authenticated",
4 "SecretKey": "SECRET JWT KEY FROM SUPABASE DASHBOARD"
5 }

Now, let's create a ValuesController and a protected GET endpoint “names”.

1[Route("api/[controller]")]
2 [ApiController]
3 public class ValuesController : ControllerBase
4 {
5 public class Employee
6 {
7 public int Id { get; set; }
8 public string Name { get; set; }
9 public string Department { get; set; }
10 }
11
12 [Authorize]
13 [HttpGet("names")]
14 public IActionResult GetEmployee()
15 {
16 var emp = new Employee
17 {
18 Id = 101,
19 Name = "John Doe",
20 Department = "IT"
21 };
22
23 return Ok(emp);
24 }
25 }

We would get a 401 UnAuthorized once we try to access the endpoint without an authenticated user.

Swagger-UI-API-Test

We would need a token to access the endpoint, let’s integrate this API into the React app and use the authenticated user token to access the endpoint.

Let’s make changes to the React app success component to call the protected API.

1import { useNavigate } from "react-router-dom";
2import { useEffect, useState } from "react";
3import { createClient } from "@supabase/supabase-js";
4import axios from "axios";
5
6const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
7const supabasePublicKey = import.meta.env.VITE_SUPABASE_KEY;
8const supabase = createClient(supabaseUrl, supabasePublicKey);
9
10const Success = () => {
11 const navigate = useNavigate();
12 const [user, setUser] = useState("");
13 const [token, setToken] = useState("");
14 const [emp, setEmp] = useState({});
15
16 useEffect(() => {
17 async function userData() {
18 const { data } = await supabase.auth.getUser();
19 if (data?.user) {
20 setUser(data.user);
21
22 const item = localStorage.getItem("sb-vcripehfbbgogiwvzbgo-auth-token");
23 const parsedItem = JSON.parse(item);
24 const accessToken = parsedItem["access_token"];
25
26 setToken(accessToken);
27 }
28 }
29 userData();
30 }, []);
31
32 async function logOut() {
33 await supabase.auth.signOut();
34 navigate("/login");
35 }
36
37 async function callAPI() {
38 const API_URL = "https://localhost:7000/api/Values/names";
39 const config = {
40 headers: {
41 Authorization: `Bearer ${token}`,
42 },
43 };
44
45 const response = await axios.get(API_URL, config);
46 setEmp(response.data);
47 }
48
49 return (
50 <>
51 {Object.keys(user).length !== 0 ? (
52 <>
53 <h1>Success</h1>
54 <button onClick={logOut}>LogOut</button>
55 <button onClick={callAPI}>Call API</button>
56 <div className="empContainer">
57 <p>{emp.id}</p>
58 <p>{emp.name}</p>
59 <p>{emp.department}</p>
60 </div>
61 </>
62 ) : (
63 <>
64 <h1>User is not logged in</h1>
65 <button onClick={() => navigate("/")}>Go back to home</button>
66 </>
67 )}
68 </>
69 );
70};
71
72export default Success;

We have a:

  • Call API button, on the click of which we have a function callAPI(). We use axios to call the external API, We extract the user token from the local storage and bind it to the Authorization header of the API call.

  • We bind the Employee details on successful response of the API.

Now, Let’s run both React/.NET apps and see them in action.

  • Login to the React app
  • Click on the Call API button
  • The control comes over to the protected “names” endpoint on the .NET API side
API

API data successfully appears on the UI.

API_Data

So, we have successfully authenticated with Supabase in both frontend and backend. The logged-in user is able to access the protected API.

Clerk

Let’s see how we can integrate Clerk auth in .NET 8 Web API.

Create a new .NET 8 Web API app.

1dotnet new webapi

Add the JWT Nuget package to work with the Clerk auth.

1dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 8.0.8

In the Program.cs file, add this code.

1using Microsoft.AspNetCore.Authentication.JwtBearer;
2using Microsoft.IdentityModel.Tokens;
3using System.Text;
4
5namespace Clerk.Auth
6{
7 public class Program
8 {
9 public static void Main(string[] args)
10 {
11 var builder = WebApplication.CreateBuilder(args);
12 var configuration = builder.Configuration;
13
14 // Add services to the container.
15
16 builder.Services.AddControllers();
17 // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
18 builder.Services.AddEndpointsApiExplorer();
19 builder.Services.AddSwaggerGen();
20
21 builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
22 .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
23 {
24 options.Authority = configuration["ClerkSettings:Authority"];
25 options.TokenValidationParameters = new TokenValidationParameters
26 {
27 ValidateAudience = false,
28 ValidateIssuer = false,
29 ValidateIssuerSigningKey = true,
30 IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["ClerkSettings:SecretKey"]))
31 };
32 options.Events = new JwtBearerEvents
33 {
34 OnTokenValidated = context =>
35 {
36 var principal = context.Principal;
37 var azpClaim = principal?.Claims.FirstOrDefault(x=>x.Type=="azp");
38 if(azpClaim is null || azpClaim.Value != builder.Configuration["ClerkSettings:AZP"])
39 {
40 context.Fail("AZP Claim is invalid or missing");
41 }
42 return Task.CompletedTask;
43 },
44 OnMessageReceived = context =>
45 {
46 return Task.CompletedTask;
47 }
48 };
49 });
50
51 builder.Services.AddCors(options =>
52 {
53 options.AddPolicy("Cors", p =>
54 {
55 p.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin();
56 });
57 });
58
59 var app = builder.Build();
60
61 // Configure the HTTP request pipeline.
62 if (app.Environment.IsDevelopment())
63 {
64 app.UseSwagger();
65 app.UseSwaggerUI();
66 }
67
68 app.UseHttpsRedirection();
69 app.UseCors("Cors");
70
71 app.UseAuthentication();
72 app.UseAuthorization();
73
74 app.MapControllers();
75
76 app.Run();
77 }
78 }
79}

We have:

  • Added support for JWT auth scheme using builder.Services.AddAuthentication() builder.

  • The token validation parameters have all the mandatory parameters:

  • Authority as Clerk project URL

  • ValidateAudience as false

  • ValidateIssuer as false

  • ValidateIssuerSigningKey as true

  • IssuerSigningKey

  • AZP is Authorized Party = “http://localhost:5173”

  • SecretKey = The JWT secret from the Clerk API dashboard

Clerk-Default-Secret-Key

again a heads-up: this is a sensitive key, so keep it safe and don’t share it or check it into source control.

1"ClerkSettings": {
2 "Authority": "https://humble-rhino-42.clerk.accounts.dev",
3 "AZP": "http://localhost:5173",
4 "SecretKey": "CLERK DASHBOARD SECRET KEY"
5 }

Clerk Auth on .NET side is a bit different as compared to Supabase, it does not validate the audience or issuer, rather it validates and checks for the “azp” claim in the incoming token.

azp” refers to Authorized Party, in our example we have set Authorized Party to “localhost:5173” which is the local home page of the Vite React app. Hence in the OnTokenValidated event, we are checking the “azp” claim in the incoming token.

Now that we have set up the auth on the backend, lets create a protected test endpoint.

1[Route("api/[controller]")]
2 [ApiController]
3 public class NamesController : ControllerBase
4 {
5 [Authorize]
6 [HttpGet("test")]
7 public IActionResult Test()
8 {
9 return Ok("This is the response from the sample protected endpoint");
10 }
11 }

We get a 401 Unauthorized while trying to browse this endpoint as it is, without a token.

Clerk-API-keys

Let’s update our Clerk React app to extract the access token and call the protected API endpoint.

1import React, { useState } from "react";
2import axios from "axios";
3import { useAuth } from "@clerk/clerk-react";
4
5
6const Success = () => {
7 const [data, setData] = useState("");
8 const { getToken } = useAuth();
9
10
11 async function callAPI() {
12 const API_URL = "https://localhost:7110/api/Names/test";
13 const token = await getToken();
14 console.log("token", token);
15 let config = {
16 headers: {
17 Authorization: "Bearer " + token,
18 },
19 };
20 const response = await axios.get(API_URL, config);
21 setData(response.data);
22 }
23 return (
24 <>
25 <h1>Success page</h1>
26 <button onClick={callAPI}>Call API</button>
27 <div>
28 <p>{data}</p>
29 </div>
30 </>
31 );
32};
33
34
35export default Success;

We use:

  • The useAuth hook from clerk-react that helps to grab the access token post successful login.

  • We have a Call API button which performs an axios GET request and appends the token in the Authorization header of the request.

  • We populate the div with the response of the API call.

Let’s run both the React and .NET apps and see how they work together.

Clerk-Auth-Success-Page-React

Click on the Call API button, the control goes to the .NET end, and we see the “azp” claim in the incoming token that is captured in the OnTokenValidated event.

Clerk-API

Once the token validation is successful, we get a positive response on the UI. This means we’ve successfully authenticated with Clerk on both the frontend and backend, and the logged-in user can access the protected API.

Both Supabase and Clerk are really easy to integrate on the backend as well.

On OAuth Provider Integration - Github

In this section, we are going to integrate GitHub login in our apps and see how Supabase and Clerk works against each other.

Supabase

Click on Authentication -> Providers in Supabase Dashboard, and we will see an extensive list of various OAuth providers that Supabase supports.

Supabase-OAuth

Let’s click on GitHub and add the details. We see that the Callback URL is pre-populated while Client ID and Client Secret fields are empty. Where to get these details from?

Supabase-OAuth-List

We need to create an OAuth app on GitHub end, and then we will get the Client ID/Secret that we can populate at Supabase’s end.

Let’s go to GitHub -> Login -> Settings -> Developer Settings -> OAuth Apps -> New OAuth App

GitHub-OAuth-Apps OAuth-Github-app

Register a new OAuth app.

  • Application Name - provide a meaningful name

  • Homepage URL - React app’s local home page url - localhost:5173

  • Application Description - provide some meaningful description

  • Authorization Callback URL - the call back URL where in the auth will fall to after successful operation, Supabase provides it, copy and paste it over here

After successfully registering the app, generate a client secret and copy both the Client ID and Client Secret over to Supabase.

Github-auth-demo-app

Let’s go to our Supabase React app and add Github as a provider, so that it appears as a login mechanism.

In the Login screen -> <Auth /> component, add the providers prop and mention “github” over there. Providers is an array that can include all the OAuth providers that have been setup.

1return (
2 <>
3 <div className="container">
4 <h1 className="center">Login</h1>
5 <Auth
6 supabaseClient={supabase}
7 appearance={{ theme: ThemeSupa }}
8 providers={["github"]}
9 theme="dark"
10 />
11 </div>
12 </>
13 );
14};
Supabase

This is the only configuration that is required at the code end, let’s run the app and observe.

We see a Sign in with GitHub link on the React Login page, let’s click on it.

We see the GitHub OAuth Authorize screen, click on the Authorize button.

Github-app

We are redirected back to localhost:5173 because it is what we have configured in github, and we see the token in the local storage of the browser, with the provider as github.

Clerk

Let’s integrate GitHub OAuth provider in Clerk.

Go to Clerk Dashboard -> Configure -> SSO Connections -> Add Connection -> Choose Github from the dropdown.

Clerk-GitHub-OAuth Clerk-GitHub

GitHub will be added as an ´OAuth` provider.

Clerk-Github

Let's run the React app and click the Signin button, we will see GitHub also as a login provider.

Clerk-Github

Click on GitHub link, and it will ask to authorize Clerk with the logged in GitHub user id.

Clerk-Github-providor

Upon authorizing it, we will be redirected back to the Success page with the UserButton component populated.

Clerk-AUth

This is the trivial way, the limitation of it is that every time we will be asked to authorize Clerk via github, to encounter this, we can click on Use Custom Credential option in Github in SSO Connections page. 

let's create a separate OAuth app on Github, then enter the Client ID/Secret at Clerk’s end, the same way we did for Supbase.

Clerk

Create a new OAuth app on GitHub, and copy the Client ID/Secret at Clerk’s end.

Upon click of the GitHub button on the Login Screen, the user will be prompted to the below Authorize screen and the login will be successful.

Let me recap and summarize all the key features I’ve covered.

Who wins between Supabase vs Clerk

Overview

Supabase

Open-source Firebase alternative providing backend services, including authentication, real-time database, and storage.

Clerk

A full-featured authentication solution focused on seamless integration and user management, primarily for front-end applications.

OAuth Support

Supabase

Supports OAuth providers like Google, GitHub, GitLab, Twitter, etc.

Clerk

Native OAuth integrations with popular providers like GitHub, Google, Facebook, and more.

Multi-Factor Authentication

Supabase

Native support for MFA via authenticator apps (TOTP) and phone verification. Includes enrollment, challenge/verify APIs, and configurable security policies. Learn more here

Clerk

Built-in multi-factor authentication (MFA) with multiple verification methods.

Session Management

Supabase

Provides JWT-based session management. Supports server-side token refresh.

Clerk

Manages sessions via Clerk's SDK, with server and client-side control over session duration and renewal.

Frontend Integration

Supabase

Can be integrated with custom UI via SDKs (JavaScript, React, etc.).

Clerk

Pre-built components for React, Next.js, and other frontend frameworks for a seamless user experience.

Backend Integration

Supabase

Typically paired with Supabase's PostgreSQL database. JWT tokens can be validated for API requests.

Clerk

Easy API access and integration with Clerk-managed sessions for backend validation and role-based access.

Pricing

Supabase

Free tier with limits on users and authentication events, paid plans for higher usage.

Clerk

Free tier available with pricing based on active users; advanced features require paid plans.

Customization

Supabase

Customizable authentication flow, but requires more setup effort for fully custom UI.

Clerk

High-level of customization with pre-built or custom authentication components.

Ease of Use

Supabase

Requires more setup for a full authentication solution, but more flexible as part of a larger BaaS.

Clerk

Highly user-friendly and developer-friendly with minimal configuration needed for basic authentication.

Documentation

Supabase

Extensive documentation, but the learning curve can be steeper due to the wide range of features. checkout Supabase docs here

Clerk

Clear and well-documented with examples, focused on authentication needs. checkout Clerk docs here

Conclusion

I’ve taken an in-depth look at both Supabase and Clerk authentications in this article.

Just to clarify, this is not a sponsored post, and as promised, no posts on this blog, DevTools Academy, will ever be sponsored. I want to share completely unbiased views on amazing developer tools. If you spot any mistakes or if you love what you read, I’d really appreciate your feedback. Feel free to share any questions you have so I can consider adding them to the blog.

  • Supabase: More focused on being a full backend as a service with authentication as part of its suite of tools. Ideal if we're looking for an open-source Firebase alternative with additional features like databases, storage, and server-side functions.

  • Clerk: Specializes in user auth and management, offering pre-built components and a front-end-centric approach. It provides a higher level of abstraction for authentication with minimal configuration required, particularly useful for complex user management scenarios.

Wrapping Up

Congratulations. 🎉 You’ve completed the project for this comparison tutorial.

You can find the full code for the app we built in this repository: Supabase Clerk Auth Demo.

Happy coding, and feel free to explore more with Supabase and Clerk.

Did you enjoy reading this article and have you learned something new? please share it on social media, reddit or in your discord communities.

See you soon in my next article. In the meantime, take care of yourself and keep learning. If you have any topic suggestions, feel free to raise an issue on GitHub.

Developer Chatter Box 💬

Join the discussion. Share your thoughts on dev tools, give feedback on the post 💪

Hey there, code whisperer. Sign in to join the conversation.

Be the first to break the silence. Your comment could start a revolution (or at least a fun thread).

Remember: Be kind, be constructive, and may your code always compile on the first try. 🍀