Convex Adapter for Auth.js (NextAuth) Setup Guide
Learn how to configure Convex as your database adapter for Auth.js See Convex with Auth.js article for more context on using Convex with Auth.js.
Do you know Auth.js and Convex already, and just want to get going with a template? Clone this repo and follow its README:
Installation
The adapter is currently not packaged into Auth.js. You will install it by copying code into your convex
folder and into your Next.js app
folder (even if you’re using the Pages router).
Dependencies
If you don’t already use it, install convex-helpers
, which is used in the adapter code:
1npm install convex-helpers
2
Schema
The adapter uses indexes for efficient reads from the database. Copy the authTables
code to your convex/schema.ts
file. This schema matches the model used by Auth.js.
convex/schema.ts
code
1import { defineSchema, defineTable } from "convex/server";
2import { Validator, v } from "convex/values";
3
4// The users, accounts, sessions and verificationTokens tables are modeled
5// from https://authjs.dev/getting-started/adapters#models
6
7export const userSchema = {
8 email: v.string(),
9 name: v.optional(v.string()),
10 emailVerified: v.optional(v.number()),
11 image: v.optional(v.string()),
12};
13
14export const sessionSchema = {
15 userId: v.id("users"),
16 expires: v.number(),
17 sessionToken: v.string(),
18};
19
20export const accountSchema = {
21 userId: v.id("users"),
22 type: v.union(
23 v.literal("email"),
24 v.literal("oidc"),
25 v.literal("oauth"),
26 v.literal("webauthn"),
27 ),
28 provider: v.string(),
29 providerAccountId: v.string(),
30 refresh_token: v.optional(v.string()),
31 access_token: v.optional(v.string()),
32 expires_at: v.optional(v.number()),
33 token_type: v.optional(v.string() as Validator<Lowercase<string>>),
34 scope: v.optional(v.string()),
35 id_token: v.optional(v.string()),
36 session_state: v.optional(v.string()),
37};
38
39export const verificationTokenSchema = {
40 identifier: v.string(),
41 token: v.string(),
42 expires: v.number(),
43};
44
45export const authenticatorSchema = {
46 credentialID: v.string(),
47 userId: v.id("users"),
48 providerAccountId: v.string(),
49 credentialPublicKey: v.string(),
50 counter: v.number(),
51 credentialDeviceType: v.string(),
52 credentialBackedUp: v.boolean(),
53 transports: v.optional(v.string()),
54};
55
56const authTables = {
57 users: defineTable(userSchema).index("email", ["email"]),
58 sessions: defineTable(sessionSchema)
59 .index("sessionToken", ["sessionToken"])
60 .index("userId", ["userId"]),
61 accounts: defineTable(accountSchema)
62 .index("providerAndAccountId", ["provider", "providerAccountId"])
63 .index("userId", ["userId"]),
64 verificationTokens: defineTable(verificationTokenSchema).index(
65 "identifierToken",
66 ["identifier", "token"],
67 ),
68 authenticators: defineTable(authenticatorSchema)
69 .index("userId", ["userId"])
70 .index("credentialID", ["credentialID"]),
71};
72
73export default defineSchema({
74 ...authTables,
75 // your other tables
76 // or pass `strictTableNameTypes: false`
77 // in the second argument argument to `defineSchema`
78});
79
Endpoints
Copy all of the following code to convex/authAdapter.ts
. This is the Convex portion of the adapter. It uses convex-helpers
to define custom query and mutation constructor functions that ensure all the endpoints in this file are secured. The endpoints match 1:1 the Auth.js Adapter
interface.
convex/authAdapter.ts
code
1import { partial } from "convex-helpers/validators";
2import {
3 customMutation,
4 customQuery,
5} from "convex-helpers/server/customFunctions";
6import { v } from "convex/values";
7import { mutation, query } from "./_generated/server";
8import {
9 accountSchema,
10 authenticatorSchema,
11 sessionSchema,
12 userSchema,
13 verificationTokenSchema,
14} from "./schema";
15
16const adapterQuery = customQuery(query, {
17 args: { secret: v.string() },
18 input: async (_ctx, { secret }) => {
19 checkSecret(secret);
20 return { ctx: {}, args: {} };
21 },
22});
23
24const adapterMutation = customMutation(mutation, {
25 args: { secret: v.string() },
26 input: async (_ctx, { secret }) => {
27 checkSecret(secret);
28 return { ctx: {}, args: {} };
29 },
30});
31
32function checkSecret(secret: string) {
33 if (process.env.CONVEX_AUTH_ADAPTER_SECRET === undefined) {
34 throw new Error(
35 "Missing CONVEX_AUTH_ADAPTER_SECRET Convex environment variable",
36 );
37 }
38 if (secret !== process.env.CONVEX_AUTH_ADAPTER_SECRET) {
39 throw new Error("Adapter API called without correct secret value");
40 }
41}
42
43export const createAuthenticator = adapterMutation({
44 args: { authenticator: v.object(authenticatorSchema) },
45 handler: async (ctx, args) => {
46 return await ctx.db.insert("authenticators", args.authenticator);
47 },
48});
49
50export const createSession = adapterMutation({
51 args: { session: v.object(sessionSchema) },
52 handler: async (ctx, { session }) => {
53 return await ctx.db.insert("sessions", session);
54 },
55});
56
57export const createUser = adapterMutation({
58 args: { user: v.object(userSchema) },
59 handler: async (ctx, { user }) => {
60 return await ctx.db.insert("users", user);
61 },
62});
63
64export const createVerificationToken = adapterMutation({
65 args: { verificationToken: v.object(verificationTokenSchema) },
66 handler: async (ctx, { verificationToken }) => {
67 return await ctx.db.insert("verificationTokens", verificationToken);
68 },
69});
70
71export const deleteSession = adapterMutation({
72 args: { sessionToken: v.string() },
73 handler: async (ctx, args) => {
74 const session = await ctx.db
75 .query("sessions")
76 .withIndex("sessionToken", (q) => q.eq("sessionToken", args.sessionToken))
77 .unique();
78 if (session === null) {
79 return null;
80 }
81 await ctx.db.delete(session._id);
82 return session;
83 },
84});
85
86export const deleteUser = adapterMutation({
87 args: { id: v.id("users") },
88 handler: async (ctx, { id }) => {
89 const user = await ctx.db.get(id);
90 if (user === null) {
91 return null;
92 }
93 await ctx.db.delete(id);
94 const sessions = await ctx.db
95 .query("sessions")
96 .withIndex("userId", (q) => q.eq("userId", id))
97 .collect();
98 for (const session of sessions) {
99 await ctx.db.delete(session._id);
100 }
101 const accounts = await ctx.db
102 .query("accounts")
103 .withIndex("userId", (q) => q.eq("userId", id))
104 .collect();
105 for (const account of accounts) {
106 await ctx.db.delete(account._id);
107 }
108 return user;
109 },
110});
111
112export const getAccount = adapterQuery({
113 args: { provider: v.string(), providerAccountId: v.string() },
114 handler: async (ctx, { provider, providerAccountId }) => {
115 return await ctx.db
116 .query("accounts")
117 .withIndex("providerAndAccountId", (q) =>
118 q.eq("provider", provider).eq("providerAccountId", providerAccountId),
119 )
120 .unique();
121 },
122});
123
124export const getAuthenticator = adapterQuery({
125 args: { credentialID: v.string() },
126 handler: async (ctx, { credentialID }) => {
127 return await ctx.db
128 .query("authenticators")
129 .withIndex("credentialID", (q) => q.eq("credentialID", credentialID))
130 .unique();
131 },
132});
133
134export const getSessionAndUser = adapterQuery({
135 args: { sessionToken: v.string() },
136 handler: async (ctx, { sessionToken }) => {
137 const session = await ctx.db
138 .query("sessions")
139 .withIndex("sessionToken", (q) => q.eq("sessionToken", sessionToken))
140 .unique();
141 if (session === null) {
142 return null;
143 }
144 const user = await ctx.db.get(session.userId);
145 if (user === null) {
146 return null;
147 }
148 return { session, user };
149 },
150});
151
152export const getUser = adapterQuery({
153 args: { id: v.id("users") },
154 handler: async (ctx, { id }) => {
155 return await ctx.db.get(id);
156 },
157});
158
159export const getUserByAccount = adapterQuery({
160 args: { provider: v.string(), providerAccountId: v.string() },
161 handler: async (ctx, { provider, providerAccountId }) => {
162 const account = await ctx.db
163 .query("accounts")
164 .withIndex("providerAndAccountId", (q) =>
165 q.eq("provider", provider).eq("providerAccountId", providerAccountId),
166 )
167 .unique();
168 if (account === null) {
169 return null;
170 }
171 return await ctx.db.get(account.userId);
172 },
173});
174
175export const getUserByEmail = adapterQuery({
176 args: { email: v.string() },
177 handler: async (ctx, { email }) => {
178 return await ctx.db
179 .query("users")
180 .withIndex("email", (q) => q.eq("email", email))
181 .unique();
182 },
183});
184
185export const linkAccount = adapterMutation({
186 args: { account: v.object(accountSchema) },
187 handler: async (ctx, { account }) => {
188 const id = await ctx.db.insert("accounts", account);
189 return await ctx.db.get(id);
190 },
191});
192
193export const listAuthenticatorsByUserId = adapterQuery({
194 args: { userId: v.id("users") },
195 handler: async (ctx, { userId }) => {
196 return await ctx.db
197 .query("authenticators")
198 .withIndex("userId", (q) => q.eq("userId", userId))
199 .collect();
200 },
201});
202
203export const unlinkAccount = adapterMutation({
204 args: { provider: v.string(), providerAccountId: v.string() },
205 handler: async (ctx, { provider, providerAccountId }) => {
206 const account = await ctx.db
207 .query("accounts")
208 .withIndex("providerAndAccountId", (q) =>
209 q.eq("provider", provider).eq("providerAccountId", providerAccountId),
210 )
211 .unique();
212 if (account === null) {
213 return null;
214 }
215 await ctx.db.delete(account._id);
216 return account;
217 },
218});
219
220export const updateAuthenticatorCounter = adapterMutation({
221 args: { credentialID: v.string(), newCounter: v.number() },
222 handler: async (ctx, { credentialID, newCounter }) => {
223 const authenticator = await ctx.db
224 .query("authenticators")
225 .withIndex("credentialID", (q) => q.eq("credentialID", credentialID))
226 .unique();
227 if (authenticator === null) {
228 throw new Error(
229 `Authenticator not found for credentialID: ${credentialID}`,
230 );
231 }
232 await ctx.db.patch(authenticator._id, { counter: newCounter });
233 return { ...authenticator, counter: newCounter };
234 },
235});
236
237export const updateSession = adapterMutation({
238 args: {
239 session: v.object({
240 expires: v.number(),
241 sessionToken: v.string(),
242 }),
243 },
244 handler: async (ctx, { session }) => {
245 const existingSession = await ctx.db
246 .query("sessions")
247 .withIndex("sessionToken", (q) =>
248 q.eq("sessionToken", session.sessionToken),
249 )
250 .unique();
251 if (existingSession === null) {
252 return null;
253 }
254 await ctx.db.patch(existingSession._id, session);
255 },
256});
257
258export const updateUser = adapterMutation({
259 args: {
260 user: v.object({
261 id: v.id("users"),
262 ...partial(userSchema),
263 }),
264 },
265 handler: async (ctx, { user: { id, ...data } }) => {
266 const user = await ctx.db.get(id);
267 if (user === null) {
268 return;
269 }
270 await ctx.db.patch(user._id, data);
271 },
272});
273
274export const useVerificationToken = adapterMutation({
275 args: { identifier: v.string(), token: v.string() },
276 handler: async (ctx, { identifier, token }) => {
277 const verificationToken = await ctx.db
278 .query("verificationTokens")
279 .withIndex("identifierToken", (q) =>
280 q.eq("identifier", identifier).eq("token", token),
281 )
282 .unique();
283 if (verificationToken === null) {
284 return null;
285 }
286 await ctx.db.delete(verificationToken._id);
287 return verificationToken;
288 },
289});
290
Interface
Copy all the following code tom app/ConvexAdapter.ts
. This is the Auth.js/Next.js portion of the adapter. It uses fetchMutation
and fetchQuery
to call your Convex backend from the Next.js server.
app/ConvexAdapter.ts
code
1import type {
2 Adapter,
3 AdapterAccount,
4 AdapterAuthenticator,
5 AdapterSession,
6 AdapterUser,
7 VerificationToken,
8} from "@auth/core/adapters";
9import { fetchMutation, fetchQuery } from "convex/nextjs";
10import { FunctionArgs, FunctionReference } from "convex/server";
11import { api } from "../convex/_generated/api";
12import { Doc, Id } from "../convex/_generated/dataModel";
13
14type User = AdapterUser & { id: Id<"users"> };
15type Session = AdapterSession & { userId: Id<"users"> };
16type Account = AdapterAccount & { userId: Id<"users"> };
17type Authenticator = AdapterAuthenticator & { userId: Id<"users"> };
18
19export const ConvexAdapter: Adapter = {
20 async createAuthenticator(authenticator: Authenticator) {
21 await callMutation(api.authAdapter.createAuthenticator, { authenticator });
22 return authenticator;
23 },
24 async createSession(session: Session) {
25 const id = await callMutation(api.authAdapter.createSession, {
26 session: toDB(session),
27 });
28 return { ...session, id };
29 },
30 async createUser({ id: _, ...user }: User) {
31 const id = await callMutation(api.authAdapter.createUser, {
32 user: toDB(user),
33 });
34 return { ...user, id };
35 },
36 async createVerificationToken(verificationToken: VerificationToken) {
37 await callMutation(api.authAdapter.createVerificationToken, {
38 verificationToken: toDB(verificationToken),
39 });
40 return verificationToken;
41 },
42 async deleteSession(sessionToken) {
43 return maybeSessionFromDB(
44 await callMutation(api.authAdapter.deleteSession, {
45 sessionToken,
46 }),
47 );
48 },
49 async deleteUser(id: Id<"users">) {
50 return maybeUserFromDB(
51 await callMutation(api.authAdapter.deleteUser, { id }),
52 );
53 },
54 async getAccount(providerAccountId, provider) {
55 return await callQuery(api.authAdapter.getAccount, {
56 provider,
57 providerAccountId,
58 });
59 },
60 async getAuthenticator(credentialID) {
61 return await callQuery(api.authAdapter.getAuthenticator, { credentialID });
62 },
63 async getSessionAndUser(sessionToken) {
64 const result = await callQuery(api.authAdapter.getSessionAndUser, {
65 sessionToken,
66 });
67 if (result === null) {
68 return null;
69 }
70 const { user, session } = result;
71 return { user: userFromDB(user), session: sessionFromDB(session) };
72 },
73 async getUser(id: Id<"users">) {
74 return maybeUserFromDB(await callQuery(api.authAdapter.getUser, { id }));
75 },
76 async getUserByAccount({ provider, providerAccountId }) {
77 return maybeUserFromDB(
78 await callQuery(api.authAdapter.getUserByAccount, {
79 provider,
80 providerAccountId,
81 }),
82 );
83 },
84 async getUserByEmail(email) {
85 return maybeUserFromDB(
86 await callQuery(api.authAdapter.getUserByEmail, { email }),
87 );
88 },
89 async linkAccount(account: Account) {
90 return await callMutation(api.authAdapter.linkAccount, { account });
91 },
92 async listAuthenticatorsByUserId(userId: Id<"users">) {
93 return await callQuery(api.authAdapter.listAuthenticatorsByUserId, {
94 userId,
95 });
96 },
97 async unlinkAccount({ provider, providerAccountId }) {
98 return (
99 (await callMutation(api.authAdapter.unlinkAccount, {
100 provider,
101 providerAccountId,
102 })) ?? undefined
103 );
104 },
105 async updateAuthenticatorCounter(credentialID, newCounter) {
106 return await callMutation(api.authAdapter.updateAuthenticatorCounter, {
107 credentialID,
108 newCounter,
109 });
110 },
111 async updateSession(session: Session) {
112 return await callMutation(api.authAdapter.updateSession, {
113 session: toDB(session),
114 });
115 },
116 async updateUser(user: User) {
117 await callMutation(api.authAdapter.updateUser, { user: toDB(user) });
118 return user;
119 },
120 async useVerificationToken({ identifier, token }) {
121 return maybeVerificationTokenFromDB(
122 await callMutation(api.authAdapter.useVerificationToken, {
123 identifier,
124 token,
125 }),
126 );
127 },
128};
129
130/// Helpers
131
132function callQuery<Query extends FunctionReference<"query">>(
133 query: Query,
134 args: Omit<FunctionArgs<Query>, "secret">,
135) {
136 return fetchQuery(query, addSecret(args) as any);
137}
138
139function callMutation<Mutation extends FunctionReference<"mutation">>(
140 mutation: Mutation,
141 args: Omit<FunctionArgs<Mutation>, "secret">,
142) {
143 return fetchMutation(mutation, addSecret(args) as any);
144}
145
146if (process.env.CONVEX_AUTH_ADAPTER_SECRET === undefined) {
147 throw new Error("Missing CONVEX_AUTH_ADAPTER_SECRET environment variable");
148}
149
150function addSecret(args: Record<string, any>) {
151 return { ...args, secret: process.env.CONVEX_AUTH_ADAPTER_SECRET! };
152}
153
154function maybeUserFromDB(user: Doc<"users"> | null) {
155 if (user === null) {
156 return null;
157 }
158 return userFromDB(user);
159}
160
161function userFromDB(user: Doc<"users">) {
162 return {
163 ...user,
164 id: user._id,
165 emailVerified: maybeDate(user.emailVerified),
166 };
167}
168
169function maybeSessionFromDB(session: Doc<"sessions"> | null) {
170 if (session === null) {
171 return null;
172 }
173 return sessionFromDB(session);
174}
175
176function sessionFromDB(session: Doc<"sessions">) {
177 return { ...session, id: session._id, expires: new Date(session.expires) };
178}
179
180function maybeVerificationTokenFromDB(
181 verificationToken: Doc<"verificationTokens"> | null,
182) {
183 if (verificationToken === null) {
184 return null;
185 }
186 return verificationTokenFromDB(verificationToken);
187}
188
189function verificationTokenFromDB(verificationToken: Doc<"verificationTokens">) {
190 return { ...verificationToken, expires: new Date(verificationToken.expires) };
191}
192
193function maybeDate(value: number | undefined) {
194 return value === undefined ? null : new Date(value);
195}
196
197function toDB<T extends object>(
198 obj: T,
199): {
200 [K in keyof T]: T[K] extends Date
201 ? number
202 : null extends T[K]
203 ? undefined
204 : T[K];
205} {
206 const result: any = {};
207 for (const key in obj) {
208 const value = obj[key];
209 result[key] =
210 value instanceof Date
211 ? value.getTime()
212 : value === null
213 ? undefined
214 : value;
215 }
216 return result;
217}
218
Environment variables
Both in Next.js and in Convex set CONVEX_AUTH_ADAPTER_SECRET
to the same secret value.
The adapter endpoints are public, but must be called only by your Next.js server code. Since you control both the Next.js server and the Convex backend, this can be done with a simple shared secret stored in an environment variable on both ends.
Generate a random value (with openssl rand -base64 33
or npx auth secret
or pick a long, unique phrase). Set it as CONVEX_AUTH_ADAPTER_SECRET
in .env.local
.
Then also paste it into your Convex deployment’s settings or set it from the CLI with npx convex env set CONVEX_AUTH_ADAPTER_SECRET somevalue
.
When you deploy to production, remember to set this on your prod deployment with npx convex env --prod set ...
or on the prod dashboard, as well as in your hosting provider’s settings (e.g. Netlify or Vercel’s environment variables).
Configuration
Use the ConvexAdapter
object as the adapter
in your Auth.js config:
1import NextAuth from "next-auth"
2import { ConvexAdapter } from "@/app/ConvexAdapter";
3
4export const { handlers, auth, signIn, signOut } = NextAuth({
5 providers: [],
6 adapter: ConvexAdapter,
7})
8
Authenticating Convex function calls
Convex uses the Open ID Connect protocol for authentication. It requires that the caller provides a JWT signed with a private key. The JWT signature can be verified using a public key normally exposed by the server that issued the JWT.
Since you control both the Next.js server and the Convex backend, you can have your Next.js server issue the JWT and your Convex backend expose the public key1.
Dependencies
Install the jose
library for creating public-private key pairs and signing JWTs:
1npm install jose
2
Generate the keys
Save this code as generateKeys.mjs
in your project directory:
generateKeys.mjs
1import { exportJWK, exportPKCS8, generateKeyPair } from "jose";
2
3const keys = await generateKeyPair("RS256");
4const privateKey = await exportPKCS8(keys.privateKey);
5const publicKey = await exportJWK(keys.publicKey);
6const jwks = JSON.stringify({ keys: [{ use: "sig", ...publicKey }] });
7
8process.stdout.write(
9 `CONVEX_AUTH_PRIVATE_KEY="${privateKey.replace(/\n/g, "\\n")}"`,
10);
11process.stdout.write("\n\n");
12process.stdout.write(`JWKS=${jwks}`);
13process.stdout.write("\n");
14
And run it with Node to generate two KEY=value
environment variables:
1node generateKeys.mjs
2
The first environment variable is the private key (CONVEX_AUTH_PRIVATE_KEY="-----BEGIN PRIVATE KEY--—…
). Configure your Next.js server with it (i.e. paste it in your .env.local
file).
The second is the public key included in a JSON Web Key Set (JWKS='{"keys”…
). Configure your Convex backend with it (i.e. paste it into your Convex deployment’s settings).
Expose the public key
Add the two endpoints as shown to your http.ts
file:
Example http.ts
1import { httpRouter } from "convex/server";
2import { httpAction } from "./_generated/server";
3
4const http = httpRouter();
5
6http.route({
7 path: "/.well-known/openid-configuration",
8 method: "GET",
9 handler: httpAction(async () => {
10 return new Response(
11 JSON.stringify({
12 issuer: process.env.CONVEX_SITE_URL,
13 jwks_uri: process.env.CONVEX_SITE_URL + "/.well-known/jwks.json",
14 authorization_endpoint:
15 process.env.CONVEX_SITE_URL + "/oauth/authorize",
16 }),
17 {
18 status: 200,
19 headers: {
20 "Content-Type": "application/json",
21 "Cache-Control":
22 "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400",
23 },
24 },
25 );
26 }),
27});
28
29http.route({
30 path: "/.well-known/jwks.json",
31 method: "GET",
32 handler: httpAction(async () => {
33 if (process.env.JWKS === undefined) {
34 throw new Error("Missing JWKS Convex environment variable");
35 }
36 return new Response(process.env.JWKS, {
37 status: 200,
38 headers: {
39 "Content-Type": "application/json",
40 "Cache-Control":
41 "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400",
42 },
43 });
44 }),
45});
46
47export default http;
48
Modify the Convex auth config
Add a new provider to your convex/auth.config.ts
:
1export default {
2 providers: [
3 {
4 domain: process.env.CONVEX_SITE_URL,
5 applicationID: "convex",
6 },
7 ],
8};
9
Because domain
is set to the value of the built-in CONVEX_SITE_URL
environment variable, you’re telling your Convex backend to check with itself for the public key it should use to validate JWTs. The applicationID
value must match the “audience” set on the JWT.
Issue the JWT
In the session callback of the Auth.js config create the JWT and add it to the session
object. This will make the JWT available on both the server and the client:
1import { ConvexAdapter } from "@/app/ConvexAdapter";
2import { SignJWT, importPKCS8 } from "jose";
3import NextAuth from "next-auth";
4
5const CONVEX_SITE_URL = process.env.NEXT_PUBLIC_CONVEX_URL!.replace(
6 /.cloud$/,
7 ".site",
8);
9
10export const { handlers, signIn, signOut, auth } = NextAuth({
11 debug: true,
12 providers: [
13 // ... configured providers
14 ],
15 adapter: ConvexAdapter,
16 callbacks: {
17 async session({ session }) {
18 const privateKey = await importPKCS8(
19 process.env.CONVEX_AUTH_PRIVATE_KEY!,
20 "RS256",
21 );
22 const convexToken = await new SignJWT({
23 sub: session.userId,
24 })
25 .setProtectedHeader({ alg: "RS256" })
26 .setIssuedAt()
27 .setIssuer(CONVEX_SITE_URL)
28 .setAudience("convex")
29 .setExpirationTime("1h")
30 .sign(privateKey);
31 return { ...session, convexToken };
32 },
33 },
34});
35
36declare module "next-auth" {
37 interface Session {
38 convexToken: string;
39 }
40}
41
Provide the JWT to the Convex React client
Replace your ConvexProvider
with ConvexProviderWithAuth
as shown:
1"use client";
2
3import { ConvexProviderWithAuth, ConvexReactClient } from "convex/react";
4import { SessionProvider, useSession } from "next-auth/react";
5import { Session } from "next-auth";
6import { ReactNode, useMemo } from "react";
7
8const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
9
10export default function ConvexClientProvider({
11 children,
12 session,
13}: {
14 children: ReactNode;
15 session: Session | null;
16}) {
17 return (
18 <SessionProvider session={session}>
19 <ConvexProviderWithAuth client={convex} useAuth={useAuth}>
20 {children}
21 </ConvexProviderWithAuth>
22 </SessionProvider>
23 );
24}
25
26function useAuth() {
27 const { data: session, update } = useSession();
28
29 const convexToken = convexTokenFromSession(session);
30 return useMemo(
31 () => ({
32 isLoading: false,
33 isAuthenticated: session !== null,
34 fetchAccessToken: async ({
35 forceRefreshToken,
36 }: {
37 forceRefreshToken: boolean;
38 }) => {
39 if (forceRefreshToken) {
40 const session = await update();
41
42 return convexTokenFromSession(session);
43 }
44 return convexToken;
45 },
46 }),
47 // We only care about the user changes, and don't want to
48 // bust the memo when we fetch a new token.
49 // eslint-disable-next-line react-hooks/exhaustive-deps
50 [JSON.stringify(session?.user)],
51 );
52}
53
54function convexTokenFromSession(session: Session | null): string | null {
55 return session?.convexToken ?? null;
56}
57
Make sure to pass the session
object from server to the client component:
App Router example, app/loggedin/layout.tsx
(in repo)
1import ConvexClientProvider from "@/app/ConvexClientProvider";
2import { auth, signOut } from "@/auth";
3import { ReactNode } from "react";
4
5export default async function LoggedInLayout({
6 children,
7}: {
8 children: ReactNode;
9}) {
10 const session = await auth();
11 return (
12 <>
13 <SignOut />
14 <ConvexClientProvider session={session}>{children}</ConvexClientProvider>
15 </>
16 );
17}
18
19function SignOut() {/*...*/}
20
Pages Router example, pages/loggedin/index.tsx
(in repo)
1import ConvexClientProvider from "@/app/ConvexClientProvider";
2import { auth } from "@/auth";
3import { Button } from "@/components/ui/button";
4import { Skeleton } from "@/components/ui/skeleton";
5import { api } from "@/convex/_generated/api";
6import { useMutation, useQuery } from "convex/react";
7import { GetServerSideProps, InferGetServerSidePropsType } from "next";
8import { Session } from "next-auth";
9
10export const getServerSideProps = (async (ctx) => {
11 const session = await auth(ctx);
12 return {
13 props: {
14 session,
15 },
16 };
17}) satisfies GetServerSideProps<{ session: Session | null }>;
18
19export default function Page({
20 session,
21}: InferGetServerSidePropsType<typeof getServerSideProps>) {
22 return (
23 <ConvexClientProvider session={session}>
24 <Content />
25 </ConvexClientProvider>
26 );
27}
28
Footnotes
Convex is the sync platform with everything you need to build your full-stack project. Cloud functions, a database, file storage, scheduling, search, and realtime updates fit together seamlessly.