> ## Documentation Index
> Fetch the complete documentation index at: https://docs.charle.agency/llms.txt
> Use this file to discover all available pages before exploring further.

# Better Auth Configuration

> How CharleOS uses Better Auth for authentication

CharleOS uses [Better Auth](https://better-auth.com/) for modern, type-safe authentication. This page explains the configuration and features.

## Why Better Auth?

<CardGroup cols={2}>
  <Card title="Type-Safe" icon="shield">
    Full TypeScript support with automatic type inference
  </Card>

  <Card title="Framework Agnostic" icon="puzzle">
    Works with any framework, but optimized for Next.js
  </Card>

  <Card title="Database-First" icon="database">
    Uses your existing database (PostgreSQL via Drizzle)
  </Card>

  <Card title="Flexible" icon="wrench">
    Easy to customize and extend
  </Card>
</CardGroup>

## Configuration

### Team Auth (`lib/auth.ts`)

```typescript theme={null}
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";

export const auth = betterAuth({
  database: drizzleAdapter(db, {
    provider: "pg",
    schema,
  }),
  
  socialProviders: {
    google: {
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
      disableSignUp: true,  // Invite-only
      overrideUserInfoOnSignIn: true,  // Update avatar on each login
      mapProfileToUser: async (profile) => {
        // Download and store Google profile picture in R2
        const r2AvatarUrl = await uploadAvatarFromUrl(
          profile.sub,
          profile.picture
        );
        return r2AvatarUrl ? { image: r2AvatarUrl } : {};
      },
    },
  },
  
  user: {
    additionalFields: {
      status: { type: "string", defaultValue: "pending" },
      birthday: { type: "string", required: false },
      jobTitle: { type: "string", required: false },
    },
  },
  
  session: {
    expiresIn: 60 * 60 * 24 * 7,  // 7 days
    updateAge: 60 * 60 * 24,      // Refresh after 1 day
  },
  
  databaseHooks: {
    user: {
      create: {
        before: async (user) => {
          // Auto-assign admin to Luke
          const isAdminEmail = user.email === "luke@charle.co.uk";
          return {
            data: {
              ...user,
              status: isAdminEmail ? "active" : "pending",
            },
          };
        },
        after: async (user) => {
          // Set access level for admin emails
          if (user.email === "luke@charle.co.uk") {
            await db
              .update(userTable)
              .set({ accessLevel: "admin" })
              .where(eq(userTable.id, user.id));
          }
        },
      },
    },
  },
});
```

### Client Portal Auth (`lib/client-auth.ts`)

```typescript theme={null}
import { betterAuth } from "better-auth";

export const clientAuth = betterAuth({
  database: drizzleAdapter(db, {
    provider: "pg",
    schema: {
      user: clientUser,
      session: clientSession,
      account: clientAccount,
      verification: clientVerification,
    },
  }),
  
  emailAndPassword: {
    enabled: true,  // Password-based auth
    requireEmailVerification: false,
  },
  
  session: {
    expiresIn: 60 * 60 * 24 * 30,  // 30 days (longer for clients)
  },
});
```

## API Handler

Better Auth provides a catch-all API handler:

```typescript theme={null}
// app/api/auth/[...all]/route.ts
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";

export const { GET, POST } = toNextJsHandler(auth.handler);
```

This creates all auth endpoints automatically:

* `/api/auth/sign-in/google`
* `/api/auth/sign-out`
* `/api/auth/session`
* `/api/auth/callback/google`

## Client-Side Hooks

### Team Auth Client (`lib/auth-client.ts`)

```typescript theme={null}
import { createAuthClient } from "better-auth/client";

export const { 
  signIn, 
  signOut, 
  useSession 
} = createAuthClient({
  baseURL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL,
});
```

**Usage:**

```typescript theme={null}
"use client";

import { signIn, signOut, useSession } from "@/lib/auth-client";

export function AuthButton() {
  const { data: session, isPending } = useSession();

  if (isPending) return <LoadingSpinner />;

  if (!session) {
    return (
      <button onClick={() => signIn.social({ provider: "google" })}>
        Sign in with Google
      </button>
    );
  }

  return (
    <button onClick={() => signOut()}>
      Sign out
    </button>
  );
}
```

## Features

### Google OAuth Integration

**Configuration:**

1. Create OAuth credentials in [Google Cloud Console](https://console.cloud.google.com/apis/credentials)
2. Set authorized redirect URIs:
   * `http://localhost:3000/api/auth/callback/google` (local)
   * `https://charle.agency/api/auth/callback/google` (production)
3. Add credentials to `.env.local`

**Sign-In Flow:**

1. User clicks "Sign in with Google"
2. Redirected to Google consent screen
3. Google redirects back to `/api/auth/callback/google`
4. Better Auth creates session
5. User redirected to dashboard

**Avatar Handling:**

* Google profile picture is downloaded
* Uploaded to Cloudflare R2
* Stored in `user.image` field
* Updated on every sign-in

### Invite-Only System

```typescript theme={null}
// lib/auth.ts
socialProviders: {
  google: {
    disableSignUp: true,  // Prevents OAuth signup
    // ... rest of config
  },
}
```

**How it works:**

1. Admin creates user account via admin panel (status: "pending")
2. Admin sends invitation email (status: "sent")
3. User clicks "Sign in with Google"
4. Better Auth verifies email exists in database
5. If not found → "No account found" error
6. If found → Session created, status auto-updated to "active"

### Session Management

**Server-Side:**

```typescript theme={null}
import { auth } from "@/lib/auth";
import { headers } from "next/headers";

const { data: session } = await auth.api.getSession({
  headers: headers(),
});

if (!session) {
  redirect("/auth");
}

// Access user data
const user = session.user;
```

**Client-Side:**

```typescript theme={null}
import { useSession } from "@/lib/auth-client";

const { data: session, isPending } = useSession();
```

### Database Hooks

Better Auth allows you to hook into lifecycle events:

```typescript theme={null}
databaseHooks: {
  user: {
    create: {
      before: async (user) => {
        // Modify user before saving
        return { data: { ...user, status: "pending" } };
      },
      after: async (user) => {
        // Run logic after user is created
        await sendWelcomeEmail(user.email);
      },
    },
  },
  session: {
    create: {
      after: async (session) => {
        // Auto-activate users with "sent" status on first login
        const [userData] = await db
          .select({ status: userTable.status })
          .from(userTable)
          .where(eq(userTable.id, session.userId))
          .limit(1);

        if (userData?.status === "sent") {
          await db
            .update(userTable)
            .set({ status: "active", updatedAt: new Date() })
            .where(eq(userTable.id, session.userId));
        }
      },
    },
  },
},
```

**User Status Flow:**

* `pending` → Account created but no invite sent
* `sent` → Invite email sent, awaiting first login
* `active` → User has logged in (auto-activated on first login after invite)

## Security

### CSRF Protection

Better Auth includes built-in CSRF protection:

* All POST requests require valid CSRF token
* Tokens are stored in cookies
* Automatically validated on each request

### Session Security

* Sessions stored server-side in PostgreSQL
* Random, unpredictable session tokens
* Automatic expiry after 7 days
* IP address and user agent tracked

### Password Security (Client Portal)

* Passwords hashed with bcrypt (cost factor 10)
* Never stored in plain text
* Password reset requires email verification

## Customization

### Adding Custom Fields

```typescript theme={null}
user: {
  additionalFields: {
    birthday: {
      type: "string",
      required: false,
      input: true,  // Allow user to update
    },
    slackUserId: {
      type: "string",
      required: false,
      input: false,  // Admin-only field
    },
  },
},
```

### Custom Session Data

```typescript theme={null}
const session = await auth.api.getSession({
  headers: headers(),
});

// Access custom fields
const birthday = session.user.birthday;
const slackUserId = session.user.slackUserId;
```

## Environment Variables

| Variable                      | Purpose                                     |
| ----------------------------- | ------------------------------------------- |
| `BETTER_AUTH_SECRET`          | Secret for encrypting sessions (32+ chars)  |
| `BETTER_AUTH_URL`             | Base URL for auth (`http://localhost:3000`) |
| `NEXT_PUBLIC_BETTER_AUTH_URL` | Public URL (client-side)                    |
| `GOOGLE_CLIENT_ID`            | Google OAuth client ID                      |
| `GOOGLE_CLIENT_SECRET`        | Google OAuth client secret                  |

<Warning>
  **Never commit secrets to git.** Use `.env.local` for local dev and Vercel environment variables for production.
</Warning>

## Troubleshooting

### "No account found" Error

**Cause:** User trying to sign in with Google but email doesn't exist in database.

**Fix:** Admin must create user account first via admin panel.

### CORS Errors

**Cause:** `BETTER_AUTH_URL` doesn't match actual URL.

**Fix:** Verify `.env.local` has correct URL for environment:

* Local: `http://localhost:3000`
* Production: `https://charle.agency`

### Session Not Persisting

**Cause:** Cookies not being set (usually HTTPS/domain issues).

**Fix:**

1. Check `BETTER_AUTH_URL` matches your actual domain
2. Ensure HTTPS in production
3. Clear cookies and try again

## Related Documentation

<CardGroup cols={2}>
  <Card title="Authentication Overview" icon="shield" href="/developer/auth">
    Auth architecture and systems
  </Card>

  <Card title="Permissions" icon="user-lock" href="/developer/auth/permissions">
    Role-based access control
  </Card>

  <Card title="Sessions" icon="clock" href="/developer/auth/sessions">
    Session lifecycle management
  </Card>

  <Card title="Better Auth Docs" icon="book" href="https://better-auth.com/">
    Official Better Auth documentation
  </Card>
</CardGroup>
