Kinde with Supabase
By Vlad Ionash —
Kinde handles user authentication, manages session tokens, and offers user management features, which can be integrated with the authorization mechanisms provided by Supabase, utilizing PostgreSQL’s Row Level Security (RLS) policies.
We will explore how to connect and leverage a Supabase database with the capabilities of Kinde, an authentication and authorization provider.
We will be going through:
- Kinde setup
- Supabase setup
- Integration
- Creating a sample To-Do application that leverages both systems
- Login to your Kinde account.
- Navigate to “Settings” and then to “Applications”
- Select your application that you are currently using or create a new one.
💡 Make sure to select the “Back-end web”. Front-end apps do not provide a “Client secret” which will be required for this to work.
Under “Details”, take note of the
- “Domain”
- “Client ID”
- “Client secret”
- Navigate to the “Callback URLs” section at the bottom and input your details. For this example I will be using the localhost, so it will be as follows for now. Take note of these URLs, we can change this later.
- Select “Save”.
- On the dashboard, create a new project.
- Give it a name and password. Be sure to select the region most appropriate for your application, more than likely this is the region that you selected when you signed up for Kinde. Then select create new project.
- This will take some time. Till then, take note of the ID’s that are provided to you as we will need to use these in our app.
Specifically take note of the:
- URL:
- anon public key:
- Once it’s done building, we will need to navigate to the settings page.
- Then to the API Page.
- Here you will scroll till you see the JWT Settings.
We will need to generate a new secret, more specifically we will use the same secret as the Kinde Client secret. We will be using Kinde to authenticate that these requests are indeed coming from the source we want.
- Input your Kinde Client secret then select apply.
- Navigate to the Table Editor.
- Give it a name that you’ll reference later.
Make sure to add columns for task
as text, user_id
as text, and completed_state
as bool
Then select save.
- In your new database, lets insert a few dummy rows. We will use these to test our app later down into the guide.
After you’re done lets move on to the next step.
Let’s create a basic app to test out the functionality and authentication of the Kinde ↔ Supabase integration. We will be using the Pages Router SDK to accomplish this, but this will work with the App Router SDK as well.
This app will simply check our JWT and authenticate what we see based on User ID and the table we have created.
- Let’s create our new project.
npx create-next-app <NAME_OF_YOUR_TODO_PROJECT>
cd <NAME_OF_YOUR_TODO_PROJECT>
⚠️ Make sure to choose “No” for app router if you’re following this guide
- Inside the root directory of your project, let’s install the dependencies we will be needing.
npm i @supabase/supabase-js
npm i @kinde-oss/kinde-auth-nextjs
- In your root directory, create a file called
env.local
and populate it with the below URLs and Keys we have acquired so far:
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
KINDE_CLIENT_ID=
KINDE_CLIENT_SECRET=
KINDE_ISSUER_URL=https://<YOUR_DOMAIN>.kinde.com
KINDE_SITE_URL=http://localhost:3000
KINDE_POST_LOGOUT_REDIRECT_URL=http://localhost:3000
KINDE_POST_LOGIN_REDIRECT_URL=http://localhost:3000/todo
- Create the following file
/pages/api/auth/[...kindeAuth].js
inside your Next.js project. Inside the file[...kindeAuth].js
put this code:
{handleAuth} from "@kinde-oss/kinde-auth-nextjs/server";
export default handleAuth();
This will handle Kinde Auth endpoints in your Next.js app.
⚠️ Important: Our SDK relies on this file existing in this location specified above.
- Now create another file
pages/_app.js.
Inside, put:
// app/layout.tsx
"use client";
import {KindeProvider} from "@kinde-oss/kinde-auth-nextjs";
function MyApp({ Component, pageProps }) {
return (
<KindeProvider>
<Component {...pageProps} />
</KindeProvider>
);
}
export default MyApp
- Let’s populate our
index.js
file in thepages
directory with the following:
import {RegisterLink, LoginLink, LogoutLink} from "@kinde-oss/kinde-auth-nextjs/components";
const HomePage = () => {
const containerStyle = {
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
height: "100vh" // Use full height of the viewport
};
const navStyle = {
marginTop: "20px"
};
const linkStyle = {
display: "inline-block",
margin: "10px",
padding: "10px 20px",
color: "white",
backgroundColor: "#007bff",
textDecoration: "none",
borderRadius: "5px",
fontWeight: "bold"
};
return (
<div style={containerStyle}>
<h1>Welcome to the Home Page</h1>
<nav style={navStyle}>
<LoginLink style={linkStyle}>Sign in</LoginLink>
<RegisterLink style={linkStyle}>Sign up</RegisterLink>
<LogoutLink style={linkStyle}>Log out</LogoutLink>
</nav>
</div>
);
};
export default HomePage;
👉 From here, you can run
npm run dev
to check your progress. You should have a modest start page with 3 buttons. If you select “Sign in” or “Sign up”, you should now be redirected to and able to authenticate with Kinde. The redirect will lead to a 404 as we have not yet built it yet!
- I find it helpful to always have a reference to the JWT tokens and it’s parameters on hand for the current user that’s signed in. This makes for a handy way to get access to the parameters and data that you would be potentially using. As such, I like to create a separate page called
pages/token.js
with the following code snippet:
import {useKindeAuth} from "@kinde-oss/kinde-auth-nextjs";
export default function ClientPage() {
const {
isLoading,
user,
permissions,
organization,
userOrganizations,
accessToken,
getBooleanFlag,
getClaim,
getFlag,
getIntegerFlag,
getPermission,
getStringFlag,
isAuthenticated,
error
} = useKindeAuth();
if (isLoading) return <div>Loading...</div>;
return (
<div className="pt-20">
<div className="mb-8">
<h4 className="text-2xl font-bold dark:text-white mb-2">User</h4>
<pre className="p-4 rounded bg-slate-950 text-green-300">
{JSON.stringify(user, null, 2)}
</pre>
</div>
<div className="mb-8">
<h4 className="text-2xl font-bold dark:text-white mb-2">Permissions</h4>
<pre className="p-4 rounded bg-slate-950 text-green-300">
{JSON.stringify(permissions, null, 2)}
</pre>
</div>
<div className="mb-8">
<h4 className="text-2xl font-bold dark:text-white mb-2">Organization</h4>
<pre className="p-4 rounded bg-slate-950 text-green-300">
{JSON.stringify(organization, null, 2)}
</pre>
</div>
<div className="mb-8">
<h4 className="text-2xl font-bold dark:text-white mb-2">User organizations</h4>
<pre className="p-4 rounded bg-slate-950 text-green-300">
{JSON.stringify(userOrganizations, null, 2)}
</pre>
</div>
<div className="mb-8">
<h4 className="text-2xl font-bold dark:text-white mb-2">Access Token</h4>
<pre className="p-4 rounded bg-slate-950 text-green-300">
{JSON.stringify(accessToken, null, 2)}
</pre>
</div>
<div className="mb-8">
<h4 className="text-2xl font-bold dark:text-white mb-2">Is Authenticated</h4>
<pre className="p-4 rounded bg-slate-950 text-green-300">
{JSON.stringify(isAuthenticated, null, 2)}
</pre>
</div>
<div className="mb-8">
<h4 className="text-2xl font-bold dark:text-white mb-2">error</h4>
<pre className="p-4 rounded bg-slate-950 text-green-300">
{JSON.stringify(error, null, 2)}
</pre>
</div>
<div className="mb-8">
<h4 className="text-2xl font-bold dark:text-white mb-2">Get boolean flag</h4>
<pre className="p-4 rounded bg-slate-950 text-green-300">
{JSON.stringify(getBooleanFlag("bodsa", true), null, 2)}
</pre>
</div>
<div className="mb-8">
<h4 className="text-2xl font-bold dark:text-white mb-2">Get claim</h4>
<pre className="p-4 rounded bg-slate-950 text-green-300">
{JSON.stringify(getClaim("bodsa"), null, 2)}
</pre>
</div>
<div className="mb-8">
<h4 className="text-2xl font-bold dark:text-white mb-2">Get integer flag</h4>
<pre className="p-4 rounded bg-slate-950 text-green-300">
{JSON.stringify(getIntegerFlag("bodsa", 1), null, 2)}
</pre>
</div>
<div className="mb-8">
<h4 className="text-2xl font-bold dark:text-white mb-2">Get string flag</h4>
<pre className="p-4 rounded bg-slate-950 text-green-300">
{JSON.stringify(getStringFlag("bodsa", "dsad"), null, 2)}
</pre>
</div>
<div className="mb-8">
<h4 className="text-2xl font-bold dark:text-white mb-2">Get permission</h4>
<pre className="p-4 rounded bg-slate-950 text-green-300">
{JSON.stringify(getPermission("bodsa"), null, 2)}
</pre>
</div>
<div className="mb-8">
<h4 className="text-2xl font-bold dark:text-white mb-2">Get flag</h4>
<pre className="p-4 rounded bg-slate-950 text-green-300">
{JSON.stringify(getFlag("bodsa", "dsad", "s"), null, 2)}
</pre>
</div>
</div>
);
}
👉 You can visit this page any time by going to
localhost:3000/token
- Create
utils/supabaseClient.js
and in it put:
// supabaseClient.js
import {createClient} from "@supabase/supabase-js";
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
const supabase = createClient(supabaseUrl, supabaseAnonKey);
export default supabase;
This is how we will be creating the connection between Supabase and our app. Supabase automatically authenticates the API connections with the JWT token that we have setup. Since our Kinde and Supabase token secrets are the same (See the Supabase Setup above), the authentication handshake should be done automatically and we do not have to do anything else.
- Create
pages/todo.js
:
import supabase from "../utils/supabaseClient";
import {getKindeServerSession} from "@kinde-oss/kinde-auth-nextjs/server";
import {LoginLink} from "@kinde-oss/kinde-auth-nextjs/components";
export async function getServerSideProps({req, res}) {
const {isAuthenticated} = getKindeServerSession(req, res);
const isAuthed = await isAuthenticated();
const {data, error} = await supabase
.from("todos") // Replace with your table name
.select("*");
if (error) {
console.error(error);
return {props: {data: []}};
}
return {props: {isAuthed, data}};
}
export default function Home({isAuthed, data}) {
return isAuthed ? (
<div style={{display: "flex", justifyContent: "center", marginTop: "50px"}}>
<table style={{width: "60%", borderCollapse: "collapse"}}>
<thead>
<tr>
<th
style={{
border: "1px solid #ddd",
padding: "8px",
textAlign: "left",
backgroundColor: "#f2f2f2"
}}
>
ID
</th>
<th
style={{
border: "1px solid #ddd",
padding: "8px",
textAlign: "left",
backgroundColor: "#f2f2f2"
}}
>
Task
</th>
<th
style={{
border: "1px solid #ddd",
padding: "8px",
textAlign: "left",
backgroundColor: "#f2f2f2"
}}
>
Is Complete
</th>
</tr>
</thead>
<tbody>
{data.map((row) => (
<tr key={row.id}>
<td style={{border: "1px solid #ddd", padding: "8px"}}>{row.id}</td>
<td style={{border: "1px solid #ddd", padding: "8px"}}>{row.task}</td>
<td style={{border: "1px solid #ddd", padding: "8px"}}>
{String(row.is_complete)}
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div>
This page is protected, please <LoginLink>Login</LoginLink> to view it.
</div>
);
}
Here is the main logic of the functionality. We will be displaying the table if we are logged in and are currently authenticated. If the status fails, we will instead be shown a “This page is protected, please Login” dialogue prompting us to sign in.
Now if you try to go through the authentication flow, your table most likely will show up as empty with only the headers. This is due to Supabase policy restrictions as we have not defined who can and cannot see the table.
Lets first get our user ID for the profile that we are signing into to test. You can find this by simply navigating to localhost:3000/token
as defined in Step 6.5, or you can navigate to your Kinde Portal, under Users.
Take note of the “User Id”.
- Navigate back to your database as defined in Step 10 from above.
- Change the user_id text of a couple of rows to the “User ID” that you retrieved from the previous steps. This will allow us to filter this database down the line.
- Select “SQL Editor” thats on the left hand menu on the dashboard.
- Here we will be putting in a custom SQL command to parse the JWT that will be incoming into the database. Specifically, we will be needing the user_id attribute from the JWT Claims portion of the token. A sample SQL function to retrieve that would be:
create or replace function get_user_id()
returns text
language sql stable
as $$
select nullif(current_setting('request.jwt.claims', true)::json->>'sub', '')::text;
$$;
This function gets the user_id of the requesting party from the JWT, then matches it with the user_id that we have setup in our table. This can lead to such functionality as displaying only the intended material to the end user that they can see based on ID type of the database that we created.
We now need to define the policy for access to the database that we created.
- Select “Authentication“.
- Select “Policies”.
- Create a new policy and will be using a custom authentication from scratch.
- Fill in the policy name and for the expression put
auth.user_id() = user_id
This will then map the requesting users with the ID in the database that we have configured.
- Select “Review” and then “Save”, and you’re done!
You can test this out by going back to your project at localhost:3000
, signing in and redirecting to localhost:3000/todo q1
and you should see your updated database results! Notice how the results displayed are not the full database that we created in the previous step as it will only display the rows that are associated with your user_id
.
You can change and update the database and your app will reflect it once you refresh the page. You can obviously extend this table to support inserting and deleting rows, but that’s for another time.
Have fun!