npm package: @ryantamulevicz/convex-better-auth-astro
Description: Astro integration helpers for combining better-auth with Convex
Author: Ryan Tamulevicz
Latest version: 0.1.0

# @ryantamulevicz/convex-better-auth-astro

Utilities for wiring [better-auth](https://github.com/better-auth/better-auth) into an [Astro](https://astro.build/) project that talks to [Convex](https://convex.dev/).

## Setup

It is expected that you have an Astro project started with Convex installed in your project to begin!

Install dependencies
```bash
pnpm add convex@latest @convex-dev/better-auth
pnpm add better-auth@1.3.27 --save-exact
pnpm add @ryantamulevicz/convex-better-auth-astro
```

Make sure you've initialized a Convex project inside the repository (for example `npx convex init`).

Pick the client-side library (Currently React or Svelte) you plan to use. Follow the matching section below after you finish the Convex setup steps.

Add the following to `convex/convex.config.ts` so Convex registers the Better Auth integration:

```ts
import { defineApp } from "convex/server";
import betterAuth from "@convex-dev/better-auth/convex.config";

const app = defineApp();
app.use(betterAuth);

export default app;
```

Update your Better Auth config (for example in `auth.config.ts`) to align domains with Convex:

```ts
export default {
  providers: [
    {
      domain: process.env.CONVEX_SITE_URL,
      applicationID: "convex",
    },
  ],
};
```

Generate and store a Better Auth secret in your Convex environment (replace the variable if you manage secrets differently):

```bash
npx convex env set BETTER_AUTH_SECRET=$(openssl rand -base64 32)
```

Set the local site URL so Convex knows where your Astro app runs:

```bash
npx convex env set SITE_URL http://localhost:3000
```

Add the Convex defaults generated by `npx convex dev` to your `.env.local` so Astro can read them:

```bash
# Deployment used by `npx convex dev`
CONVEX_DEPLOYMENT=dev:adjective-animal-123 # team: team-name, project: project-name
PUBLIC_CONVEX_URL=https://adjective-animal-123.convex.cloud
# Same as PUBLIC_CONVEX_URL but ends in .site
PUBLIC_CONVEX_SITE_URL=https://adjective-animal-123.convex.site
# Your local site URL
SITE_URL=http://localhost:4321
```

Update `SITE_URL` and `PUBLIC_CONVEX_SITE_URL` to match your local dev server or hosted deployment before starting Astro.

Create `convex/auth.ts` so Better Auth can run on Convex:

```ts
import { createClient, type GenericCtx } from "@convex-dev/better-auth";
import { convex } from "@convex-dev/better-auth/plugins";
import { components } from "./_generated/api";
import { DataModel } from "./_generated/dataModel";
import { query } from "./_generated/server";
import { betterAuth } from "better-auth";

const siteUrl = process.env.SITE_URL!;

// The component client has methods needed for integrating Convex with Better Auth,
// as well as helper methods for general use.
export const authComponent = createClient<DataModel>(components.betterAuth);

export const createAuth = (
  ctx: GenericCtx<DataModel>,
  { optionsOnly } = { optionsOnly: false },
) => {
  return betterAuth({
    // disable logging when createAuth is called just to generate options.
    // this is not required, but there's a lot of noise in logs without it.
    logger: {
      disabled: optionsOnly,
    },
    baseURL: siteUrl,
    database: authComponent.adapter(ctx),
    // Configure simple, non-verified email/password to get started
    emailAndPassword: {
      enabled: true,
      requireEmailVerification: false,
    },
    plugins: [
      // The Convex plugin is required for Convex compatibility
      convex(),
    ],
  });
};

// Example function for getting the current user
// Feel free to edit, omit, etc.
export const getCurrentUser = query({
  args: {},
  handler: async (ctx) => {
    return authComponent.getAuthUser(ctx);
  },
});
```

Mount the Better Auth HTTP handlers by creating `convex/http.ts`:

```ts
import { httpRouter } from "convex/server";
import { authComponent, createAuth } from "./auth";

const http = httpRouter();
authComponent.registerRoutes(http, createAuth);

export default http;
```
Mount the Astro API route that proxies requests to Convex authentication by creating `src/pages/api/auth/[...all]/route.ts`:

```ts
import { astroHandler } from "@ryantamulevicz/convex-better-auth-astro";
import type { APIRoute } from "astro";

const convexSiteUrl = import.meta.env.PUBLIC_CONVEX_SITE_URL;
const handler = astroHandler(convexSiteUrl ? { convexSiteUrl } : undefined);

export const prerender = false;

export const ALL: APIRoute = async (context) => {
  return handler(context);
};
```

## React

### Setup (if you're using React for client-side UI)

Wire up the frontend helpers in this order:

Create `src/lib/auth-client.ts` to access Better Auth from your Astro front end:

```ts
import { createAuthClient } from "better-auth/react";
import { convexClient } from "@convex-dev/better-auth/client/plugins";

export const authClient = createAuthClient({
  plugins: [convexClient()],
});
```

Add a light helper in `src/lib/react-convex.tsx` to wrap components with both Convex and Better Auth context:

```ts
import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react";
import { ConvexReactClient } from "convex/react";
import type { FunctionComponent, JSX } from "react";
import { authClient } from "./auth-client";

// Initialized once so all components share the same client.
const convex = new ConvexReactClient(
  import.meta.env.PUBLIC_CONVEX_URL as string,
  {
    // Optionally pause queries until the user is authenticated
    expectAuth: true,
  }
);

export function withConvexProvider<Props extends JSX.IntrinsicAttributes>(
  Component: FunctionComponent<Props>
) {
  return function WithConvexProvider(props: Props) {
    return (
      <ConvexBetterAuthProvider client={convex} authClient={authClient}>
        <Component {...props} />
      </ConvexBetterAuthProvider>
    );
  };
}
```

### Usage

Wrap only the React components that Astro mounts (for example via `client:only`) with the helper when you export them:

```tsx
import { withConvexProvider } from "@/lib/react-convex";

export default withConvexProvider(function AccountMenu() {
  return (
    <>
      Menu
    </>
  );
});
```

Once React or Svelte setup is complete you can continue onto the server-side setup section below. You can run Astro + your framework without any server-side wiring (no SSR) and everything works, but enabling middleware or server actions requires a bit more setup.

## Svelte

### Setup (if you're using Svelte for client-side UI)

Add the Convex client setup once near the root by creating `src/lib/auth-client.svelte`:

```svelte
<script lang="ts">
  import { setupConvex } from "convex-svelte";

  // Call this once near the root so children can use Convex hooks
  setupConvex(import.meta.env.PUBLIC_CONVEX_URL as string);
</script>

<slot />
```

Create `src/lib/svelte-convex.svelte` (used for layouts) with the same setup:

```svelte
<script lang="ts">
  import { setupConvex } from "convex-svelte";

  // Call this once near the root so children can use Convex hooks
  setupConvex(import.meta.env.PUBLIC_CONVEX_URL as string);
</script>

<slot />
```

### Usage

Wrap your Astro layout (or top-level Svelte entry point) with the setup component so all children get Convex context, for example in `src/layouts/Layout.astro`:

```astro
---
import "../styles/global.css";
import ConvexSetup from "../../lib/svelte/svelte-convex.svelte";
---
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="generator" content={Astro.generator} />
    <title>Svelte Convex Better Auth Astro</title>
  </head>
  <body>
    <ConvexSetup>
      <slot />
    </ConvexSetup>
  </body>
</html>

<style>
  html,
  body {
    margin: 0;
    width: 100%;
    height: 100%;
  }
</style>
```

## Server helpers (middleware, actions, Convex access)

All server surfaces share the same Better Auth helpers so you only initialize `createAuth` once.

1. Create `src/lib/auth-server.ts` and bind the helpers:

   ```ts
   import { createAstroAuthHelpers } from "@ryantamulevicz/convex-better-auth-astro";
   import { createAuth } from "../convex/auth";

   const { getToken, getAuth, setupFetchClient } = createAstroAuthHelpers(createAuth);

   export { getToken, getAuth, setupFetchClient };
   ```

2. Use `getToken` (and optionally `fetchSession`) in Astro middleware so every request has the current auth context:

   ```ts
   import { defineMiddleware } from "astro:middleware";
   import { fetchSession } from "@ryantamulevicz/convex-better-auth-astro";
   import { getToken } from "./lib/auth-server";

   const convexSiteUrl = import.meta.env.PUBLIC_CONVEX_SITE_URL;

   export const onRequest = defineMiddleware(async (context, next) => {
     try {
       const token = getToken(context);
       const { session } = await fetchSession(
         context.request,
         convexSiteUrl ? { convexSiteUrl } : undefined
       );
       context.locals.user = session?.user ?? null;
       context.locals.session = session?.session ?? null;
       context.locals.convexToken = token ?? null;
     } catch {
       context.locals.user = null;
       context.locals.session = null;
       context.locals.convexToken = null;
     }

     return next();
   });
   ```

3. Inside an Astro Action, call `setupFetchClient` to run Convex mutations or queries on behalf of the authenticated user:

   ```ts
   import { defineAction } from "astro:actions";
   import { z } from "astro:schema";
   import { api } from "../convex/_generated/api";
   import { setupFetchClient } from "../lib/auth-server";

   export const updateUsername = defineAction({
     input: z.object({ name: z.string() }),
     handler: async (input, context) => {
       const convex = await setupFetchClient(context, {
         convexUrl: import.meta.env.PUBLIC_CONVEX_URL as string,
       });
       await convex.fetchMutation(api.users.updateUsername, {
         username: input.name,
       });
       return { success: true };
     },
   });
   ```

   When you need to call the action from a React island (see `example/src/components/react/Hello.tsx`), import `actions` from `astro:actions` and invoke it like this:

   ```tsx
   import { actions } from "astro:actions";

   await actions.updateUsername({ name: sanitized });
   ```

4. Define the Convex mutation that the action calls (for example `convex/users.ts`):

   ```ts
   import { mutation } from "./_generated/server";
   import { v } from "convex/values";
   import { createAuth, authComponent } from "./auth";

   export const updateUsername = mutation({
     args: { username: v.string() },
     handler: async (ctx, args) => {
       const { auth, headers } = await authComponent.getAuth(createAuth, ctx);
       await auth.api.updateUser({
         body: { name: args.username },
         headers,
       });
     },
   });
   ```

These four pieces give you a consistent server-side surface: middleware populates locals, actions call Convex with `setupFetchClient`, and Convex mutations reuse Better Auth context when talking back to Better Auth.