Better Auth for Convex

repository·main·Indexed 18 days ago

https://github.com/get-convex/better-auth

An integration between Convex and Better Auth that enables developers to implement authentication systems, including Email/Password, OAuth, and 2FA, within Convex-backed applications.

Tokens
64.6K
Snippets
197
Records
227
Agent score
70%

What's inside @convex-dev/better-auth

  1. Overview of Convex + Better Auth

    main
    Convex + Better Auth is an integration that allows you to use Better Auth with Convex. It provides a framework-agnostic authentication solution that supports various frontend frameworks and includes features like email/password authentication, social sign-on (OAuth), and multi-factor authentication (2FA).
  2. Authentication exceptions in Convex

    main

    While most Better Auth concepts apply to Convex, there are two key differences to keep in mind:

    1. Server-side authentication: Because Convex functions run over websockets and cannot return HTTP responses or set cookies, you cannot use server-side sign-in/out functions. All signing in, signing up, and signing out must be performed from the client using authClient.signIn.* methods.

    2. Schemas and migrations: The Better Auth CLI for schema generation and migrations only applies if you are using a local install. For all other projects, you must use the default schema provided by the Better Auth component, which is preconfigured with supported plugins and cannot be altered.

  3. Make authenticated requests in Svelte

    main

    You can handle authenticated Convex requests in two ways depending on your application's access model:

    Option 1: Conditional Queries (Mixed Access)

    Use this if your app has both public and private content. Use useAuth to check isAuthenticated and return 'skip' to the useQuery hook if the user is not logged in.

    Option 2: Global Authentication (Members-only)

    Use this if almost all data requires authentication. Configure createSvelteAuthClient with options: { expectAuth: true }. This ensures all queries/mutations automatically include the auth token and will not execute until the user is authenticated.

    // Option 1: Conditional
    const auth = useAuth();
    const memberOnlyPosts = useQuery(api.posts.getMemberOnlyPosts, () =>
      auth.isAuthenticated ? {} : "skip"
    );
    
    // Option 2: Global
    createSvelteAuthClient({
      authClient,
      options: {
        expectAuth: true,
      },
    });
  4. How Triggers work in Convex + Better Auth

    main

    Triggers provide a Convex-first way to run mutations in response to changes in your Better Auth schema. Unlike standard Better Auth databaseHooks, Triggers run within the same transaction as the original operation. This ensures atomicity: if a trigger throws an error, the database operation that triggered it will fail and roll back.

    Important Transactional Note: A single Better Auth endpoint or auth.api call can perform multiple database interactions. While throwing an error in a trigger will fail the specific database operation that triggered it, any previous operations within that same API call may still commit.

    // Triggers run in the same transaction as the original operation.
    // Throwing an error in a trigger will stop the original operation from committing.
  5. How to retrieve the authenticated user

    main

    The subject property in the identity token no longer refers to your application's user ID; it now refers to the Better Auth user ID.

    To get your application user's ID, use authComponent.getAuthUser(ctx), which returns the full Better Auth user object.

    Note on Error Handling:

    • authComponent.getAuthUser(ctx) throws an error if the user is not found.
    • Use authComponent.safeGetAuthUser(ctx) if you want the previous behavior where it returns null instead of throwing.
    // Use getAuthUser for required auth
    const userId = (await authComponent.getAuthUser(ctx))?.userId;
    
    // Use safeGetAuthUser for optional auth
    const userMetadata = await authComponent.safeGetAuthUser(ctx);
    if (!userMetadata) {
      return null;
    }
  6. Access Better Auth component data from other functions

    main

    Functions defined within a component directory can access the component's tables directly. These functions can be called from outside the component using ctx.runQuery, ctx.runMutation, or ctx.runAction.

    Key Rules:

    • Visibility: Functions exported by a component are never exposed to the internet, even if they are marked as public. Only functions defined in the main app/component are internet-facing.
    • Typing: When calling a component function from outside, you must provide a returns validator in the component function so that the return type can be correctly inferred by the caller.
    • Internal Functions: Functions marked as internalQuery or internalMutation within a component are not accessible from outside the component.
    import { query } from "./_generated/server";
    import { doc } from "convex-helpers/validators";
    import schema from "./schema";
    import { v } from "convex/values";
    
    // This is accessible from outside the component
    export const someFunction = query({
      args: { sessionId: v.id("session") },
      // Add a return validator so the return value is typed when
      // called from outside the component.
      returns: v.union(v.null(), doc(schema, "session")),
      handler: async (ctx, args) => {
        return await ctx.db.get(args.sessionId);
      },
    });
    import { query } from "./_generated/server";
    import { components } from "./_generated/api";
    import { v } from "convex/values";
    
    export const someFunction = query({
      args: { sessionId: v.id("session") },
      handler: async (ctx, args) => {
        return await ctx.runQuery(components.betterAuth.someFile.someFunction, {
          sessionId: args.sessionId,
        });
      },
    });
  7. Getting Started with Convex + Better Auth

    main

    To use Convex with Better Auth, follow these general setup steps:

    1. Prerequisites: Ensure you have a working environment for your chosen framework.
    2. Create a Convex project: Initialize a new Convex project to host your backend functions and database.
    3. Run convex dev: Start the Convex development server to sync your schema and functions.
    4. Select your framework: Follow the specific installation and configuration guide tailored to your frontend framework.

    Installation steps vary significantly depending on whether you are using a Single Page Application (SPA), a meta-framework, or a mobile environment.

  8. Handle Sign Out when using `expectAuth: true`

    main

    When expectAuth: true is enabled, it only affects the state before the initial authentication. If a user signs out and signs back in, authenticated queries may be called before the authentication state is ready, causing errors.

    Recommendation: Reload the page on sign out. This ensures the application state is reset correctly. For apps with authentication-based redirects, the reload will trigger the necessary unauthenticated redirect.

    import { authClient } from "~/lib/auth-client";
    
    const handleSignOut = async () => {
      await authClient.signOut({
        fetchOptions: {
          onSuccess: () => {
            location.reload();
          },
        },
      });
    };
    import { authClient } from "~/lib/auth-client";
    
    const handleSignOut = async () => {
      await authClient.signOut({
        fetchOptions: {
          onSuccess: () => {
            location.reload();
          },
        },
      });
    };
  9. Use Better Auth server methods with `auth.api`

    main

    You can invoke Better Auth server-side auth.api methods within Convex mutations or actions. Since many of these methods require an authenticated user, you must pass the request headers so session cookies can be parsed and validated.

    Use the authComponent.getAuth(createAuth, ctx) method to conveniently retrieve both the auth object and the necessary headers from the Convex context.

    export const updateUserPassword = mutation({
      args: {
        currentPassword: v.string(),
        newPassword: v.string(),
      },
      handler: async (ctx, args) => {
        // The `getAuth` method provides both the auth object and headers for convenience.
        const { auth, headers } = await authComponent.getAuth(createAuth, ctx);
        await auth.api.changePassword({
          body: {
            currentPassword: args.currentPassword,
            newPassword: args.newPassword,
          },
          headers,
        });
      },
    });