Stack logo
Sync up on the latest from Convex.
Michal Srb's avatar
Michal Srb
7 months ago

Convex Adapter for Auth.js (NextAuth) Setup Guide

Next auth logo inside the convex logo

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:

get-convex/convex-nextauth-template

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

Example in repo

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

Example in repo

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

Example in repo

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

Example in repo

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

  1. We do this instead of hosting the public key on the Next.js server so that Convex can always access it from the cloud. When you’re developing your Next.js server locally, your Convex backend can’t reach it (without something like ngrok).

Build in minutes, scale forever.

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.

Get started