Stack logo
Sync up on the latest from Convex.
Michal Srb's avatar
Michal Srb
6 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:

npm install convex-helpers

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
import { defineSchema, defineTable } from "convex/server";
import { Validator, v } from "convex/values";

// The users, accounts, sessions and verificationTokens tables are modeled
// from https://authjs.dev/getting-started/adapters#models

export const userSchema = {
  email: v.string(),
  name: v.optional(v.string()),
  emailVerified: v.optional(v.number()),
  image: v.optional(v.string()),
};

export const sessionSchema = {
  userId: v.id("users"),
  expires: v.number(),
  sessionToken: v.string(),
};

export const accountSchema = {
  userId: v.id("users"),
  type: v.union(
    v.literal("email"),
    v.literal("oidc"),
    v.literal("oauth"),
    v.literal("webauthn"),
  ),
  provider: v.string(),
  providerAccountId: v.string(),
  refresh_token: v.optional(v.string()),
  access_token: v.optional(v.string()),
  expires_at: v.optional(v.number()),
  token_type: v.optional(v.string() as Validator<Lowercase<string>>),
  scope: v.optional(v.string()),
  id_token: v.optional(v.string()),
  session_state: v.optional(v.string()),
};

export const verificationTokenSchema = {
  identifier: v.string(),
  token: v.string(),
  expires: v.number(),
};

export const authenticatorSchema = {
  credentialID: v.string(),
  userId: v.id("users"),
  providerAccountId: v.string(),
  credentialPublicKey: v.string(),
  counter: v.number(),
  credentialDeviceType: v.string(),
  credentialBackedUp: v.boolean(),
  transports: v.optional(v.string()),
};

const authTables = {
  users: defineTable(userSchema).index("email", ["email"]),
  sessions: defineTable(sessionSchema)
    .index("sessionToken", ["sessionToken"])
    .index("userId", ["userId"]),
  accounts: defineTable(accountSchema)
    .index("providerAndAccountId", ["provider", "providerAccountId"])
    .index("userId", ["userId"]),
  verificationTokens: defineTable(verificationTokenSchema).index(
    "identifierToken",
    ["identifier", "token"],
  ),
  authenticators: defineTable(authenticatorSchema)
    .index("userId", ["userId"])
    .index("credentialID", ["credentialID"]),
};

export default defineSchema({
  ...authTables,
  // your other tables
	// or pass `strictTableNameTypes: false`
	// in the second argument argument to `defineSchema`
});

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
import { partial } from "convex-helpers/validators";
import {
  customMutation,
  customQuery,
} from "convex-helpers/server/customFunctions";
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import {
  accountSchema,
  authenticatorSchema,
  sessionSchema,
  userSchema,
  verificationTokenSchema,
} from "./schema";

const adapterQuery = customQuery(query, {
  args: { secret: v.string() },
  input: async (_ctx, { secret }) => {
    checkSecret(secret);
    return { ctx: {}, args: {} };
  },
});

const adapterMutation = customMutation(mutation, {
  args: { secret: v.string() },
  input: async (_ctx, { secret }) => {
    checkSecret(secret);
    return { ctx: {}, args: {} };
  },
});

function checkSecret(secret: string) {
	if (process.env.CONVEX_AUTH_ADAPTER_SECRET === undefined) {
    throw new Error(
      "Missing CONVEX_AUTH_ADAPTER_SECRET Convex environment variable",
    );
  }
  if (secret !== process.env.CONVEX_AUTH_ADAPTER_SECRET) {
    throw new Error("Adapter API called without correct secret value");
  }
}

export const createAuthenticator = adapterMutation({
  args: { authenticator: v.object(authenticatorSchema) },
  handler: async (ctx, args) => {
    return await ctx.db.insert("authenticators", args.authenticator);
  },
});

export const createSession = adapterMutation({
  args: { session: v.object(sessionSchema) },
  handler: async (ctx, { session }) => {
    return await ctx.db.insert("sessions", session);
  },
});

export const createUser = adapterMutation({
  args: { user: v.object(userSchema) },
  handler: async (ctx, { user }) => {
    return await ctx.db.insert("users", user);
  },
});

export const createVerificationToken = adapterMutation({
  args: { verificationToken: v.object(verificationTokenSchema) },
  handler: async (ctx, { verificationToken }) => {
    return await ctx.db.insert("verificationTokens", verificationToken);
  },
});

export const deleteSession = adapterMutation({
  args: { sessionToken: v.string() },
  handler: async (ctx, args) => {
    const session = await ctx.db
      .query("sessions")
      .withIndex("sessionToken", (q) => q.eq("sessionToken", args.sessionToken))
      .unique();
    if (session === null) {
      return null;
    }
    await ctx.db.delete(session._id);
    return session;
  },
});

export const deleteUser = adapterMutation({
  args: { id: v.id("users") },
  handler: async (ctx, { id }) => {
    const user = await ctx.db.get(id);
    if (user === null) {
      return null;
    }
    await ctx.db.delete(id);
    const sessions = await ctx.db
      .query("sessions")
      .withIndex("userId", (q) => q.eq("userId", id))
      .collect();
    for (const session of sessions) {
      await ctx.db.delete(session._id);
    }
    const accounts = await ctx.db
      .query("accounts")
      .withIndex("userId", (q) => q.eq("userId", id))
      .collect();
    for (const account of accounts) {
      await ctx.db.delete(account._id);
    }
    return user;
  },
});

export const getAccount = adapterQuery({
  args: { provider: v.string(), providerAccountId: v.string() },
  handler: async (ctx, { provider, providerAccountId }) => {
    return await ctx.db
      .query("accounts")
      .withIndex("providerAndAccountId", (q) =>
        q.eq("provider", provider).eq("providerAccountId", providerAccountId),
      )
      .unique();
  },
});

export const getAuthenticator = adapterQuery({
  args: { credentialID: v.string() },
  handler: async (ctx, { credentialID }) => {
    return await ctx.db
      .query("authenticators")
      .withIndex("credentialID", (q) => q.eq("credentialID", credentialID))
      .unique();
  },
});

export const getSessionAndUser = adapterQuery({
  args: { sessionToken: v.string() },
  handler: async (ctx, { sessionToken }) => {
    const session = await ctx.db
      .query("sessions")
      .withIndex("sessionToken", (q) => q.eq("sessionToken", sessionToken))
      .unique();
    if (session === null) {
      return null;
    }
    const user = await ctx.db.get(session.userId);
    if (user === null) {
      return null;
    }
    return { session, user };
  },
});

export const getUser = adapterQuery({
  args: { id: v.id("users") },
  handler: async (ctx, { id }) => {
    return await ctx.db.get(id);
  },
});

export const getUserByAccount = adapterQuery({
  args: { provider: v.string(), providerAccountId: v.string() },
  handler: async (ctx, { provider, providerAccountId }) => {
    const account = await ctx.db
      .query("accounts")
      .withIndex("providerAndAccountId", (q) =>
        q.eq("provider", provider).eq("providerAccountId", providerAccountId),
      )
      .unique();
    if (account === null) {
      return null;
    }
    return await ctx.db.get(account.userId);
  },
});

export const getUserByEmail = adapterQuery({
  args: { email: v.string() },
  handler: async (ctx, { email }) => {
    return await ctx.db
      .query("users")
      .withIndex("email", (q) => q.eq("email", email))
      .unique();
  },
});

export const linkAccount = adapterMutation({
  args: { account: v.object(accountSchema) },
  handler: async (ctx, { account }) => {
    const id = await ctx.db.insert("accounts", account);
    return await ctx.db.get(id);
  },
});

export const listAuthenticatorsByUserId = adapterQuery({
  args: { userId: v.id("users") },
  handler: async (ctx, { userId }) => {
    return await ctx.db
      .query("authenticators")
      .withIndex("userId", (q) => q.eq("userId", userId))
      .collect();
  },
});

export const unlinkAccount = adapterMutation({
  args: { provider: v.string(), providerAccountId: v.string() },
  handler: async (ctx, { provider, providerAccountId }) => {
    const account = await ctx.db
      .query("accounts")
      .withIndex("providerAndAccountId", (q) =>
        q.eq("provider", provider).eq("providerAccountId", providerAccountId),
      )
      .unique();
    if (account === null) {
      return null;
    }
    await ctx.db.delete(account._id);
    return account;
  },
});

export const updateAuthenticatorCounter = adapterMutation({
  args: { credentialID: v.string(), newCounter: v.number() },
  handler: async (ctx, { credentialID, newCounter }) => {
    const authenticator = await ctx.db
      .query("authenticators")
      .withIndex("credentialID", (q) => q.eq("credentialID", credentialID))
      .unique();
    if (authenticator === null) {
      throw new Error(
        `Authenticator not found for credentialID: ${credentialID}`,
      );
    }
    await ctx.db.patch(authenticator._id, { counter: newCounter });
    return { ...authenticator, counter: newCounter };
  },
});

export const updateSession = adapterMutation({
  args: {
    session: v.object({
      expires: v.number(),
      sessionToken: v.string(),
    }),
  },
  handler: async (ctx, { session }) => {
    const existingSession = await ctx.db
      .query("sessions")
      .withIndex("sessionToken", (q) =>
        q.eq("sessionToken", session.sessionToken),
      )
      .unique();
    if (existingSession === null) {
      return null;
    }
    await ctx.db.patch(existingSession._id, session);
  },
});

export const updateUser = adapterMutation({
  args: {
    user: v.object({
      id: v.id("users"),
      ...partial(userSchema),
    }),
  },
  handler: async (ctx, { user: { id, ...data } }) => {
    const user = await ctx.db.get(id);
    if (user === null) {
      return;
    }
    await ctx.db.patch(user._id, data);
  },
});

export const useVerificationToken = adapterMutation({
  args: { identifier: v.string(), token: v.string() },
  handler: async (ctx, { identifier, token }) => {
    const verificationToken = await ctx.db
      .query("verificationTokens")
      .withIndex("identifierToken", (q) =>
        q.eq("identifier", identifier).eq("token", token),
      )
      .unique();
    if (verificationToken === null) {
      return null;
    }
    await ctx.db.delete(verificationToken._id);
    return verificationToken;
  },
});

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
import type {
  Adapter,
  AdapterAccount,
  AdapterAuthenticator,
  AdapterSession,
  AdapterUser,
  VerificationToken,
} from "@auth/core/adapters";
import { fetchMutation, fetchQuery } from "convex/nextjs";
import { FunctionArgs, FunctionReference } from "convex/server";
import { api } from "../convex/_generated/api";
import { Doc, Id } from "../convex/_generated/dataModel";

type User = AdapterUser & { id: Id<"users"> };
type Session = AdapterSession & { userId: Id<"users"> };
type Account = AdapterAccount & { userId: Id<"users"> };
type Authenticator = AdapterAuthenticator & { userId: Id<"users"> };

export const ConvexAdapter: Adapter = {
  async createAuthenticator(authenticator: Authenticator) {
    await callMutation(api.authAdapter.createAuthenticator, { authenticator });
    return authenticator;
  },
  async createSession(session: Session) {
    const id = await callMutation(api.authAdapter.createSession, {
      session: toDB(session),
    });
    return { ...session, id };
  },
  async createUser({ id: _, ...user }: User) {
    const id = await callMutation(api.authAdapter.createUser, {
      user: toDB(user),
    });
    return { ...user, id };
  },
  async createVerificationToken(verificationToken: VerificationToken) {
    await callMutation(api.authAdapter.createVerificationToken, {
      verificationToken: toDB(verificationToken),
    });
    return verificationToken;
  },
  async deleteSession(sessionToken) {
    return maybeSessionFromDB(
      await callMutation(api.authAdapter.deleteSession, {
        sessionToken,
      }),
    );
  },
  async deleteUser(id: Id<"users">) {
    return maybeUserFromDB(
      await callMutation(api.authAdapter.deleteUser, { id }),
    );
  },
  async getAccount(providerAccountId, provider) {
    return await callQuery(api.authAdapter.getAccount, {
      provider,
      providerAccountId,
    });
  },
  async getAuthenticator(credentialID) {
    return await callQuery(api.authAdapter.getAuthenticator, { credentialID });
  },
  async getSessionAndUser(sessionToken) {
    const result = await callQuery(api.authAdapter.getSessionAndUser, {
      sessionToken,
    });
    if (result === null) {
      return null;
    }
    const { user, session } = result;
    return { user: userFromDB(user), session: sessionFromDB(session) };
  },
  async getUser(id: Id<"users">) {
    return maybeUserFromDB(await callQuery(api.authAdapter.getUser, { id }));
  },
  async getUserByAccount({ provider, providerAccountId }) {
    return maybeUserFromDB(
      await callQuery(api.authAdapter.getUserByAccount, {
        provider,
        providerAccountId,
      }),
    );
  },
  async getUserByEmail(email) {
    return maybeUserFromDB(
      await callQuery(api.authAdapter.getUserByEmail, { email }),
    );
  },
  async linkAccount(account: Account) {
    return await callMutation(api.authAdapter.linkAccount, { account });
  },
  async listAuthenticatorsByUserId(userId: Id<"users">) {
    return await callQuery(api.authAdapter.listAuthenticatorsByUserId, {
      userId,
    });
  },
  async unlinkAccount({ provider, providerAccountId }) {
    return (
      (await callMutation(api.authAdapter.unlinkAccount, {
        provider,
        providerAccountId,
      })) ?? undefined
    );
  },
  async updateAuthenticatorCounter(credentialID, newCounter) {
    return await callMutation(api.authAdapter.updateAuthenticatorCounter, {
      credentialID,
      newCounter,
    });
  },
  async updateSession(session: Session) {
    return await callMutation(api.authAdapter.updateSession, {
      session: toDB(session),
    });
  },
  async updateUser(user: User) {
    await callMutation(api.authAdapter.updateUser, { user: toDB(user) });
    return user;
  },
  async useVerificationToken({ identifier, token }) {
    return maybeVerificationTokenFromDB(
      await callMutation(api.authAdapter.useVerificationToken, {
        identifier,
        token,
      }),
    );
  },
};

/// Helpers

function callQuery<Query extends FunctionReference<"query">>(
  query: Query,
  args: Omit<FunctionArgs<Query>, "secret">,
) {
  return fetchQuery(query, addSecret(args) as any);
}

function callMutation<Mutation extends FunctionReference<"mutation">>(
  mutation: Mutation,
  args: Omit<FunctionArgs<Mutation>, "secret">,
) {
  return fetchMutation(mutation, addSecret(args) as any);
}

if (process.env.CONVEX_AUTH_ADAPTER_SECRET === undefined) {
  throw new Error("Missing CONVEX_AUTH_ADAPTER_SECRET environment variable");
}

function addSecret(args: Record<string, any>) {
  return { ...args, secret: process.env.CONVEX_AUTH_ADAPTER_SECRET! };
}

function maybeUserFromDB(user: Doc<"users"> | null) {
  if (user === null) {
    return null;
  }
  return userFromDB(user);
}

function userFromDB(user: Doc<"users">) {
  return {
    ...user,
    id: user._id,
    emailVerified: maybeDate(user.emailVerified),
  };
}

function maybeSessionFromDB(session: Doc<"sessions"> | null) {
  if (session === null) {
    return null;
  }
  return sessionFromDB(session);
}

function sessionFromDB(session: Doc<"sessions">) {
  return { ...session, id: session._id, expires: new Date(session.expires) };
}

function maybeVerificationTokenFromDB(
  verificationToken: Doc<"verificationTokens"> | null,
) {
  if (verificationToken === null) {
    return null;
  }
  return verificationTokenFromDB(verificationToken);
}

function verificationTokenFromDB(verificationToken: Doc<"verificationTokens">) {
  return { ...verificationToken, expires: new Date(verificationToken.expires) };
}

function maybeDate(value: number | undefined) {
  return value === undefined ? null : new Date(value);
}

function toDB<T extends object>(
  obj: T,
): {
  [K in keyof T]: T[K] extends Date
    ? number
    : null extends T[K]
      ? undefined
      : T[K];
} {
  const result: any = {};
  for (const key in obj) {
    const value = obj[key];
    result[key] =
      value instanceof Date
        ? value.getTime()
        : value === null
          ? undefined
          : value;
  }
  return result;
}

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:

import NextAuth from "next-auth"
import { ConvexAdapter } from "@/app/ConvexAdapter";
 
export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [],
  adapter: ConvexAdapter,
})

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:

npm install jose

Generate the keys

Save this code as generateKeys.mjs in your project directory:

generateKeys.mjs
import { exportJWK, exportPKCS8, generateKeyPair } from "jose";

const keys = await generateKeyPair("RS256");
const privateKey = await exportPKCS8(keys.privateKey);
const publicKey = await exportJWK(keys.publicKey);
const jwks = JSON.stringify({ keys: [{ use: "sig", ...publicKey }] });

process.stdout.write(
	`CONVEX_AUTH_PRIVATE_KEY="${privateKey.replace(/\n/g, "\\n")}"`,
);
process.stdout.write("\n\n");
process.stdout.write(`JWKS=${jwks}`);
process.stdout.write("\n");

And run it with Node to generate two KEY=value environment variables:

node generateKeys.mjs

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
import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";

const http = httpRouter();

http.route({
	path: "/.well-known/openid-configuration",
	method: "GET",
	handler: httpAction(async () => {
		return new Response(
			JSON.stringify({
				issuer: process.env.CONVEX_SITE_URL,
				jwks_uri: process.env.CONVEX_SITE_URL + "/.well-known/jwks.json",
				authorization_endpoint:
					process.env.CONVEX_SITE_URL + "/oauth/authorize",
			}),
			{
				status: 200,
				headers: {
					"Content-Type": "application/json",
					"Cache-Control":
						"public, max-age=15, stale-while-revalidate=15, stale-if-error=86400",
				},
			},
		);
	}),
});

http.route({
	path: "/.well-known/jwks.json",
	method: "GET",
	handler: httpAction(async () => {
	  if (process.env.JWKS === undefined) {
      throw new Error("Missing JWKS Convex environment variable");
    }
		return new Response(process.env.JWKS, {
			status: 200,
			headers: {
				"Content-Type": "application/json",
				"Cache-Control":
					"public, max-age=15, stale-while-revalidate=15, stale-if-error=86400",
			},
		});
	}),
});

export default http;

Example in repo

Modify the Convex auth config

Add a new provider to your convex/auth.config.ts:

export default {
  providers: [
    {
      domain: process.env.CONVEX_SITE_URL,
      applicationID: "convex",
    },
  ],
};

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:

import { ConvexAdapter } from "@/app/ConvexAdapter";
import { SignJWT, importPKCS8 } from "jose";
import NextAuth from "next-auth";
	
const CONVEX_SITE_URL = process.env.NEXT_PUBLIC_CONVEX_URL!.replace(
  /.cloud$/,
  ".site",
);

export const { handlers, signIn, signOut, auth } = NextAuth({
  debug: true,
  providers: [
    // ... configured providers
  ],
  adapter: ConvexAdapter,
  callbacks: {
    async session({ session }) {
      const privateKey = await importPKCS8(
        process.env.CONVEX_AUTH_PRIVATE_KEY!,
        "RS256",
      );
      const convexToken = await new SignJWT({
        sub: session.userId,
      })
        .setProtectedHeader({ alg: "RS256" })
        .setIssuedAt()
        .setIssuer(CONVEX_SITE_URL)
        .setAudience("convex")
        .setExpirationTime("1h")
        .sign(privateKey);
      return { ...session, convexToken };
    },
  },
});

declare module "next-auth" {
  interface Session {
    convexToken: string;
  }
}

Provide the JWT to the Convex React client

Replace your ConvexProvider with ConvexProviderWithAuth as shown:

"use client";

import { ConvexProviderWithAuth, ConvexReactClient } from "convex/react";
import { SessionProvider, useSession } from "next-auth/react";
import { Session } from "next-auth";
import { ReactNode, useMemo } from "react";

const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);

export default function ConvexClientProvider({
  children,
  session,
}: {
  children: ReactNode;
  session: Session | null;
}) {
  return (
    <SessionProvider session={session}>
      <ConvexProviderWithAuth client={convex} useAuth={useAuth}>
        {children}
      </ConvexProviderWithAuth>
    </SessionProvider>
  );
}

function useAuth() {
  const { data: session, update } = useSession();

  const convexToken = convexTokenFromSession(session);
  return useMemo(
    () => ({
      isLoading: false,
      isAuthenticated: session !== null,
      fetchAccessToken: async ({
        forceRefreshToken,
      }: {
        forceRefreshToken: boolean;
      }) => {
        if (forceRefreshToken) {
          const session = await update();

          return convexTokenFromSession(session);
        }
        return convexToken;
      },
    }),
    // We only care about the user changes, and don't want to
    // bust the memo when we fetch a new token.
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [JSON.stringify(session?.user)],
  );
}

function convexTokenFromSession(session: Session | null): string | null {
  return session?.convexToken ?? null;
}

Make sure to pass the session object from server to the client component:

App Router example, app/loggedin/layout.tsx (in repo)
import ConvexClientProvider from "@/app/ConvexClientProvider";
import { auth, signOut } from "@/auth";
import { ReactNode } from "react";

export default async function LoggedInLayout({
	children,
}: {
	children: ReactNode;
}) {
	const session = await auth();
	return (
		<>
			<SignOut />
			<ConvexClientProvider session={session}>{children}</ConvexClientProvider>
		</>
	);
}

function SignOut() {/*...*/}
Pages Router example, pages/loggedin/index.tsx (in repo)
import ConvexClientProvider from "@/app/ConvexClientProvider";
import { auth } from "@/auth";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { api } from "@/convex/_generated/api";
import { useMutation, useQuery } from "convex/react";
import { GetServerSideProps, InferGetServerSidePropsType } from "next";
import { Session } from "next-auth";

export const getServerSideProps = (async (ctx) => {
	const session = await auth(ctx);
	return {
		props: {
			session,
		},
	};
}) satisfies GetServerSideProps<{ session: Session | null }>;

export default function Page({
	session,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
	return (
		<ConvexClientProvider session={session}>
			<Content />
		</ConvexClientProvider>
	);
}

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