# Authentication

How sign-in, sessions and workspaces work with Better Auth, GitHub and Google, and how the app protects routes and data.

Docup uses **Better Auth** with the Drizzle adapter. Auth routes are served under `/api/auth/*`.

## Sign-in

Two providers are enabled: GitHub and Google. There is no password. A person who signs in with both using the same verified email gets one account, because both are trusted providers for account linking. Provider tokens are stored encrypted.

The login page redirects signed-in visitors to the dashboard, and skips the auth instance entirely when there is no session cookie.

## Workspaces

The `organization` plugin models workspaces. When a user is created, a database hook creates a personal organization named after them with a random suffix in its slug, and adds the user as its `owner`. A session starts in the user's first organization.

Projects belong to an organization, not to a user. `getSession()` returns the user and the active organization. If a session has no active organization, which can happen for sessions created before organizations existed, it uses the user's first one.

## Protecting routes

| Helper | Use |
| --- | --- |
| `getSession()` | Returns the session or null. Used by API routes, which answer `401` when it is null. |
| `requireSession()` | Redirects visitors to `/login`. Used by pages and server actions. |

Every query that touches a project is scoped by `organization_id`. Server actions and API routes call an ownership check first, such as `owns(projectId)`, and page edits use a single statement that updates only when the project belongs to the workspace. A project from another workspace looks like it does not exist.

## Sessions

`cookieCache` is on for five minutes. A signed cookie carries the session data, so repeat checks skip the database. 

The auth instance is built once per request. Since API routes and server actions run outside the React renderer where React's `cache()` does not reach, `cacheForRequest` caches the instance across the request scope. React's `cache()` is used as the fallback during page rendering. This ensures connection pooling is efficient, as a Worker cannot share a database pool across requests.

## Rate limits

Better Auth's own limits (3 sign-in attempts per 10 seconds, 100 for everything else) are counted in Redis, through a custom storage. Its default counts in memory, and a Worker has many copies that each keep their own, so the limits would barely apply. The visitor's address comes from the `cf-connecting-ip` header, and every address in an IPv6 `/64` counts as one visitor. See [Rate limiting](/development/rate-limiting) for implementation details.

## Analytics events

Sign-ups and key actions are sent to PostHog under the user id, with the workspace and project ids. Sending never throws, so analytics cannot fail an action.

## Regenerating the auth schema

The plugin list in `lib/auth.ts` must match `db/auth-cli.ts`. After changing it, run `pnpm db:auth-schema` to regenerate `db/schema/auth.ts`. See [Database](/development/database) for more on schema management.
