Bright ideas and techniques for building with Convex.
Profile image
Michal Srb
10 days ago

Convex with Auth.js (NextAuth)

Convex with Auth.js

Many developers have asked us for a guide for using Auth.js with Convex. Here it is!

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

Introduction to authentication and Auth.js

Auth.js (previously called NextAuth) is a library for implementing user authentication on your Next.js, SvelteKit, or Express server. We’re going to focus on Next.js for this guide.

Auth.js makes it easy to implement several authentication methods. The most popular ones, and the ones used in the example repo, are:

  1. Integrating OAuth providers (Github, Google, Facebook, etc.) - the docs include a list of 80 providers you can leverage. Users of your app are redirected to the OAuth provider, sign in with them, and then are redirected back to your app. This is a great option to get started with because it doesn’t require any data stored in your Convex database related to the authentication process itself.
  2. Sending “magic” links, which include a unique verification token, to your users’ email address. The user then clicks the link to open the app and sign in. You must store the verification links in a database, and you can use your Convex database for that.

Nextauth sign in screen example

When using Auth.js with Convex, there are two servers involved: Your Next.js server, and your Convex backend. Since you fully control both servers, you can treat them as a single authentication issuer. This will significantly simplify the setup. Here’s a picture that illustrates this approach:

illustration of using Next.js and Convex with Auth.js

Getting started

We’ll assume that you have a working Next.js app with Convex. If you don’t, follow our quickstart to get one set up.

Set up Auth.js

Follow the Installation instructions for Auth.js.

Then pick and configure providers as shown on the Authentication page. Don’t worry about setting up a database adapter, as you’ll do that in the next step.

Set up the Convex database adapter

Follow the Convex Adapter for Auth.js article to install and set up Convex as your database adapter, and to authenticate your Convex functions.

Test it out!

You should have a working authentication with Convex and Auth.js now. When you sign in to your app, you should see a document created in the "users" table on your Convex dashboard. You can test that the authentication of function calls works by executing ctx.auth.getUserIdentity() from a query or mutation called from your app:

convex/myFunctions.ts
import { query } from "./_generated/server";
import { Id } from "./_generated/dataModel";

export const viewerInfo = query({
  args: {},
  handler: async (ctx) => {
    // Assuming this query is only called after authentication
    const { subject: userId } = (await ctx.auth.getUserIdentity())!;
    return await ctx.db.get(userId as Id<"users">);
  },
});

After the function is called, check your Convex logs to see the user document printed. If you don’t, you can also check your browser logs, the steps above, and as a last resort follow the authentication debugging guide.

Tips

Splitting up your app between signed in and signed out routes

The simplest approach to using authentication is to split your app’s pages between those which require authentication and those which don’t. In the example repo, we used "loggedin" as the route for the authenticated portion of the app, and we left the home "/" route as the unauthenticated entry point.

To do this, you can redirect the user after signing in to the authenticated portion, by adding redirectTo option to the signIn call on your button:

app/SignIn.tsx
import { signIn } from "@/auth";

export function SignIn() {
  return (
    <form
      action={async () => {
        "use server";
        await signIn(undefined, { redirectTo: "/loggedin" });
      }}
    >
      <button type="submit">Sign in with GitHub</button>
    </form>
  );
}

Similarly you can redirect the user back on sign-out:

app/SignOut.tsx
import { signOut } from "@/auth";

export function SignOut() {
  return (
    <form
      action={async () => {
        "use server";
        await signOut({ redirectTo: "/" });
      }}
    >
      <button type="submit">Sign out</button>
    </form>
  );
}

And to redirect unauthenticated users, you can change the [middleware.ts](https://github.com/get-convex/convex-nextauth-template/blob/main/middleware.ts) file:

middleware.ts
import { auth } from "@/auth";

export default auth((req) => {
  if (!req.auth) {
    const url = req.url.replace(req.nextUrl.pathname, "/");
    return Response.redirect(url);
  }
});

export const config = {
  matcher: ["/(loggedin.*)"],
};

This middleware will execute for all routes under "loggedin", and if the user isn’t logged in they will be redirected back to the home page.

Skipping sign-in when the user is already logged in

With the implementation shown above if the user comes back to the home screen and clicks on Sign in they will see the sign in screen again, even if they have previously signed in in the same browser. You can avoid this with a small change to the button callback:

app/SignIn.tsx
import { signIn, auth } from "@/auth";

export function SignIn() {
  return (
    <form
      action={async () => {
        "use server";

        // Skip sign-in screen if the user is already signed in
        if ((await auth()) !== null) {
          redirect("/loggedin");
        }

        await signIn(undefined, { redirectTo: "/loggedin" });
      }}
    >
      <Button type="submit">Sign in</Button>
    </form>
  );
}

Working with sessions

Right now the JWT only stores the user’s Convex ID. You might also want to access the current session ID. Your app could use it to implement logic tied to the current session, such as session(device)-specific settings or say, separate shopping carts.

Being able to access the current session’s ID in your Convex functions is a matter of passing it to the JWT in auth.ts:

auth.ts
      const convexToken = await new SignJWT({
        // These fields will be available on `ctx.auth.getUserIdentity()`
        // in Convex functions:
        sub: `${session.userId};${(session as any)._id}`,
      })

We chose to use the sub field to pass both the user and the session ID to leave the other fields available for their standard meaning.

With this change, you can get both IDs in your convex functions. The example repo defines helpers that can be called from Convex queries, mutations and actions:

convex/auth.ts
import { Auth } from "convex/server";
import { Id } from "./_generated/dataModel";

export async function getSessionId(ctx: { auth: Auth }) {
  const identity = await ctx.auth.getUserIdentity();
  if (identity === null) {
    return null;
  }
  const [, sessionId] = identity.subject.split(";");
  return sessionId as Id<"sessions">;
}

export async function getUserId(ctx: { auth: Auth }) {
  const identity = await ctx.auth.getUserIdentity();
  if (identity === null) {
    return null;
  }
  const [userId] = identity.subject.split(";");
  return userId as Id<"users">;
}

Note on security

The Convex team does not guarantee the security of this setup. All the code is yours / open source and it is your responsibility to ensure it matches your requirements. For any security issues in Auth.js refer to their docs and for any issues with Convex itself please reach out to us at security@convex.dev.

Wrap

We hope that this enables you to finally use Auth.js with your favorite backend. Let us know if you run into any issues on our discord.

Build in minutes, scale forever.

Convex is the backend application platform with everything you need to build your project. Cloud functions, a database, file storage, scheduling, search, and realtime updates fit together seamlessly.

Get started