DocupDocup
Esc

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 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 for more on schema management.

Documentation written with Docup