Kinde and edge worker services
Integrations
User authentication and secure data access are critical components of modern web applications. Kinde simplifies the process by handling user authentication, managing session tokens, and offering robust user management features.
When combined with Supabase’s powerful database management and PostgreSQL’s Row-Level Security (RLS) policies, you can create a secure, scalable application that ensures user-specific data access.
In this guide, we will walk through integrating Kinde and Supabase to build a personalized and secure to-do application. We’ll use Next.js as the application framework.
Sign in to Kinde and on the front page, select Add application.
Enter a name for the application and select Back-end web as the application type.
Select Save. The Quick start page opens.
Select NextJS from the list of back-end SDKs, then select Save.
On the Quick Start page, select either Starter Kit or Existing codebase and follow the setup steps. For either path:
Select Set for each default callback URL to configure your application’s callback settings.
Copy the environment variables. We will use these later.
Select Authentication in the menu.
Enable the authentication types you want your users to sign in with (e.g. Email and Google), and select Save.
Sign in to your Supabase dashboard.
Select New Project.
Enter a project name.
Set a secure password for your database.
Select a database region that best suits your application.
Select Create new project.
In the Project settings, go to Configuration > Data API. Copy the following:
anon
public
.Scroll to the JWT Settings section.
Select Generate a new secret, then Create my own secret. You need to use the same secret as your application in Kinde.
In Kinde, go to Settings > Applications > Your application, and copy the Client Secret.
Paste it into the Custom JWT secret field and select Apply new JWT secret.
Still in Supabase, navigate to SQL Editor and paste the following SQL code into the command window and select Run:
-- Create the tableCREATE TABLE todos ( id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, task TEXT NOT NULL, user_id TEXT NOT NULL, completed_state BOOLEAN DEFAULT FALSE);
-- Insert some sample data into the tableINSERT INTO todos (task, user_id, completed_state)VALUES ('Finish writing blog post', 'kp_123', FALSE), ('Buy groceries', 'kp_456', TRUE), ('Work out at the gym', 'kp_789', FALSE), ('Read 10 pages of a book', 'kp_101', TRUE);
-- Enable row-level securityALTER TABLE todos ENABLE ROW LEVEL SECURITY;
This command creates a todos
table with the following columns:
task
: with a type of text.user_id
: with a type of text.completed_state
: with a type of boolean. We use boolean because the completed state of each item is either true or false.It also inserts four sample todo items. We will need this for later.
Run the following command in your terminal window to start a new project with Next.js and Supabase. Follow the on-screen instructions.
npx create-next-app -e with-supabase
Set a name for your project (e.g: kinde-with-supabase
)
Go into the project directory:
cd kinde-with-supabase
Install the Kinde dependency with this command:
npm install @kinde-oss/kinde-auth-nextjs jsonwebtokennpm install --save-dev @types/jsonwebtoken
Create the Kinde auth endpoint in this path app/api/auth/[kindeAuth]:
mkdir -p app/api/auth/[kindeAuth]touch app/api/auth/[kindeAuth]/route.js
Open the newly created route.js
file, enter the following code, and save the file:
import { handleAuth } from "@kinde-oss/kinde-auth-nextjs/server"export const GET = handleAuth()
Create an environment variables file .env.local
by typing:
touch .env.local
Add the following to your .env.local
file:
KINDE_CLIENT_ID=<kinde_client_id>KINDE_CLIENT_SECRET=<kinde_client_secret>KINDE_ISSUER_URL=https://<your_kinde_business>.kinde.comKINDE_SITE_URL=http://localhost:3000KINDE_POST_LOGOUT_REDIRECT_URL=http://localhost:3000KINDE_POST_LOGIN_REDIRECT_URL=http://localhost:3000/dashboardNEXT_PUBLIC_SUPABASE_URL=https://<supabase_project_id>.supabase.coNEXT_PUBLIC_SUPABASE_ANON_KEY=<supabase_anon_public_key>
Open the components/header-auth.tsx
file and replace the entire content with the following. This code will replace Supabase Auth functions with Kinde Auth.
import { RegisterLink, LoginLink, LogoutLink,} from "@kinde-oss/kinde-auth-nextjs/components"import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server"
import { signOutAction } from "@/app/actions"import { hasEnvVars } from "@/utils/supabase/check-env-vars"import { Badge } from "./ui/badge"import { Button } from "./ui/button"
export default async function AuthButton() { const { getUser } = getKindeServerSession() const user = await getUser()
if (!hasEnvVars) { return ( <> <div className='flex gap-4 items-center'> <div> <Badge variant={"default"} className='font-normal pointer-events-none' > Please update .env.local file with anon key and url </Badge> </div> <div className='flex gap-2'> <Button asChild size='sm' variant={"outline"} disabled className='opacity-75 cursor-none pointer-events-none' > <LoginLink className='btn btn-ghost sign-in-btn'> Sign in </LoginLink> </Button> <Button asChild size='sm' variant={"default"} disabled className='opacity-75 cursor-none pointer-events-none' > <RegisterLink className='btn btn-dark'>Sign up</RegisterLink> </Button> </div> </div> </> ) } return user ? ( <div className='flex items-center gap-4'> Hey, {user.given_name}! <form action={signOutAction}> <Button type='submit' variant={"outline"}> <LogoutLink className='text-subtle'>Log out</LogoutLink> </Button> </form> </div> ) : ( <div className='flex gap-2'> <Button asChild size='sm' variant={"outline"}> <LoginLink className='btn btn-ghost sign-in-btn'>Sign in</LoginLink> </Button> <Button asChild size='sm' variant={"default"}> <RegisterLink className='btn btn-dark'>Sign up</RegisterLink> </Button> </div> )}
Replace utils/supabase/server.ts
with the following code:
import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server"import { createServerClient } from "@supabase/ssr"import { cookies } from "next/headers"import jwt from "jsonwebtoken"
export const createClient = async () => { const cookieStore = await cookies()
const { getIdToken } = getKindeServerSession() const idToken = await getIdToken()
let token: string if (idToken) { token = jwt.sign(idToken, process.env.KINDE_CLIENT_SECRET!) } else { token = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! }
return createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { global: { headers: { Authorization: `Bearer ${token}`, }, }, cookies: { getAll() { return cookieStore.getAll() }, setAll(cookiesToSet) { try { cookiesToSet.forEach(({ name, value, options }) => { cookieStore.set(name, value, options) }) } catch (error) { // The `set` method was called from a Server Component. // This can be ignored if you have middleware refreshing // user sessions. } }, }, } )}
Start the development environment by typing the following in your terminal:
npm run dev
Go to http://localhost:3000 and sign up/sign in to your Kinde application to test the integration.
In your project directory, on a new terminal window, create a todo
directory with a page.tsx
file inside it by running the following command. This file will serve as your to-do page.
mkdir app/todotouch app/todo/page.tsx
Add the following code to page.tsx
which is a basic to-do list table:
import { createClient } from "@/utils/supabase/server"
export default async function TodoList() { const supabase = await createClient() const { data: todos } = await supabase.from("todos").select()
return ( <div style={{ display: "flex", justifyContent: "center", marginTop: "50px" }} > <table style={{ width: "100%", 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> {todos?.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.completed_state)} </td> </tr> ))} </tbody> </table> </div> )}
Go to http://localhost:3000/todo to preview the page. You won’t see any to-do items because we haven’t set any permissions yet.
To ensure the user is redirected to the to-do page after they sign in, update the KINDE_POST_LOGIN_REDIRECT_URL
in your .env.local
file to:
KINDE_POST_LOGIN_REDIRECT_URL=http://localhost:3000/todo
Now that your to-do app is built, proceed to update your database policies to display to-do items for the correct users.
In Supabase, go to SQL Editor.
Select the plus “+” icon, and then Create a new snippet.
Paste the following SQL code into the command window and select Run:
create or replace function get_user_id()returns textlanguage sql stableas $$ select nullif(current_setting('request.jwt.claims', true)::json->>'sub', '')::text;$$;
This function extracts the sub
field from the JSON Web Token (JWT) sent by Kinde auth. The sub
field contains the Kinde user ID, which will allow filtering to-do items specific to each user.
Create another new snippet and run the following SQL statement to enable the row-level policy.
create policy "users can read only their todos"on public.todosfor select to publicusing (get_user_id() = user_id);
Select Save policy.
Your database policy is now set up to ensure that only the authenticated user’s to-do items are displayed. In the next step, you’ll update your table rows to include user IDs.
Open Kinde and select Users to view the user list.
Locate the user registered with your Supabase to-do app (e.g: Tamal Chowdhury) and select their name to view their profile.
Copy the unique Kinde profile ID displayed on their profile page.
Go to Table Editor in your Supabase project, and open the todos table.
Locate the rows corresponding to your to-do items and update the user_id
column with the Kinde ID you copied. Save your changes.
In the Kinde project, log in with the same user you signed up with. Verify that the to-do items linked to this user are now visible on the to-do page of your app.
This step links the Kinde user ID to specific to-do items, enabling the app to filter and display tasks based on the authenticated users.
You’ve successfully built a secure and personalized to-do app using Kinde for authentication and Supabase for database management.
With this foundation, your app is now equipped to manage user authentication and personalized data securely. You can extend this project further by adding features like task creation, deletion, updates, or even real-time collaboration.