Package Architecture
The @repo/auth package structure, import conventions, server/client split, and integration points.
The @repo/auth package wraps Better Auth configuration and exposes three import paths with distinct purposes. This separation prevents server-only code from leaking into client bundles and enforces a clear boundary between auth types, auth operations, and browser-side auth calls.
Import Paths
| Import | Exports | Use In |
|---|---|---|
@repo/auth | Session, User (types only) | Anywhere |
@repo/auth/server | auth instance, ensurePersonalOrganization, ensureActiveOrganization, Auth type | Server components, tRPC context, route handlers |
@repo/auth/client | authClient, AuthClient type | Client components only |
Root Import: Types Only
import type { Session, User } from "@repo/auth";
The root import (@repo/auth) exports only TypeScript types. It never imports Better Auth at runtime. This is safe to use in any file -- server, client, or shared.
The types are inferred from the Better Auth instance using typeof auth.$Infer.Session.session and typeof auth.$Infer.Session.user, defined in packages/auth/src/types.ts.
Server Import: Auth Instance
import { auth, ensurePersonalOrganization, ensureActiveOrganization } from "@repo/auth/server";
The server export provides the configured Better Auth instance and the two organization bootstrapping helpers. This import triggers database connections and must only be used in server-side code.
The auth instance is configured with:
- Database: Drizzle adapter connected to the shared
@repo/dbclient - Base URL:
process.env["BETTER_AUTH_URL"] - Secret:
process.env["BETTER_AUTH_SECRET"] - Social providers: Google only (MVP)
- Plugins:
organization()with customtypefield - Session: Cookie cache (5-minute TTL)
- Rate limiting: 30 requests per 60-second window, in-memory storage
Client Import: Browser Auth Client
import { authClient } from "@repo/auth/client";
The client export provides a Better Auth client configured with the organizationClient() plugin. It reads NEXT_PUBLIC_BETTER_AUTH_URL for the base URL.
This is used in "use client" components for browser-side operations like authClient.signIn.social().
Web App Integration Points
The @repo/auth package integrates into the web app at four points:
1. Route Handler (apps/web/src/app/api/auth/[...all]/route.ts)
The catch-all auth route forwards all requests to Better Auth using toNextJsHandler(auth). This handles OAuth callbacks, session creation, sign-out, and all other Better Auth API endpoints.
import { toNextJsHandler } from "better-auth/next-js";
import { auth } from "@repo/auth/server";
export const { GET, POST } = toNextJsHandler(auth);
2. Server Component Helper (apps/web/src/lib/auth-server.ts)
A thin wrapper that calls auth.api.getSession() with the current request headers. Used in server component pages to check authentication.
export async function getSession() {
return auth.api.getSession({ headers: await headers() });
}
3. tRPC Context (packages/api/src/context.ts)
Creates the tRPC context by reading the session with cookie cache disabled (for mutation freshness):
const session = await auth.api.getSession({
headers: opts.headers,
query: { disableCookieCache: true },
});
4. Client Auth (apps/web/src/lib/auth-client.ts)
Re-exports the auth client from @repo/auth/client (or creates a local instance with the same config). Used by components like GoogleSignInButton.
SDK Boundary Enforcement
Better Auth is an external SDK that must only be imported through @repo/auth. This is enforced at two levels:
- ESLint
no-restricted-imports-- any direct import ofbetter-authoutside@repo/authtriggers a lint error dependency-cruiser-- the CI pipeline validates the dependency graph;better-authmay only appear as a dependency of@repo/auth
The restriction is defined in packages/config-eslint/restrictions.js under the SDK_BOUNDARIES map.
File Layout
packages/auth/
src/
index.ts -- Re-exports Session and User types (root entry point)
types.ts -- Type inference from Better Auth instance
server.ts -- auth instance + ensurePersonalOrganization + ensureActiveOrganization
client.ts -- authClient for browser-side operations
CLAUDE.md -- Package conventions and gotchas
package.json -- Exports map defining the three entry points
Gotchas
Do not import from @repo/auth when you need the auth instance. The root import is types only. Use @repo/auth/server.
The nextCookies() plugin is not in this package. It imports next/headers which fails outside a Next.js app context. It lives in the route handler file.
Organization slugs have a 48-character limit. The generateSlug() function in server.ts truncates to 48 characters, lowercase, hyphens only.
Personal orgs cannot have members or invitations. This is enforced in CASL (see Identity & Access -- Authorization), not in Better Auth config.
drizzle-orm is a peer dependency. The @repo/auth package lists drizzle-orm as a peer dep because the Drizzle adapter requires it at runtime.
Related Pages
- Session Management -- how the
authinstance is used for session validation - Sign-In Flow -- how the route handler and client auth work together
- ADR-002: Better Auth Decision -- why this package structure exists