Integrate Convex with Kinde
Integrations
In this tutorial you’ll build a small message board where only signed-in users can post, and everyone sees new messages as they arrive—no page reload. You’ll define a messages table in Convex, add list and send mutations plus a query, and wire up a React UI that uses the user data you synced in the Kinde + Convex integration guide.
messages schema and functionAdd the following messages schema to your Convex schema:
export default defineSchema({ // ... existing schema ...
messages: defineTable({ userId: v.string(), authorName: v.optional(v.string()), message: v.string(), }).index("by_user_id", ["userId"]),});Create the messages file and add the contents:
touch convex/messages.tsimport { v } from "convex/values";import { mutation, query } from "./_generated/server";
export const list = query({ args: {}, handler: async (ctx) => { return await ctx.db .query("messages") .order("desc") .take(100); },});
export const sendMessage = mutation({ args: { message: v.string(), }, handler: async (ctx, args) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) { throw new Error("Must be authenticated to send a message"); } const user = await ctx.db .query("users") .withIndex("by_kinde_id", (q) => q.eq("kindeId", identity.subject)) .unique(); if (!user) { throw new Error("User not found"); } await ctx.db.insert("messages", { userId: user.kindeId, authorName: user.givenName ?? "Someone", message: args.message, }); },});The above code creates two functions:
list: A query function that returns the last 100 messages from the database.sendMessage: A mutation function that inserts a new message into the database.MessageBoard componentCreate a new file and add the following code:
touch src/MessageBoard.tsximport { useMutation, useQuery } from "convex/react";import { useState } from "react";import { api } from "../convex/_generated/api";
export default function MessageBoard() { const messages = useQuery(api.messages.list); const sendMessage = useMutation(api.messages.sendMessage); const [message, setMessage] = useState("");
async function handleSubmit(e: React.FormEvent) { e.preventDefault(); const trimmed = message.trim(); if (!trimmed) return; await sendMessage({ message: trimmed }); setMessage(""); }
return ( <section className="message-board"> <h2 className="message-board-title">Message Board</h2> <ul className="message-list"> {messages === undefined ? ( <li className="message-item">Loading messages…</li> ) : messages.length === 0 ? ( <li className="message-item">No messages yet. Send one below.</li> ) : ( messages.map((m) => ( <li key={m._id} className="message-item"> <span className="message-author">{m.authorName ?? "Someone"}:</span> {m.message} </li> )) )} </ul> <form onSubmit={handleSubmit} className="message-form"> <input type="text" className="message-input" value={message} onChange={(e) => setMessage(e.target.value)} placeholder="Type a message..." aria-label="Message" /> <button type="submit" className="btn btn-primary" disabled={!message.trim()}> Send </button> </form> </section> );}Update your App.tsx file to include the MessageBoard component inside the <Authenticated> block:
/* ... existing imports ... */import MessageBoard from "./MessageBoard";
<Authenticated> {/* ... existing code ... */} <MessageBoard /></Authenticated>:root { --kinde-primary: #111111; --kinde-primary-hover: #333333; --kinde-base: #1f2937; --kinde-base-muted: #6b7280; --kinde-bg: #ffffff; --kinde-bg-subtle: #f9fafb; --kinde-border: #e5e7eb; --kinde-radius: 8px; --kinde-font: system-ui, -apple-system, sans-serif;}
/* Layout */#root { max-width: 640px; margin: 0 auto; padding: 2rem; font-family: var(--kinde-font); color: var(--kinde-base); background: var(--kinde-bg);}
.app { min-height: 100vh;}
.app-header { margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid var(--kinde-border); font-size: 0.9375rem; color: var(--kinde-base-muted);}
.app-header strong { color: var(--kinde-base);}
.auth-links a { color: var(--kinde-primary); text-decoration: none; font-weight: 500;}
.auth-links a:hover { color: var(--kinde-primary-hover); text-decoration: underline;}
.auth-links a + a { margin-left: 1rem;}
.loading { color: var(--kinde-base-muted); padding: 2rem; text-align: center;}
/* Message board */.message-board { background: var(--kinde-bg-subtle); border: 1px solid var(--kinde-border); border-radius: var(--kinde-radius); padding: 1.25rem;}
.message-board-title { margin: 0 0 1rem; font-size: 1rem; font-weight: 600; color: var(--kinde-base);}
.message-list { list-style: none; padding: 0; margin: 0 0 1rem; max-height: 40vh; overflow-y: auto;}
.message-item { padding: 0.5rem 0.75rem; margin-bottom: 0.25rem; background: var(--kinde-bg); border-radius: 6px; font-size: 0.875rem; border: 1px solid var(--kinde-border);}
.message-author { color: var(--kinde-primary); font-weight: 600; margin-right: 0.35rem;}
.message-form { display: flex; gap: 0.5rem; margin-top: 0.75rem;}
.message-input { flex: 1; padding: 0.5rem 0.75rem; font-size: 0.875rem; font-family: inherit; border: 1px solid var(--kinde-border); border-radius: 6px; background: var(--kinde-bg); color: var(--kinde-base);}
.message-input::placeholder { color: var(--kinde-base-muted);}
.message-input:focus { outline: none; border-color: var(--kinde-primary); box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);}
/* Buttons – Kinde primary style */.btn { padding: 0.5rem 1rem; font-size: 0.875rem; font-weight: 500; font-family: inherit; border-radius: 6px; border: none; cursor: pointer; transition: background-color 0.15s;}
.btn-primary { background: var(--kinde-primary); color: white;}
.btn-primary:hover:not(:disabled) { background: var(--kinde-primary-hover);}
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed;}Run the application using the following terminal command:
npm run devGo to http://localhost:5173 and sign in or create a new account.
You should see the message board with the ability to send messages.