Authentication vs Authorization
Let’s start with two words that are often used interchangeably but mean different things. Most of the confusion students have about “auth” comes from mixing these two up, so we will sort them out before we write any code.
The Two Questions of Auth
Every time a user tries to do something in a multi-user app, the backend has to answer two questions:
-
Who are you? — This is authentication. It is about identity. When you “Sign in” to an app, you are proving your identity to the system. The system might need this just to say “Hello, Alice!” in the UI, or it might need it to know which data belongs to you. But authentication is just about establishing who you are.
-
Are you allowed to do this? — This is authorization. It is about permission. Knowing you are “Alice” does not automatically mean Alice is allowed to delete Bob’s project. Authorization is the set of rules that decides which actions an identity is allowed to perform.
A short way to remember the difference: authentication is a noun question (“who?”), authorization is a verb question (“may I?”).
You can have authentication without authorization. A website that lets you sign in but gives every signed-in user the exact same abilities only needs the first question answered. You cannot reasonably have authorization without authentication. Rules about “who can do what” do not mean much if the system does not know who anyone is.
Among software developers, “auth” is often used as a catch-all term that covers both authentication and authorization.
Auth in a Full-Stack App
A common mistake is to think of auth as something that lives entirely in one place. In a full-stack app, authentication and authorization both show up on both the frontend and the backend, but they do different work on each side:
-
On the frontend, auth is about user experience. If you are not signed in, we hide the
New Projectbutton. If you are not a project’s owner, we hide theDelete projectbutton. These checks mean users do not see controls they cannot use. -
On the backend, auth is about security. The frontend hiding a button is not a security boundary. Anyone who opens their browser’s devtools can call the mutation directly, or send a raw request with
curl. The real enforcement must happen on the server, inside every mutation that modifies data.
This means we will almost always implement each rule twice: once in the UI for user experience, and once in the backend (e.g., Convex functions) for safety. The check in the UI is there so a user does not see a control they cannot use. The check on the server is there so the rule is actually enforced. We need both. Throughout this chapter, whenever we add a new authorization rule, we will add it on the server first and then reflect it in the UI.
How Convex Helps
Convex provides a library (currently in beta) called Convex Auth that makes it easy to implement authentication on Convex platform. It offers several authentication methods including email/password, magic links, and OAuth providers like GitHub. For this chapter, we will use GitHub OAuth since it is quick to set up and does not require users to create a new password. Because Convex Auth stores the user data in the same database as your app data, you can easily query it from your Convex functions.
We need to implement authorization rules ourselves. On the backend, every query and mutation receives a ctx object with an auth field. Inside any function, we can call await getAuthUserId(ctx) to get the ID of the signed-in user — or null if the caller is not signed in. That is how we answer the “who are you?” question. Once we have the ID, answering “are you allowed to do this?” is just regular TypeScript: read the document, compare fields, throw an error if the rule is violated.
On the frontend, Convex Auth provides React hooks that mirror the hooks we already know: useConvexAuth() tells us whether the current user is signed in, and useAuthActions() gives us signIn() and signOut() functions. Wrapping the app in <ConvexAuthProvider> in main.tsx makes the auth state available throughout the app. This is very similar to how we wrapped the app in <ConvexProvider> in the previous chapters to get access to the Convex client.