This guide outlines the Kinde-specific setup for Convex, following a flow similar to the Convex & Clerk integration but focusing on how to integrate Kinde with Convex.
It addresses many of the questions raised by the Kinde developer community, which can be found here: Kinde Community - Integrating Convex with Kinde
The tutorial provides clear, actionable steps for integrating Kinde authentication with Convex while adhering to best practices.
Kinde is an authentication platform that enables passwordless user sign-ins via methods such as magic links, SMS codes, or authenticator apps. It also supports multi-factor authentication (MFA) for added security, enterprise-level single sign-on (SSO) with SAML, and offers robust user management tools for businesses.
Example: Convex Authentication with Kinde
If you're using Next.js see the Next.js setup guide for Convex.
Get started
This guide assumes you already have a working Next.js app with Convex. If not follow the Convex Next.js Quickstart first. Then:
- Sign up for Kinde
Sign up for a free Kinde account at kinde.com/register.
- Create a business in Kinde
Enter the name of your business or application.
- Select your tech stack
Select the tech stack or tools that you would be using to build this application.
- Select authentication methods
Choose how you want your users to sign in.
- Connect your app to Kinde
Connect your Next.js application to Kinde.
- Create the auth config
Copy your KINDE_ISSUER_URL
from your .env.local
file. Move into the convex
folder and create a new file auth.config.ts
with the server-side configuration for validating access tokens.
Paste in the KINDE_ISSUER_URL and set the applicationID
to "convex"
(the value and the "aud"
Claims field).
const authConfig = {
providers: [
{
domain: process.env.KINDE_ISSUER_URL, // Example: https://barswype.kinde.com
applicationID: "convex",
},
]
};
export default authConfig;
- Set up the Convex & Kinde Webhook
In Kinde Dashboard, go to Settings > Webhooks > Click Add Webhook > Name the webhook and paste your Convex endpoint URL, e.g., https://<your-convex-app>.convex.site/kinde
.
Select events to trigger, such as user.created
and user.deleted
.
Now back to your code. Open your convex/
folder and create a new file http.ts
, then copy and paste this code:
import { httpRouter } from "convex/server";
import { internal } from "./_generated/api";
import { httpAction } from "./_generated/server";
import { jwtVerify, createRemoteJWKSet } from "jose";
type KindeEventData = {
user: {
id: string;
email: string;
first_name?: string;
last_name?: string | null;
is_password_reset_requested: boolean;
is_suspended: boolean;
organizations: {
code: string;
permissions: string | null;
roles: string | null;
}[];
phone?: string | null;
username?: string | null;
image_url?: string | null;
};
};
type KindeEvent = {
type: string;
data: KindeEventData;
};
const http = httpRouter();
const handleKindeWebhook = httpAction(async (ctx, request) => {
const event = await validateKindeRequest(request);
if (!event) {
return new Response("Invalid request", { status: 400 });
}
switch (event.type) {
case "user.created":
await ctx.runMutation(internal.users.createUserKinde, {
kindeId: event.data.user.id,
email: event.data.user.email,
username: event.data.user.first_name || ""
});
break;
{/**
case "user.updated":
const existingUserOnUpdate = await ctx.runQuery(
internal.users.getUserKinde,
{ kindeId: event.data.user.id }
);
if (existingUserOnUpdate) {
await ctx.runMutation(internal.users.updateUserKinde, {
kindeId: event.data.user.id,
email: event.data.user.email,
username: event.data.user.first_name || ""
});
} else {
console.warn(
`No user found to update with kindeId ${event.data.user.id}.`
);
}
break;
*/}
case "user.deleted":
const userToDelete = await ctx.runQuery(internal.users.getUserKinde, {
kindeId: event.data.user.id,
});
if (userToDelete) {
await ctx.runMutation(internal.users.deleteUserKinde, {
kindeId: event.data.user.id,
});
} else {
console.warn(
`No user found to delete with kindeId ${event.data.user.id}.`
);
}
break;
default:
console.warn(`Unhandled event type: ${event.type}`);
}
return new Response(null, { status: 200 });
});
// ===== JWT Validation =====
async function validateKindeRequest(request: Request): Promise<KindeEvent | null> {
try {
if (request.headers.get("content-type") !== "application/jwt") {
console.error("Invalid Content-Type. Expected application/jwt");
return null;
}
const token = await request.text(); // JWT is sent as raw text in the body.
const JWKS_URL = `${process.env.KINDE_ISSUER_URL}/.well-known/jwks.json`;
const JWKS = createRemoteJWKSet(new URL(JWKS_URL));
const { payload } = await jwtVerify(token, JWKS);
// Ensure payload contains the expected properties
if (
typeof payload === "object" &&
payload !== null &&
"type" in payload &&
"data" in payload
) {
return {
type: payload.type as string,
data: payload.data as KindeEventData,
};
} else {
console.error("Payload does not match the expected structure");
return null;
}
} catch (error) {
console.error("JWT verification failed", error);
return null;
}
}
http.route({
path: "/kinde",
method: "POST",
handler: handleKindeWebhook,
});
export default http;
For a detailed guide on setting up webhooks between Kinde and Convex, refer to this post.
- Deploy your changes
Run npx convex dev
to automatically sync your configuration to your backend.
npx convex dev
- Install Kinde
In a new terminal window, install the Kinde Next.js library
npm install @kinde-oss/kinde-auth-nextjs
- Copy your Kinde environment variables
On the Kinde dashboard, click view details on your app.
Scroll down and copy your Client ID and Client secret
- Set up Kinde auth route handlers
Create the following file app/api/auth/[kindeAuth]/route.ts
inside your Next.js project. Inside the file route.ts
copy and paste this code:
import {handleAuth} from "@kinde-oss/kinde-auth-nextjs/server";
export const GET = handleAuth();
This will handle Kinde Auth endpoints in your Next.js app.
Important! The Kinde SDK relies on this file exisiting in this location as specified above.
- Configure a new provider for Convex and Kinde integration
Create a providers
folder in your root directory and add a new file ConvexKindeProvider.tsx
. This provider will integrate Convex with Kinde and wrap your entire app.
Inside ConvexKindeProvider.tsx
, wrap the ConvexProvider
with KindeProvider
, and use useKindeAuth
to fetch the authentication token and pass it to Convex.
Paste the domain
, clientId
and redirectUri
as props to KindeProvider
.
"use client";
import { ReactNode, useEffect } from "react";
import { KindeProvider, useKindeAuth } from "@kinde-oss/kinde-auth-nextjs";
import { ConvexProvider, ConvexReactClient, AuthTokenFetcher } from "convex/react";
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL as string);
const ConvexKindeProvider = ({ children }: { children: ReactNode }) => {
const { getToken } = useKindeAuth();
useEffect(() => {
const fetchToken: AuthTokenFetcher = async () => {
const token = await getToken();
return token || null;
};
if (typeof getToken === "function") {
convex.setAuth(fetchToken);
}
}, [getToken]);
return (
<KindeProvider
domain={process.env.NEXT_PUBLIC_KINDE_DOMAIN as string}
clientId={process.env.NEXT_PUBLIC_KINDE_CLIENT_ID as string}
redirectUri={process.env.NEXT_PUBLIC_KINDE_REDIRECT_URI as string}
>
<ConvexProvider client={convex}>{children}</ConvexProvider>
</KindeProvider>
);
};
export default ConvexKindeProvider;
Import your configured ConvexKindeProvider.tsx
to your main layout.tsx
file.
import type { Metadata } from "next";
import "./globals.css";
import ConvexKindeProvider from "@/providers/ConvexKindeProvider";
export const metadata: Metadata = {
title: "Create Next App",
description: "Kinde and Convex Demo",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<ConvexKindeProvider>
<html lang="en">
<body>
{children}
</body>
</html>
</ConvexKindeProvider>
);
};
- Show UI based on authentication state
You can control which UI is shown when the user is signed in or signed out with the provided components from "convex/react"
and "@kinde-oss/kinde-auth-nextjs"
.
To get started create a shell that will let the user sign in and sign out.
Because the DisplayContent
component is a chold of Authenticated
, within it and any of its children authentication is guaranteed and Convex queries can require it.
"use client";
import { useKindeBrowserClient } from "@kinde-oss/kinde-auth-nextjs";
import {
RegisterLink,
LoginLink,
LogoutLink,
} from "@kinde-oss/kinde-auth-nextjs/components";
import { Authenticated, Unauthenticated, useQuery } from "convex/react";
import { api } from "@/convex/_generated/api";
function App() {
const { isAuthenticated, getUser } = useKindeBrowserClient();
const user = getUser();
return (
<main>
<Unauthenticated>
<LoginLink postLoginRedirectURL="/dashboard">Sign in</LoginLink>
<RegisterLink postLoginRedirectURL="/welcome">Sign up</RegisterLink>
</Unauthenticated>
<Authenticated>
{isAuthenticated && user ? (
<div>
<p>Name: {user.given_name} {user.family_name}</p>
<p>Email: {user.email}</p>
<p>Phone: {user.phone_number}</p>
</div>
) : null}
<DisplayContent />
<LogoutLink>Log out</LogoutLink>
</Authenticated>
</main>
);
}
function DisplayContent() {
const { user } = useKindeBrowserClient();
const files = useQuery(api.files.getForCurrentUser, {
kindeId: user?.id,
});
return <div>Authenticated content: {files?.length}</div>;
}
export default App;
- Use authentication state in your Convex functions
If the client is authenticated, you can access the information stored in the JWT sent by Kinde via ctx.auth.getUserIdentity
.
If the client isn't authenticated, ctx.auth.getUserIdentity
will return null
.
Make sure that the component calling this query is a child of Authenticated
from "convex/react"
, otherwise it will throw on page load.
import { query } from "./_generated/server";
export const getForCurrentUser = query({
args: { kindeId: v.string() },
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (identity === null) {
throw new Error("Not authenticated");
}
const files = await ctx.db
.query("files")
.filter((q) => q.eq(q.field("kindeId"), args.kindeId))
.collect();
if (!files) {
throw new Error("No files found for this user");
}
return files;
},
});
Login and logout Flows
Now that you have everything set up, you can use the LoginLink
component to create a login flow for your app.
If you would prefer to configure custom sign-in/sign-up forms for your app, see this post.
import {LoginLink} from "@kinde-oss/kinde-auth-nextjs/components";
<LoginLink>Sign in</LoginLink>
To enable a logout flow you can use the LogoutLink
component to enable a user seamlessly logout of your app.
import {LogoutLink} from "@kinde-oss/kinde-auth-nextjs/components";
<LogoutLink>Log out</LogoutLink>
Logged-in and logged-out views
Use the useConvexAuth()
hook instead of Kinde's useKindeBrowserClient
hook when you need to check whether the user is logged in or not. the useConvexAuth
hook makes sure that the browser has fetched the auth token needed to make authenticated requests to your Convex backend, and that the Convex backend has validated it:
import { useConvexAuth } from "convex/react";
function App() {
const { isLoading, isAuthenticated } = useConvexAuth();
return (
<div className="App">
{isAuthenticated ? "Logged in" : "Logged out or still loading"}
</div>
);
}
User information in functions
See Auth in Functions to learn about how to access information about the authenticated user in your queries, mutations and actions.
See Storing Users in the Convex Database to learn about how to store user information in the Convex database.
User information in Next.js
You can access information about the authenticated user like their name and email address from Kinde's useKindeBrowserClient
or getKindeServerSession
hooks. See the User information object for the list of available fields:
"use client";
import { useKindeBrowserClient } from "@kinde-oss/kinde-auth-nextjs";
export default function Hero() {
const { user } = useKindeBrowserClient();
return <span>Logged in as {user?.given_name} {user?.family_name}</span>;
};
Configuring dev and prod instances
To configure a different Kinde instance between your Convex development and production deployments you can use environment variables configured on the Convex dashboard.
Configuring the backend
Kinde's default configurations are set to a production environment. To use a custom domain instead of the issued <your_app>.kinde.com
domain, see this guide.
Development configuration
Open the settings for your dev deployment on the Convex dashboard and add all the variables from your .env.local
there:
Production configuration
Similarly on the convex dashboard switch to your production deployment in the left side menu and set the variables from your .env.local
there.
Now switch to the new configuration by running npx convex deploy
.
npx convex deploy
Deploying your Next.js App
Set the environment variable in your production environment depending on your hosting platform. See Hosting.
Debugging authentication
If a user goes through the Kinde register or login flow successfully, and after being saved to your Convex database and redirected back to your page useConvexAuth
gives isAuthenticated: false
, it's possible that your backend isn't correctly configured.
The auth.config.ts
files in your convex/
directory contains a list of configured authentication providers. You must run npx convex dev
or npx convex deploy
after adding a new provider to sync the configuration to your backend.
For more thorough debugging steps, see Debugging Authentication.
Under the hood
The authentication flow looks like this under the hood:
- The user clicks a register or login button.
- The user is redirected to a hosted Kinde page where they sign up or log in via whatever method you configure in Kinde.
- After a successful sign up or login, their details are sent through a webhook and stored securely in Convex, after which they are redirected immediately back to your page, or a different page which you configure via the Kinde
postLoginRedirectURL
prop. - The
KindeProvider
now knows that the user is authenticated. - The
useKindeAuth
andAuthTokenFetcher
fetches an auth token from Kinde. - Then the react
useEffect
hook sets this token to asetAuth
instance of Convex. - The
ConvexProvider
then passes this token down to your Convex backend to validate. - Your Convex backend retrieves the domain, clientId and redirectUri from Kinde to check that the token's signature is valid.
- The
ConvexProvider
is notified of successful authentication, and now your entire application knows that the user is authenticated with Convex.useConvexAuth
returnisAuthenticated: true
and theAuthenticated
component renders its children.
The configuration in the ConvexKindeProvider.tsx
file takes care of refetching the token when needed to make sure the user stays authenticated with your backend.
Top comments (0)