Skip to main content

Authentication

Example: Users and Authentication

Authentication allows you to identify users are restrict what data they can see and edit. This can be used to build "logged-in" experiences.

Convex integrates with identity providers implementing the OpenID Connect protocol. Developers pass an ID token to the Convex client in order to reference the user in their queries and mutations.

Authentication in Convex is checked at the function level. New or signed-out users of an app can still subscribe to queries and even make mutations if you let them! Most apps require authentication to load or mutate data and present only a simple login page to logged-out users.

Identity providers

Developers can choose any OpenID-compliant authentication provider (e.g. Google, Github, Facebook) for use with their Convex app.

For the purposes of this guide we'll be using Auth0, which provides "universal login" across a number of supported providers.

Auth0

You'll need to start by creating a free account with Auth0. Next, create a new single-page app from your Auth0 dashboard:

Create a new application

From here, we'll roughly be following Auth0's React Login guide, shown after creating your app.

In your application settings, we need to tell Auth0 what URL origins can log in and log out. For local development, you can just use http://localhost:3000 for the Callback URL, Logout URL, and Allowed Web Origin fields.

At the top of the page you'll also see your Client ID and Domain, which you'll need for the next step.

Configuring Convex

Your Convex project needs to be configured with your chosen identity provider(s) to ensure that only valid identity tokens are accepted.

In your terminal, run the following command to configure a new identity provider:

npx convex auth add

You'll be prompted first for the identity provider's domain and then for your Client ID, both of which are shown in your Auth0 Application Settings.

npx convex auth add

After adding the identity provider, npx convex dev will automatically sync your changes.

Passing authentication tokens to Convex

If your app:

  1. Uses Auth0
  2. Only loads data from Convex when the user is logged in.

then you can use the ConvexProviderWithAuth0 component to show a login or loading component until the user has logged in. This replaces the standard ConvexProvider component.

Using this component requires installing the Auth0 React SDK with:

npm install @auth0/auth0-react

Then you can create the ConvexProviderWithAuth0 by passing it:

  • client: A ConvexReactClient.
  • authInfo: The authentication configuration stored in convex.json in the previous section.
  • [Optional] loggedOut: A React element to render if the user is logged out.
  • [Optional] loading: A React element to render while the authentication information is loading.
import { StrictMode } from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import LoginPage from "./LoginPage";
import { ConvexReactClient } from "convex/react";
import { ConvexProviderWithAuth0 } from "convex/react-auth0";
import convexConfig from "../convex.json";

const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL);
const authInfo = convexConfig.authInfo[0];

ReactDOM.render(
<StrictMode>
<ConvexProviderWithAuth0
client={convex}
authInfo={authInfo}
loggedOut={<LoginPage />}
>
<App />
</ConvexProviderWithAuth0>
</StrictMode>,
document.getElementById("root")
);
tip

If you need to customize the login flow or use another authentication provider, you can use whatever client-side authentication method your authentication provider suggests and call ConvexReactClient.setAuth() to fetch the user's identity token once they've completed the login flow.

Common Authentication Patterns

"Log In" and "Log Out" buttons

Your "Log In" and "Log Out" buttons will depend on your choice of authentication provider. These examples continue to use Auth0 and Auth0's React hooks. You can install the Auth0 React SDK with:

npm install @auth0/auth0-react

Here's an example of a simple "Log In" button:

import { useAuth0 } from "@auth0/auth0-react";

export default function LoginButton() {
const { isLoading, loginWithRedirect } = useAuth0();
return (
<button disabled={isLoading} onClick={loginWithRedirect}>
Log in
</button>
);
}

This button could be passed to the loggedOut prop of ConvexProviderWithAuth0 to render it while the user is logged out.

A "Log Out" button works similarly. Note that it isn't necessary to check isLoading here because the button should only be rendered when the user is logged in.

import { useAuth0 } from "@auth0/auth0-react";

export default function LogoutButton() {
const { logout } = useAuth0();
return (
<button onClick={() => logout({ returnTo: window.location.origin })}>
Log out
</button>
);
}

Accessing user information in React components

If you're using Auth0, you can also access properties of the user like their name from the useAuth0 hook:

import { useAuth0 } from "@auth0/auth0-react";

export default function Badge() {
const { user } = useAuth0();

return (
<p className="badge">
<span>Logged in{user.name ? ` as ${user.name}` : ""}</span>
</p>
);
}

Accessing user information in Convex functions

Within a Convex function, you can access information about the currently logged-in user by using the the auth property of the QueryCtx, MutationCtx, or ActionCtx:

export default mutation(async ({ db, auth }) => {
const identity = await auth.getUserIdentity();
if (!identity) {
throw new Error("Unauthenticated call to mutation");
}
//...
});

Storing users in Convex

To permanently store information about the user, you must save this data as a document. To do this you'll need to:

  1. Define a new users table (if you're using a schema).
  2. Write a new storeUser mutation function that reads user information from auth and stores it in your table.
  3. Call this mutation in a useEffect.
  4. Load the current user in other queries and mutations.

Code examples of these steps are below.

Here is a a users table. Note that this uses an index for efficient database queries.

convex/schema.ts
users: defineTable({
name: s.string(),
tokenIdentifier: s.string(),
}).index("by_token", ["tokenIdentifier"]),

This is an example of a storeUser mutation:

convex/storeUser.js
import { mutation } from "./_generated/server";

/**
* Insert or update the user in a Convex table then return the document's ID.
*
* The `UserIdentity.tokenIdentifier` string is a stable and unique value we use
* to look up identities.
*
* Keep in mind that `UserIdentity` has a number of optional fields, the
* presence of which depends on the identity provider chosen. It's up to the
* application developer to determine which ones are available and to decide
* which of those need to be persisted.
*/
export default mutation(async ({ db, auth }) => {
const identity = await auth.getUserIdentity();
if (!identity) {
throw new Error("Called storeUser without authentication present");
}

// Check if we've already stored this identity before.
const user = await db
.query("users")
.withIndex("by_token", q =>
q.eq("tokenIdentifier", identity.tokenIdentifier)
)
.unique();
if (user !== null) {
// If we've seen this identity before but the name has changed, patch the value.
if (user.name != identity.name) {
await db.patch(user._id, { name: identity.name });
}
return user._id;
}
// If it's a new identity, create a new `User`.
return db.insert("users", {
name: identity.name,
tokenIdentifier: identity.tokenIdentifier,
});
});

You can call this mutation when the user logs in by placing it in a useEffect. This example also stores the currently logged in user's ID in state. Once this state is not null we know the user information has been saved.

const [userId, setUserId] = useState(null);
const storeUser = useMutation("storeUser");
// Call the `storeUser` mutation function to store
// the current user in the `users` table and return the `Id` value.
useEffect(() => {
// Store the user in the database.
// Recall that `storeUser` gets the user information via the `auth`
// object on the server. You don't need to pass anything manually here.
async function createUser() {
const id = await storeUser();
setUserId(id);
}
createUser();
return () => setUserId(null);
}, [storeUser]);

Now you can load this user document in query and mutation functions. This can be useful for referencing the user's document ID in other documents. Note that using the "by_token" index requires you to define that index.

const identity = await auth.getUserIdentity();
if (!identity) {
throw new Error("Unauthenticated call to mutation");
}
// Note: If you don't want to define an index right away, you can use
// db.query("users")
// .filter(q => q.eq(q.field("tokenIdentifier"), identity.tokenIdentifier))
// .unique();
const user = await db
.query("users")
.withIndex("by_token", q =>
q.eq("tokenIdentifier", identity.tokenIdentifier)
)
.unique();
if (!user) {
throw new Error("Unauthenticated call to mutation");
}

App with a logged-out mode

Instead of having a separate logged out page you can render your app regardless of whether the user is logged in:

<ConvexProviderWithAuth0
client={convex}
authInfo={authInfo}
loggedOut={<App />}
>
<App />
</ConvexProviderWithAuth0>

And anywhere in your app code you can use the useAuth0 hook to check whether the user is logged in or not:

const { isAuthenticated } = useAuth0();
return isAuthenticated ? "Logged in" : "Logged out";

Or you could pass down this information via a prop:

<ConvexProviderWithAuth0
client={convex}
authInfo={authInfo}
loggedOut={<App loggedIn={false} />}
>
<App loggedIn={true} />
</ConvexProviderWithAuth0>

In either approach useQuery and useMutation will be available in the logged out state.