![Profile image](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Fts10onj4%2Fproduction%2Fe8231f5314b107688be9636bd8a855e820cbca20-512x512.png&w=96&q=75)
Introducing Convex Auth
![padlock icon to represent auth](/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Fts10onj4%2Fproduction%2Fa1bb13a6b67d4791bd1c91cc6e3315191240bbea-1452x956.png&w=3840&q=75)
Convex Auth is a library for implementing authentication using your Convex backend.
Check it out in the Convex docs to get started or play with the example demo (source).
This article is based on the launch video.
Motivation
Convex is designed for building multi-user applications. And if your app has users, it needs authentication. But authentication is a really complex component of a full-stack app. For this reason we have been recommending the built-in integrations with mature authentication platforms like Clerk and Auth0.
Yet many developers have been asking for a self-hosted solution. The auth platforms I mentioned have a ton of features but they also store the authentication data. This complicates your app, as that data has to be somehow synced or shared with your backend and database.
This is a surmountable challenge, but maybe you’re just getting started and your app doesn’t need every authentication feature. You’d rather have more control over the data and a simpler architecture to build on top of.
Functionality
Convex Auth enables you to build such a solution. It is inspired by the excellent Auth.js and Lucia libraries, and reuses some of their implementation. It is also similar in its capabilities to auth solutions such as Firebase Auth and Supabase Auth (although it doesn’t have all of their features yet).
In the demo you can see the various sign-in methods you can implement with Convex Auth. It supports sign-in via OAuth, magic links, one-time-passwords and normal email and password combination. You can use any of the 80 OAuth providers supported by Auth.js.
Code deep dive
Server configuration
The main configuration file is auth.ts
in your convex directory, which configures the available authentication methods:
import GitHub from "@auth/core/providers/github";
import Google from "@auth/core/providers/google";
import Resend from "@auth/core/providers/resend";
import { Password } from "@convex-dev/auth/providers/Password";
export const { auth, signIn, signOut, store } = convexAuth({
providers: [GitHub, Google, Resend, Password],
});
Your schema.ts
must also include the tables used by the library, including the users
table. This is because the library uses indexes on these tables for efficient lookups:
import { authTables } from "@convex-dev/auth/server";
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
...authTables,
// Your other tables...
});
There is additional configuration in auth.config.ts
and https.ts
, but you won’t need to touch these. See Manual Setup for more details if you’re interested.
Frontend configuration
On the frontend, instead of using ConvexProvider
, the app is wrapped in ConvexAuthProvider
. Then in the App
root component, the Authenticated
and Unauthenticated
components (from convex/react
) are used to render different UI based on the authentication state. When not authenticated, your app can render the sign-in form:
import { Content } from "@/Content";
import { SignInForm } from "@/auth/SignInForm";
import { Authenticated, Unauthenticated } from "convex/react";
export default function App() {
return (
<>
<Authenticated>
<Content />
</Authenticated>
<Unauthenticated>
<SignInForm />
</Unauthenticated>
</>
);
}
The key part of the sign-in UI is calling the signIn
function with the name of one of the authentication methods configured in auth.ts
. For example here’s a form that sends the user a magic link:
import { useAuthActions } from "@convex-dev/auth/react";
export function SignInWithMagicLink() {
const { signIn } = useAuthActions();
return (
<form
className="flex flex-col"
onSubmit={(event) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
void signIn("resend", formData);
}}
>
<label htmlFor="email">Email</label>
<input name="email" id="email" className="mb-4" autoComplete="email" />
<button type="submit">Send sign-in link</button>
</form>
);
}
Handling authentication
Finally, let’s look at how the authentication state is used to power the signed-in experience. The auth.ts
file exports an auth
helper, which has methods for retrieving the current user and session ID. Using these methods, we can return the information about the current user back to the client:
import { query } from "./_generated/server";
import { auth } from "./auth";
export const viewer = query({
args: {},
handler: async (ctx) => {
const userId = await auth.getUserId(ctx);
return userId !== null ? ctx.db.get(userId) : null;
},
});
As well as enforce that certain functions can only be called by signed-in users:
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
import { auth } from "./auth";
export const send = mutation({
args: { body: v.string() },
handler: async (ctx, { body }) => {
const userId = await auth.getUserId(ctx);
if (userId === null) {
throw new Error("Not signed in");
}
// Send a new message.
await ctx.db.insert("messages", { body, userId });
},
});
Conclusion
And that’s all it takes to get self-hosted auth to work. From here I recommend you read through the docs. They go into detail on how to implement the various authentication methods and on the trade-offs between them. I hope you’ll find the library useful. Please let us know what you think on our discord.