Kinde is an authentication and identity management platform that helps developers implement secure user authentication, role-based access control (RBAC), and social logins (e.g., Google, Facebook, GitHub).
It offers passwordless sign-ins, SSO, and webhooks for real-time user data syncing, making it easy to manage user access and permissions in web applications.
Whereas Convex provides you with a fully featured backend with cloud functions, database, scheduling, and a sync engine that keeps your frontend and backend up to date in real-time.
What You'll Learn in this Tutorial
- Setting up Kinde in your Next.js app: Learn how to integrate Kinde for user authentication.
- Supporting multiple authentication providers: Integrate social logins like Google, Facebook and GitHub.
- Creating protected routes with middleware: Restrict access to unauthenticated routes.
- User Authentication Workflow: Manage login and logout seamlessly.
- Configuring Convex for user data management: Set up Convex to store and manage user data.
- Using webhooks to sync authentication events: Sync user authentication and data events from Kinde to Convex.
- Introduction to RBAC and Setup: How to restrict access to functionalities based on roles, enhancing security.
Prerequisites
- Node.js installed on your computer.
- Knowledge of TypeScript React, and Next.js.
- A free account on Kinde to manage authentication.
- A free account on Convex for user data management.
Get started
Setting Up the Starter Repository
To simplify the process, we've created a starter repository that includes:
- A pre-configured Next.js project with TypeScript and TailwindCSS.
- Shadcn UI components already set up.
- Basic app router folder structure.
- Essential libraries pre-installed.
Clone the Starter Project
First, clone the starter repository:
git clone https://github.com/sholajegede/nextjs-shadcn-starter.git
cd nextjs-shadcn-starter
Install the dependencies:
npm install
# or
yarn install
# or
bun install
Start the development server:
npm run dev
# or
yarn dev
# or
bun run dev
Navigate to http://localhost:3000
in your browser. You should see a simple interface that looks like this:
Steps to Get Started with Kinde in your Next.js Project
1. Visit Kinde's Website
- Go to the Kinde website via the link in the tutorial.
- If you don’t have an account, click "Start for free" to create one.
- Log in to access the dashboard.
2. Create a New Project
- In the Kinde dashboard, click on "Add Business" (projects are referred to as businesses in Kinde's terminology).
- Fill in the project details (e.g., name, region, number of employees).
- Save the project and access the onboarding flow.
3. Configure the Project
- Select the option to integrate Kinde into an existing project.
- Choose the appropriate SDK (Next.js in this case).
- Select authentication methods "Google and Facebook".
4. Access the Documentation
- Navigate to the documentation from the dashboard to find detailed setup instructions for your SDK.
- For this tutorial, we would be using Next.js, see the Next App Router setup guide.
- If you would like to use Next.js Pages Router, see the setup guide.
5. Install Kinde SDK
- Use the provided installation command:
npm i @kinde-oss/kinde-auth-nextjs
- Restart your development server if necessary.
6. Integrate Kinde into the Codebase
- Configure environment variables:
KINDE_CLIENT_ID=<your_kinde_client_id>
KINDE_CLIENT_SECRET=<your_kinde_client_secret>
KINDE_ISSUER_URL=https://<your_kinde_subdomain>.kinde.com
KINDE_SITE_URL=http://localhost:3000
KINDE_POST_LOGOUT_REDIRECT_URL=http://localhost:3000
KINDE_POST_LOGIN_REDIRECT_URL=http://localhost:3000/dashboard
- Replace the information in the example with your own information. (i) In Kinde, go to your Dashboard > Applications > [Your app > View details (ii) Scroll down to "app keys", there you would see your Domain, Client ID, and Client Secret
7. 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 put 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.
Steps to Get Started with Convex in your Next.js Project
1. Install Convex
To get started, install the convex package which provides a convenient interface for working with Convex from a Next.js app:
npm install convex
2. Set up a Convex dev deployment
Next, run npx convex dev
. This will prompt you to log in with GitHub, create a project, and save your production and deployment URLs.
It will also create a convex/
folder for you to write your backend API functions in. The dev
command will then continue running to sync your functions with your dev deployment in the cloud.
As part of this setup, Convex automatically sets some environment variables for your deployment, such as:
-
CONVEX_DEPLOYMENT
: Specifies the deployment name (e.g.,dev:necessary-jackal-122
). -
NEXT_PUBLIC_CONVEX_URL
: The public-facing URL for your Convex backend (e.g.,https://necessary-jackal-122.convex.cloud
).
npx convex dev
For webhooks, which we will cover later in the tutorial, you'll also need to add:
NEXT_PUBLIC_CONVEX_HTTP_URL
: The HTTP endpoint for handling webhooks (e.g., https://necessary-jackal-122.convex.site
).
3. Create a Schema for your project
The schema defines the structure of your database tables and the fields each table contains. To do this, go into your convex
folder and create a schema.ts
file. In it, copy and paste this code:
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
users: defineTable({
email: v.string(),
kindeId: v.string(),
username: v.optional(v.string()),
imageUrl: v.optional(v.string()),
imageStorageId: v.optional(v.id("_storage")),
notificationType: v.optional(v.string()),
communication_updates: v.optional(v.boolean()), // true or false : default = true
marketing_updates: v.optional(v.boolean()), // true or false
social_updates: v.optional(v.boolean()), // true or false
security_updates: v.optional(v.boolean()), // true or false : default = true
stripeId: v.optional(v.string()),
}),
});
-
email
andkindeId
: Required fields storing user's email and unique identifier which would be gotten from Kinde after authentication. -
username
andimageUrl
: Optional fields for storing the first name and profile image/picture from Kinde after authentication. -
imageStorageId
: Links to an uploaded image stored in Convex's storage system. This would be stored when a user updates their profile image in the application. -
notificationType
and update preferences (communication_updates
,marketing_updates
, etc.): Boolean fields that let users customize their notification preferences. -
stripeId
: Optionally stores the user's Stripe ID for payment integration.
4. Create the auth config
In the convex
folder create a new file auth.config.ts
with the server-side configuration for validating access tokens.
Paste in your KINDE_ISSUER_URL
from your .env.local
file and set applicationID to convex
.
const authConfig = {
providers: [
{
domain: process.env.KINDE_ISSUER_URL,
applicationID: "convex",
},
]
};
export default authConfig;
5. Deploy your changes
Run npx convex dev
to automatically sync your configuration to your backend.
npx convex dev
6. Configure your ConvexKindeProvider
To integrate Convex and Kinde authentication in your app, you'll need a custom provider component. Follow these steps to create and configure it:
- Create the
providers
folder: At the root of your project, create a folder namedproviders
. This will hold custom provider components. - Create the
ConvexKindeProvider.tsx
file: Inside theproviders
folder, create a new file namedConvexKindeProvider.tsx
. - Paste the code: Copy and paste the provided code into this file.
"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;
What the Code Does
The ConvexKindeProvider
component integrates Kinde's authentication with Convex's data synchronization, ensuring authenticated user sessions are securely managed. Here's a breakdown of its functionality:
- Import Required Libraries:
-
KindeProvider
anduseKindeAuth
: Handle authentication using Kinde. -
ConvexProvider
andConvexReactClient
: Manage Convex's real-time database and syncing capabilities.
-
- Create a Convex Client:
- The
ConvexReactClient
is initialized with your Convex app's URL (NEXT_PUBLIC_CONVEX_URL
).
- The
- Fetch and Set Auth Token:
- A
fetchToken
function is defined using Kinde'sgetToken()
to retrieve the user's authentication token. - The token is passed to Convex's
setAuth
method to secure API requests.
- A
- Use Effect Hook:
- Ensures the token-fetching logic runs whenever
getToken
changes.
- Ensures the token-fetching logic runs whenever
- Render Children with Providers:
- Wraps your app's components (
children
) in both theKindeProvider
andConvexProvider
, enabling authentication and database access across your app.
- Wraps your app's components (
Key Features of This Setup
- Secure Authentication:
- Retrieves and uses Kinde's authentication token to authenticate users with Convex.
- Environment Variables:
- Uses environment variables (
NEXT_PUBLIC_KINDE_DOMAIN
,NEXT_PUBLIC_KINDE_CLIENT_ID
, etc.) to configure Kinde and Convex securely.
- Uses environment variables (
- Reusable Component:
- Encapsulates all authentication and data sync logic into a single component, simplifying integration across your app.
By including this provider in your app, you ensure seamless integration between Kinde's authentication and Convex's real-time data capabilities.
7. Pass your ConvexKindeProvider
to your root layout.tsx
To ensure Kinde authentication and Convex database synchronization are accessible across your entire application, you need to wrap your root layout with the ConvexKindeProvider
. This allows all child components to inherit these features without additional setup.
Here's how you can do it:
- Open your layout.tsx file.
- Wrap your content inside the
ConvexKindeProvider
as shown below:
import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider";
import ConvexKindeProvider from "@/providers/ConvexKindeProvider";
import { TooltipProvider } from "@/components/ui/tooltip";
const geistSans = localFont({
src: "../fonts/GeistVF.woff",
variable: "--font-geist-sans",
weight: "100 900",
});
const geistMono = localFont({
src: "../fonts/GeistMonoVF.woff",
variable: "--font-geist-mono",
weight: "100 900",
});
export const calSans = localFont({
src: "../fonts/CalSans-SemiBold.ttf",
variable: "--font-calsans",
})
export const metadata: Metadata = {
title: "File Share App",
description: "The fastest and most secure way to share your files.",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<ConvexKindeProvider>
<html lang="en" suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<TooltipProvider>
{children}
</TooltipProvider>
</ThemeProvider>
</body>
</html>
</ConvexKindeProvider>
);
};
Why This Matters
- Global Authentication: Wrapping the root layout ensures all components in your app can access authenticated sessions.
- Real-Time Data: Convex's real-time updates are now available throughout your app.
- Simplified Setup: Centralizing this logic at the root reduces repetitive code and ensures consistent behavior.
By following this step, your app is fully equipped to handle authentication and real-time data syncing seamlessly.
Middleware Setup to Protect Routes
To protect your app’s routes and handle authentication, you’ll use middleware to redirect unauthenticated users and manage their access.
Step 1: Create the Middleware File
In the root directory (outside the app
directory), create a file named middleware.ts
.
Step 2: Define the Middleware
Here’s the complete code to define and configure the middleware:
import { withAuth } from "@kinde-oss/kinde-auth-nextjs/middleware";
export default withAuth({
loginPage: "/api/auth/login", // Redirect unauthenticated users to this page
isReturnToCurrentPage: true, // Redirect users back to their current page after logging in
});
export const config = {
matcher: [
/*
* Protect all routes except:
* - api (API routes)
* - about, privacypolicy, termsofservice (public pages)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico, sitemap.xml, robots.txt (metadata files)
*/
'/((?!api|about|privacypolicy|termsofservice|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|$).*)',
],
};
What the Middleware Does
- Restricts Access: Blocks access to protected routes for unauthenticated users. Routes like
api
,about
, and static files remain accessible. - Handles Redirection: Redirects unauthenticated users to the specified
loginPage
(/api/auth/login
in this case). - Returns Users to Their Original Page: With
isReturnToCurrentPage: true
, users are brought back to the page they were trying to access after successfully logging in. - Flexible Matcher Configuration: The
matcher
property defines which routes should bypass the middleware (e.g., API and static resources). This ensures only relevant routes are protected.
Logic Overview
- Unauthenticated User: When a user accesses a restricted route, the middleware intercepts the request and redirects them to the login page.
- Authenticated User: If the user is already authenticated, the middleware allows access to the requested route without interruption.
- Post-Login Behavior: After logging in, users are automatically redirected to the page they originally tried to access.
This middleware ensures that only authenticated users can access sensitive parts of your app, providing a seamless login experience with smart redirection.
Sync Authenticated Users to Database Using Webhooks
- Webhooks are HTTP callbacks triggered by specific events in an application (e.g.,
user.created
in Kinde). - How Convex Uses Webhooks: Convex exposes HTTP endpoints through
httpRouter
. These endpoints receive webhook payloads, validate them, and trigger internal processes like mutations or queries. - Convex’s HTTPS URL: Each
httpRouter
route (like/kinde
) generates a unique HTTPS endpoint, e.g.,https://<your-convex-app>.convex.site/kinde
. You provide this URL to Kinde to send webhook events.
Setting up Webhook Configuration in Kinde
To create a Webhook in Kinde, go to your Kinde Dashboard and click on Settings:
Next, scroll down till your see Webhooks and then click on it:
- Here I already have a Webhook setup, but if this is your first time, simply click on Add Webhook:
- Next, give your webhook a name and description (optional) and then copy and paste your
NEXT_PUBLIC_CONVEX_HTTP_URL
into the Endpoint URL. Do not forget to add/kinde
:
- Finally, select the events that you will like to trigger. For this tutorial, we will be selecting the
user.created
anduser.deleted
events.
There are a lot more that you can choose from:
Now unlike Clerk which uses a Webhook Secret to verify webhooks, Kinde automatically signs webhook payloads with a JWT and I will be showing you how that works.
You can also use tools like webhook.site to test your webhook and see the type of payload it sends.
When testing with your Convex http url, ensure you check Convex's logs for received events.
Step-by-Step Convex Integration with Kinde Webhooks
When integrating webhooks and JWT validation in serverless environments like Convex, it's essential to use the right libraries for handling JWTs. Convex functions can't use Node.js built-in modules like jsonwebtoken
because they rely on features like crypto and stream, which are unavailable in serverless setups.
Why Use jose
Instead of jsonwebtoken
?
- Compatibility with Serverless Environments:
jsonwebtoken
relies on Node.js modules not supported in serverless environments, whilejose
is designed for such platforms and works without native Node.js dependencies. - Browser Compatibility: Unlike
jsonwebtoken
, which is server-side only,jose
can run in both the browser and serverless platforms, making it a more versatile, lightweight choice.
Issue with jsonwebtoken
for Kinde Webhooks
In the tutorial on integrating Kinde with Convex webhooks here, the jsonwebtoken
package is suggested for JWT verification. However, this method has an issue when using Convex, as Convex doesn't support Node.js built-in modules. Moreover, Kinde doesn't send the authorization token in the Authorization
header, which complicates the verification process. As a result, we need to adopt a different approach, using content-type
instead of the standard Authorization
header.
Here’s how we can implement this with jose
for JWT verification:
Step 1: Install jose
:
- First, install the
jose
library, which will be used to validate the JWTs.
npm install jose
Step 2: Import the Necessary Libraries:
import { httpRouter } from "convex/server";
import { internal } from "./_generated/api";
import { httpAction } from "./_generated/server";
import { jwtVerify, createRemoteJWKSet } from "jose";
-
httpRouter
: This is from Convex, and it's used to set up HTTP routes for handling HTTP requests within Convex functions. It will help define how your serverless app handles requests, in this case, webhook events. -
internal
: This refers to your Convex internal API, which allows you to run queries and mutations. You'll use this to interact with your Convex database (e.g., adding, updating or deleting users). -
httpAction
: This is a Convex function wrapper used to define serverless functions that handle HTTP requests. It encapsulates your logic for responding to the requests. -
jwtVerify
andcreateRemoteJWKSet
: These are functions from thejose
library.jwtVerify
is used to verify the validity of a JWT (JSON Web Token) against a set of keys, whilecreateRemoteJWKSet
allows you to retrieve a set of public keys from a URL, which can be used to verify the JWT.
Step 3: Define Types for Kinde Webhook Data
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;
};
Here, we define the types for the event data structure that comes from the Kinde webhook.
-
KindeEventData
: Represents the actual data sent by Kinde, including user details likeid
,email
,first_name
,last_name
, etc. -
KindeEvent
: The overall structure of the webhook event, which includes atype
(e.g.,user.created
,user.deleted
) and the associateddata
(which is of typeKindeEventData
).
Step 4: Setup the HTTP Router
const http = httpRouter();
This line initializes the HTTP router. This is where you define the routes for handling specific HTTP requests. You can think of it as setting up an endpoint where Kinde can send its webhook events.
Step 5: Define the Webhook Handler
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.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 });
});
This function is responsible for handling Kinde webhook requests. It:
- Validates the incoming request using the
validateKindeRequest
function. - If the validation fails, it responds with a
400
status code (Bad Request). - If the request is valid, it checks the event type (e.g.,
user.created
,user.deleted
) and performs appropriate actions:- For a
user.created
event, it calls thecreateUserKinde
mutation to add a new user to the Convex database. - For a
user.deleted
event, it attempts to fetch the user from the database and delete them if found.
- For a
- If the event type is unhandled, it logs a warning.
Step 6: Create the JWT Validation Function
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);
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;
}
}
This function is responsible for validating the JWT in the incoming request. Here’s what it does:
- Content-Type Check: It ensures the
Content-Type
isapplication/jwt
. If it's not, the function logs an error and returnsnull
. - Extract JWT: The JWT token is extracted directly from the body of the request using
request.text()
. This is different from many standard setups where the JWT is typically passed in theAuthorization
header. - Get JWK Set: It fetches the JSON Web Key Set (JWKS) from Kinde’s URL (
process.env.KINDE_ISSUER_URL
). The JWKS is a set of public keys used to verify the JWT’s signature. - Verify JWT: It uses
jose
'sjwtVerify
function to verify the JWT against the JWKS. If the verification is successful, it checks that the payload contains the expectedtype
anddata
fields. - Return Valid Event: If the JWT is valid, it returns the parsed event with its type and data. Otherwise, it logs the error and returns
null
.
Step 7: Register the Route
http.route({
path: "/kinde",
method: "POST",
handler: handleKindeWebhook,
});
This registers the /kinde
endpoint to handle POST
requests. When Kinde sends a webhook to this endpoint, the handleKindeWebhook
function will be executed.
Step 8: Export the Router
export default http;
Finally, the HTTP router is exported for use in your Convex application.
You will get a couple errors in your code, this is because we are yet to write out the functions to add a new user to convex after the event is called.
To do this, create a users.ts
file in your Convex folder. Then copy and paste the codes below into it:
import { ConvexError, v } from "convex/values";
import { internalMutation, internalQuery, mutation, query } from "./_generated/server";
export const createUserKinde = internalMutation({
args: {
kindeId: v.string(),
email: v.string(),
username: v.optional(v.string()),
imageUrl: v.optional(v.string()),
imageStorageId: v.optional(v.id("_storage")),
notificationType: v.optional(v.string()),
communication_updates: v.optional(v.boolean()), // true or false : default = true
marketing_updates: v.optional(v.boolean()), // true or false
social_updates: v.optional(v.boolean()), // true or false
security_updates: v.optional(v.boolean()), // true or false : default = true
stripeId: v.optional(v.string())
},
handler: async (ctx, args) => {
try {
const newUserId = await ctx.db.insert("users", {
kindeId: args.kindeId,
email: args.email,
username: args.username || "",
imageUrl: args.imageUrl,
imageStorageId: args.imageStorageId,
notificationType: "all",
communication_updates: true,
marketing_updates: false,
social_updates: false,
security_updates: true,
stripeId: args.stripeId || ""
});
const updatedUser = await ctx.db.get(newUserId);
return updatedUser;
} catch (error) {
console.error("Error creating user:", error);
throw new ConvexError("Failed to create user.");
}
}
});
export const getUserKinde = internalQuery({
args: { kindeId: v.optional(v.string()) },
handler: async (ctx, args) => {
const user = await ctx.db
.query("users")
.filter((q) => q.eq(q.field("kindeId"), args.kindeId))
.unique();
if (!user) {
throw new ConvexError("User not found");
}
return user;
},
});
export const updateUserKinde = internalMutation({
args: {
kindeId: v.string(),
imageUrl: v.optional(v.string()),
email: v.optional(v.string()),
username: v.optional(v.string()),
stripeId: v.optional(v.string())
},
handler: async (ctx, args) => {
const user = await ctx.db
.query("users")
.filter((q) => q.eq(q.field("kindeId"), args.kindeId))
.unique();
if (!user) {
throw new ConvexError("User not found");
}
const updateFields = {
...(args.kindeId !== undefined && { kindeId: args.kindeId }),
...(args.imageUrl !== undefined && { imageUrl: args.imageUrl }),
...(args.email !== undefined && { email: args.email }),
...(args.username !== undefined && { username: args.username }),
...(args.stripeId !== undefined && { stripeId: args.stripeId })
};
await ctx.db.patch(user._id, updateFields);
return user._id;
},
});
export const deleteUserKinde = internalMutation({
args: { kindeId: v.string() },
async handler(ctx, args) {
const user = await ctx.db
.query("users")
.filter((q) => q.eq(q.field("kindeId"), args.kindeId))
.unique();
if (!user) {
throw new ConvexError("User not found");
}
await ctx.db.delete(user._id);
},
});
export const updateUser = mutation({
args: {
userId: v.id("users"),
email: v.optional(v.string()),
username: v.optional(v.string()),
imageUrl: v.optional(v.string()),
imageStorageId: v.optional(v.id("_storage")),
notificationType: v.optional(v.string()),
communication_updates: v.optional(v.boolean()), // true or false : default = true
marketing_updates: v.optional(v.boolean()), // true or false
social_updates: v.optional(v.boolean()), // true or false
security_updates: v.optional(v.boolean()), // true or false : default = true
stripeId: v.optional(v.string())
},
handler: async (ctx, args) => {
const user = await ctx.db
.query("users")
.filter((q) => q.eq(q.field("_id"), args.userId))
.unique();
if (!user) {
throw new ConvexError("User not found");
}
const updateFields = {
...(args.imageUrl !== undefined && { imageUrl: args.imageUrl }),
...(args.imageStorageId !== undefined && { imageStorageId: args.imageStorageId }),
...(args.notificationType !== undefined && { notificationType: args.notificationType }),
...(args.communication_updates !== undefined && { communication_updates: args.communication_updates }),
...(args.marketing_updates !== undefined && { marketing_updates: args.marketing_updates }),
...(args.social_updates !== undefined && { social_updates: args.social_updates }),
...(args.security_updates !== undefined && { security_updates: args.security_updates }),
...(args.email !== undefined && { email: args.email }),
...(args.username !== undefined && { username: args.username }),
...(args.stripeId !== undefined && { stripeId: args.stripeId })
};
await ctx.db.patch(args.userId, updateFields);
return args.userId;
},
});
export const getUserByKindeId = query({
args: { kindeId: v.string() },
handler: async (ctx, args) => {
const user = await ctx.db
.query("users")
.filter((q) => q.eq(q.field("kindeId"), args.kindeId))
.unique();
if (!user) {
throw new ConvexError("User not found");
}
return user;
},
});
export const getUserByEmail = query({
args: { email: v.string() },
handler: async (ctx, args) => {
const user = await ctx.db
.query("users")
.filter((q) => q.eq(q.field("email"), args.email))
.unique();
if (!user) {
throw new ConvexError("User not found");
}
return user;
},
});
export const getUserByConvexId = query({
args: { userId: v.string() },
handler: async (ctx, args) => {
const user = await ctx.db
.query("users")
.filter((q) => q.eq(q.field("_id"), args.userId))
.unique();
if (!user) {
throw new ConvexError("User not found");
}
return user;
}
});
export const deleteAndUpdateImage = mutation({
args: {
userId: v.id("users"),
oldImageStorageId: v.id('_storage'),
newImageUrl: v.string(),
newImageStorageId: v.id("_storage")
},
handler: async (ctx, args) => {
await ctx.storage.delete(args.oldImageStorageId);
const user = await ctx.db
.query("users")
.filter((q) => q.eq(q.field("_id"), args.userId))
.unique();
if (!user) {
throw new ConvexError("User not found");
}
const updateProfileImage = {
...(args.newImageUrl !== undefined && { imageUrl: args.newImageUrl }),
...(args.newImageStorageId !== undefined && { imageStorageId: args.newImageStorageId })
};
await ctx.db.patch(args.userId, updateProfileImage);
},
});
export const saveNewProfileImage = mutation({
args: {
userId: v.id("users"),
newImageUrl: v.string(),
newImageStorageId: v.id("_storage")
},
handler: async (ctx, args) => {
const user = await ctx.db
.query("users")
.filter((q) => q.eq(q.field("_id"), args.userId))
.unique();
if (!user) {
throw new ConvexError("User not found");
}
const updateProfileImage = {
...(args.newImageUrl !== undefined && { imageUrl: args.newImageUrl }),
...(args.newImageStorageId !== undefined && { imageStorageId: args.newImageStorageId })
};
await ctx.db.patch(args.userId, updateProfileImage);
},
});
export const deleteUser = mutation({
args: {
userId: v.id("users"),
},
handler: async (ctx, args) => {
const user = await ctx.db.get(args.userId);
if (!user) {
throw new ConvexError("User not found");
}
return await ctx.db.delete(args.userId);
},
});
export const getUrl = mutation({
args: {
storageId: v.id("_storage"),
},
handler: async (ctx, args) => {
return await ctx.storage.getUrl(args.storageId);
},
});
export const getAllUsers = query({
handler: async (ctx) => {
return await ctx.db.query('users').order('desc').collect()
},
});
Now save and run the command:
npx convex dev
This will revalidate your enter convex codebase and push it.
Test Login and Logout Flows
To test the login and logout functionality of your application, follow these steps:
- Navigate to the
app
directory and open thepage.tsx
file. - Locate and double-click the
Hero
component file. - Replace the existing code with the snippet below.
"use client";
import { Button } from "@/components/ui/button";
import { ArrowRight, FileUp, LogIn, LogOut } from "lucide-react";
import Link from "next/link";
import {
RegisterLink,
LoginLink,
LogoutLink,
} from "@kinde-oss/kinde-auth-nextjs/components";
import { useKindeBrowserClient } from "@kinde-oss/kinde-auth-nextjs";
export async function Hero() {
const { user } = useKindeBrowserClient();
return (
<div className="relative isolate pt-14">
<div className="absolute inset-x-0 -top-40 -z-10 transform-gpu overflow-hidden blur-3xl sm:-top-80">
<div className="relative left-[calc(50%-11rem)] aspect-[1155/678] w-[36.125rem] -translate-x-1/2 rotate-[30deg] bg-gradient-to-tr from-primary to-secondary opacity-30 sm:left-[calc(50%-30rem)] sm:w-[72.1875rem]" />
</div>
<div className="py-24 sm:py-32 lg:pb-40">
<div className="mx-auto max-w-7xl px-6 lg:px-8">
<div className="mx-auto max-w-2xl text-center">
<h1 className="text-4xl font-bold tracking-tight sm:text-6xl">
Share Files Securely with Anyone, Anywhere
</h1>
<p className="mt-6 text-lg leading-8 text-muted-foreground">
The fastest and most secure way to share your files. No signup
required for basic sharing. Enterprise-grade encryption for all
your files.
</p>
{user ? (
<div className="mt-10 flex items-center justify-center gap-x-6">
<Button size="lg" variant="outline" asChild>
<Link href="/dashboard">
<FileUp className="mr-2 h-4 w-4" />
Upload Now
</Link>
</Button>
<LogoutLink>
<Button size="lg" variant="outline">
<LogOut className="mr-2 h-4 w-4" />
Sign in
</Button>
</LogoutLink>
</div>
) : (
<div className="mt-10 flex items-center justify-center gap-x-6">
<RegisterLink>
<Button size="lg">
Get Started
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</RegisterLink>
<LoginLink>
<Button size="lg" variant="outline">
<LogIn className="mr-2 h-4 w-4" />
Sign in
</Button>
</LoginLink>
</div>
)}
</div>
</div>
</div>
</div>
);
};
This code dynamically checks the user's authentication state and renders appropriate actions for logged-in and logged-out users. While testing, monitor your logs and the users
table in Convex to verify behavior.
Fetch User Data from Convex
To fetch user data from Convex into your application, follow these steps:
- Navigate to the
app
directory and locate theroot
folder. - Open the
dashboard
folder, then access thepage.tsx
file. - Replace the existing code in the file with the snippet provided below.
"use client";
import { AppSidebar } from "@/components/app-sidebar"
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"
import { Separator } from "@/components/ui/separator"
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar"
import { api } from "@/convex/_generated/api"
import { useQuery } from "convex/react"
import {useKindeBrowserClient} from "@kinde-oss/kinde-auth-nextjs";
export default function Page() {
const { user, isAuthenticated, getPermissions, getPermission } = useKindeBrowserClient();
const profile = useQuery(api.users.getUserByKindeId, {
kindeId: user?.id as string
});
return (
<SidebarProvider>
<AppSidebar />
<SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbLink href="#">
Building Your Application
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>Data Fetching {profile?.username}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
</header>
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
<div className="aspect-video rounded-xl bg-muted/50" />
<div className="aspect-video rounded-xl bg-muted/50" />
<div className="aspect-video rounded-xl bg-muted/50" />
</div>
<div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min" />
</div>
</SidebarInset>
</SidebarProvider>
)
}
Key Features
-
useQuery
Hook: Fetches user data from the Convex database by using the user's Kinde ID. - Kinde Integration: Utilizes
useKindeBrowserClient
to manage user authentication and permissions. - Dynamic Rendering: Displays the username (
profile?.username
) fetched from Convex in the breadcrumb navigation.
RBAC (or role-based access control) with Kinde
Role-Based Access Control (RBAC) is a critical authorization mechanism that determines users' level of access based on their roles within an organization. By assigning specific permissions to roles, RBAC ensures that users can only perform actions they're authorized for. This part of the tutorial walks you through implementing RBAC using Kinde for our application.
In our scenario, the application allows:
- Members to upload files but not delete them.
- Admins to delete files, both their own and others' within the organization.
By following this guide, you'll learn to manage roles and permissions effectively, both on the frontend (hiding UI elements) and the backend (enforcing restrictions).
Step 1: Setting Up Permissions in Kinde
Permissions define the actions users can perform. Let’s start by creating a delete:file
permission.
- Access the Kinde Dashboard
- Log in to your Kinde Dashboard.
- Navigate to Settings in the sidebar and select User Management.
- Add a Permission
- Click on Permissions in the User Management section.
- Click Add Permission.
- Add details of a permission and then save
- Name:
Delete File
(or any descriptive name). - Description: Leave blank or add and optional context.
- Key:
delete:file
.
- Name:
Step 2: Creating Roles for Users
Roles group permissions together and are assigned to users based on their responsibilities. Let’s create an admin role that includes the delete:file
permission.
- Add a Role
- Go to the Users section on the dashboard.
- Click on a user.
- Next, click on Permissions
- Click Add Role.
- Name: admin.
- Key: admin.
- Permissions: Select
delete:file
.
- Save the role.
Step 3: Assigning Roles to Users
Now that the admin role is created, assign it to specific users.
- Assign the Role to a User
- Return to the Users section.
- Select the user you want to make an admin.
- Toggle the switch for the Admin role to Active.
- Save the changes
Step 4: Verifying Permissions
To confirm the user’s permissions:
- Go back to the Users section and select the user.
- View the Permissions tab to see that the
delete:file
permission is now linked to their role.
Final Thoughts
This tutorial provides a detailed guide to integrating Kinde and Convex into a Next.js application to build a secure, scalable, and user-friendly file-sharing platform with features like passwordless authentication, real-time data synchronization, and role-based access control (RBAC).
Highlights:
1. Authentication with Kinde:
- Seamlessly integrate passwordless sign-ins and support social logins (Google and Facebook).
- Use Kinde's middleware to protect routes and manage secure user sessions.
- Configure webhooks to synchronize user authentication events with Convex in real time.
2. Backend with Convex:
- Leverage Convex's real-time database and serverless architecture to manage user data.
- Define schemas, queries, and mutations to store and retrieve user information dynamically.
- Utilize Convex's HTTP routing for webhook integration and automate user data updates.
3. Role-Based Access Control (RBAC):
- Setup RBAC using Kinde to restrict user actions based on roles.
- Create permissions (e.g., delete:file) and roles (e.g., admin) directly in the Kinde dashboard.
4. Practical Implementation:
- Develop a feature-rich user interface with dynamic user state handling for logged-in and logged-out experiences.
- Use webhooks and JWT validation to synchronize authentication events securely.
- Test login/logout flows and fetch user data from Convex for personalized dashboards.
5. Developer-Friendly Setup:
- Utilize a pre-configured Next.js starter repository with TypeScript, TailwindCSS, and Shadcn UI components.
- Comprehensive step-by-step instructions for setting up Kinde and Convex, ensuring seamless integration.
Access the Full Codebase
Want to explore the complete implementation? Check out the fully implemented codebase on GitHub. Feel free to clone, experiment, and adapt it to your needs. Contributions and stars are always welcome!
sholajegede / kinde-convex-starter
The Kinde and Convex Starter App is a fully pre-configured Next.js application designed to help you quickly integrate secure authentication, role-based access control (RBAC), and user data management using Kinde and Convex.
Kinde-Convex File Sharing App: Part 1
A fully pre-configured Next.js application to kickstart your development with Kinde and Convex, enabling secure authentication, real-time data syncing, and role-based access control (RBAC). This app serves as a foundational template for building scalable, secure, and user-friendly file-sharing applications.
🚀Features
- Passwordless Authentication: Enable seamless logins with Kinde's secure authentication services.
- Social Login Integration: Support for Google, Facebook, and GitHub sign-ins.
- Real-Time Data Management: Powered by Convex, ensuring real-time updates between the frontend and backend.
- Role-Based Access Control (RBAC): Assign and manage roles like Admins and Members to restrict or enable access to features.
- Webhook Integration: Sync user authentication events to your database in real-time.
- Pre-Styled UI Components: Built using Shadcn and TailwindCSS for a responsive and intuitive design.
🎓What You'll Learn
This repository demonstrates how to:
- Integrate Kinde into your Next.js project for authentication.
- Configure Convex for database and backend API functions.
- Implement RBAC…
What’s Next?
In Part 2, the focus shifts to implementing RBAC in a production-ready file-sharing application. You’ll learn to configure advanced roles and permissions, apply granular access controls, and enforce RBAC across API endpoints and frontend workflows. This ensures secure and scalable authorization for real-world use cases.
With this tutorial, you're equipped to build applications with enterprise-grade security with Kinde, real-time data updates, and precise access control mechanisms. Part 2 promises to take this foundation to the next level! 🚀
We’d love to hear from you!
Got thoughts, questions, or suggestions? Drop them in the comments below or reach out to me directly on GitHub. Your feedback helps to improve and ensures we cover everything you need to succeed. Let us know what you think—let’s build together!
Top comments (1)
dev.to/hanzla-baig/winterland-a-be...