> ## 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.

# Sessions

> Session lifecycle and management in CharleOS

CharleOS uses server-side sessions stored in PostgreSQL. This page explains how sessions work and how to manage them.

## Session Architecture

### Storage

Sessions are stored in the database, not in JWTs:

**Benefits:**

* Can be revoked instantly
* Track session metadata (IP, user agent)
* No token size limits
* More secure (no client-side storage)

**Tables:**

* `session` - Team member sessions
* `client_session` - Client portal sessions

## Session Lifecycle

### Creation

Sessions are created when a user signs in:

```
User signs in
  ↓
Better Auth creates session record
  ↓
Session token stored in HTTP-only cookie
  ↓
User redirected to dashboard
```

**Session Record:**

```typescript theme={null}
{
  id: "cuid-session-id",
  userId: "user-123",
  token: "random-secure-token",
  expiresAt: "2024-01-30T12:00:00Z",  // 7 days from creation
  ipAddress: "192.168.1.1",
  userAgent: "Mozilla/5.0...",
  createdAt: "2024-01-23T12:00:00Z",
  updatedAt: "2024-01-23T12:00:00Z"
}
```

### Validation

On every request, the session is validated:

<Steps>
  <Step title="Extract Token">
    Session token read from HTTP-only cookie
  </Step>

  <Step title="Database Lookup">
    Query `session` table for matching token
  </Step>

  <Step title="Check Expiry">
    Verify `expiresAt` is in the future
  </Step>

  <Step title="Load User">
    Join with `user` table to get user data
  </Step>

  <Step title="Return Session">
    Return session object with user data
  </Step>
</Steps>

### Refresh

Sessions are automatically refreshed on activity:

```typescript theme={null}
// lib/auth.ts
session: {
  expiresIn: 60 * 60 * 24 * 7,  // 7 days
  updateAge: 60 * 60 * 24,      // Refresh after 1 day
}
```

**How it works:**

* If session is older than 1 day, it's refreshed
* `updatedAt` timestamp updated
* `expiresAt` extended by 7 days
* User stays logged in without re-authentication

### Expiry

Sessions expire automatically:

* **Team sessions**: 7 days from last activity
* **Client sessions**: 30 days from last activity

**Cleanup:**

* Expired sessions are periodically purged from database
* Better Auth handles this automatically

### Revocation

Sessions can be revoked (sign out):

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

// Sign out current user
await signOut();
```

**What happens:**

1. Session record deleted from database
2. Cookie cleared
3. User redirected to sign-in page

## Server-Side Session Access

### In Server Components

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

export default async function Page() {
  const { data: session } = await auth.api.getSession({
    headers: headers(),
  });

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

  // Access user data
  const user = session.user;
  console.log(user.name, user.email, user.accessLevel);

  return <div>Welcome, {user.name}!</div>;
}
```

### In API Routes

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

export async function GET(req: Request) {
  const { data: session } = await auth.api.getSession({
    headers: headers(),
  });

  if (!session) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }

  // Use session data
  const userId = session.user.id;

  return Response.json({ user: session.user });
}
```

### In Server Actions

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

import { auth } from "@/lib/auth";
import { headers } from "next/headers";

export async function updateProfile(formData: FormData) {
  const { data: session } = await auth.api.getSession({
    headers: headers(),
  });

  if (!session) {
    throw new Error("Not authenticated");
  }

  // Update user profile...
}
```

## Client-Side Session Access

### Using React Hook

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

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

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

  if (isPending) {
    return <LoadingSpinner />;
  }

  if (!session) {
    return <SignInButton />;
  }

  return (
    <div>
      <img src={session.user.image} alt={session.user.name} />
      <span>{session.user.name}</span>
    </div>
  );
}
```

### Session Object Structure

```typescript theme={null}
{
  user: {
    id: string;
    name: string;
    email: string;
    image: string | null;
    // Additional fields
    status: "pending" | "sent" | "active" | "deactivated";
    accessLevel: "admin" | "manager" | "staff";
    workType: "development" | "design" | "pm" | "csm" | "qa" | "sdr" | null;
    hasBillableCapacity: boolean;
    hasExecutiveAccess: boolean;
    birthday: string | null;
    jobTitle: string | null;
  };
  session: {
    id: string;
    userId: string;
    expiresAt: string;
    createdAt: string;
    updatedAt: string;
  };
}
```

## Session Security

### HTTP-Only Cookies

Session tokens are stored in HTTP-only cookies:

**Benefits:**

* Not accessible via JavaScript
* Protected from XSS attacks
* Automatically sent with requests
* Secure flag in production (HTTPS only)

**Cookie Configuration:**

```typescript theme={null}
// Set by Better Auth automatically
{
  name: "auth.session_token",
  httpOnly: true,
  secure: process.env.NODE_ENV === "production",
  sameSite: "lax",
  path: "/",
  maxAge: 60 * 60 * 24 * 7  // 7 days
}
```

### CSRF Protection

Better Auth includes built-in CSRF protection:

* All state-changing requests require valid CSRF token
* Tokens are stored in separate cookie
* Automatically validated on each request

### Session Metadata

CharleOS tracks additional session metadata:

| Field       | Purpose                         |
| ----------- | ------------------------------- |
| `ipAddress` | Track IP for security audits    |
| `userAgent` | Identify device/browser         |
| `createdAt` | When session was created        |
| `updatedAt` | When session was last refreshed |

## Multi-Device Sessions

Users can be signed in on multiple devices:

```typescript theme={null}
// Get all active sessions for a user
const activeSessions = await db
  .select()
  .from(session)
  .where(eq(session.userId, userId))
  .where(gt(session.expiresAt, new Date()));

console.log(`User has ${activeSessions.length} active sessions`);
```

### Revoking All Sessions

```typescript theme={null}
// Sign out from all devices
await db
  .delete(session)
  .where(eq(session.userId, userId));
```

## Client Portal Sessions

Client portal sessions work identically but:

* Use `client_session` table
* Longer expiry (30 days vs 7 days)
* Separate cookie (`client_auth.session_token`)

```typescript theme={null}
// Client portal session access
import { clientAuth } from "@/lib/client-auth";

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

if (!session) {
  redirect("/client-portal/sign-in");
}

// Access client user data
const clientUser = session.user;
const clientId = clientUser.clientId;
```

## Session Management UI

### Current Session Info

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

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

export function SessionInfo() {
  const { data: session } = useSession();

  if (!session) return null;

  return (
    <div>
      <p>Signed in as: {session.user.email}</p>
      <p>Session expires: {new Date(session.session.expiresAt).toLocaleString()}</p>
    </div>
  );
}
```

### Sign Out Button

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

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

export function SignOutButton() {
  return (
    <button onClick={() => signOut()}>
      Sign Out
    </button>
  );
}
```

## Troubleshooting

### Session Not Persisting

**Symptoms:**

* User signed in but immediately signed out
* Cookies not being set

**Causes:**

1. `BETTER_AUTH_URL` doesn't match actual URL
2. HTTPS/domain mismatch
3. Browser blocking cookies

**Fix:**

1. Verify `.env.local` has correct URL
2. Check browser console for cookie warnings
3. Clear cookies and try again

### Session Expired Too Quickly

**Symptoms:**

* User signed out before 7 days

**Causes:**

* Session not being refreshed
* `updateAge` too long

**Fix:**

* Check `session.updateAge` in `lib/auth.ts`
* Ensure user is making requests (refreshes session)

### Multiple Sign-Outs Required

**Symptoms:**

* User signs out but still sees authenticated state

**Causes:**

* Client-side cache not cleared
* SWR still holding session data

**Fix:**

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

// Clear SWR cache on sign out
await signOut();
mutate(() => true, undefined, { revalidate: false });
```

## Best Practices

<AccordionGroup>
  <Accordion title="Always Validate Sessions Server-Side" icon="server">
    Never trust client-side session state:

    ```typescript theme={null}
    // ✅ Good: Validate on server
    const { data: session } = await auth.api.getSession({ headers: headers() });
    if (!session) {
      return Response.json({ error: "Unauthorized" }, { status: 401 });
    }

    // ❌ Bad: Trust client-side check
    "use client";
    if (session) {
      // Anyone can bypass this
    }
    ```
  </Accordion>

  <Accordion title="Use HTTP-Only Cookies" icon="lock">
    Never store session tokens in localStorage or sessionStorage:

    * Better Auth uses HTTP-only cookies by default
    * Not accessible via JavaScript
    * Protected from XSS
  </Accordion>

  <Accordion title="Implement Activity-Based Expiry" icon="clock">
    Sessions should expire after inactivity:

    ```typescript theme={null}
    session: {
      expiresIn: 60 * 60 * 24 * 7,  // 7 days max
      updateAge: 60 * 60 * 24,       // Refresh after 1 day activity
    }
    ```
  </Accordion>

  <Accordion title="Track Session Metadata" icon="fingerprint">
    Store IP and user agent for security:

    * Helps identify suspicious activity
    * Useful for security audits
    * Better Auth stores this automatically
  </Accordion>
</AccordionGroup>

## Related Documentation

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

  <Card title="Better Auth" icon="lock" href="/developer/auth/better-auth">
    Auth configuration details
  </Card>

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