@repo/auth is the server-side authentication layer. It provides getUserFromToken to resolve a JWT into a User object via @repo/db repos, an abstract BaseAuthClient with a concrete SupabaseAuthClient implementation, shared auth types, and auth-specific error classes. This package is framework-agnostic -- see @repo/auth-next for Next.js integration.
graph TD
auth["@repo/auth"] --> db["@repo/db"]
auth --> safe["@repo/safe"]
auth --> logger["@repo/logger"]
auth --> errors["@repo/errors"]
auth -.-> typescript_config["@repo/typescript-config"]
auth -.-> vitest_config["@repo/vitest-config"]| Import | Resolves to | Description |
|---|---|---|
@repo/auth |
src/index.ts |
getUserFromToken |
@repo/auth/types |
src/types.ts |
User, OAuthTokens, AuthToken, RefreshToken, SupabaseToken |
@repo/auth/errors |
src/errors.ts |
UnhandledAuthError, UnhandledSupabaseAuthError, UnauthorizedError |
@repo/auth/clients/base |
src/clients/base.auth-client.ts |
BaseAuthClient abstract class |
@repo/auth/clients/supabase |
src/clients/supabase.auth-client.ts |
SupabaseAuthClient implementation |
getUserFromToken WorksCaller->>Auth: getUserFromToken(token) Auth->>JWT: decodeJwt(token) JWT-->>Auth: { sub: UserId, exp } par Parallel fetches Auth->>DB: profilesRepo.getByAuthUserId(sub) Auth->>DB: userEntitiesRepo.getByUserId(sub) end DB-->>Auth: profile, userEntity Auth->>DB: entitiesRepo.getById(userEntity.entity_id) DB-->>Auth: entity Auth-->>Caller: User object
Caller->>Auth: getUserFromToken(token) Auth->>JWT: decodeJwt(token) JWT-->>Auth: { sub: UserId, exp } par Parallel fetches Auth->>DB: profilesRepo.getByAuthUserId(sub) Auth->>DB: userEntitiesRepo.getByUserId(sub) end DB-->>Auth: profile, userEntity Auth->>DB: entitiesRepo.getById(userEntity.entity_id) DB-->>Auth: entity Auth-->>Caller: User object
sequenceDiagram
participant Caller
participant Auth as getUserFromToken
participant JWT as decodeJwt
participant DB as "@repo/db repos"
Caller->>Auth: getUserFromToken(token)
Auth->>JWT: decodeJwt(token)
JWT-->>Auth: { sub: UserId, exp }
par Parallel fetches
Auth->>DB: profilesRepo.getByAuthUserId(sub)
Auth->>DB: userEntitiesRepo.getByUserId(sub)
end
DB-->>Auth: profile, userEntity
Auth->>DB: entitiesRepo.getById(userEntity.entity_id)
DB-->>Auth: entity
Auth-->>Caller: User object
The function decodes the JWT payload (base64, no signature verification -- Supabase handles that), then fetches the user's profile and organization in parallel from @repo/db. It uses unwrap() from @repo/safe, so any repo error propagates as a thrown exception.
import { getUserFromToken } from "@repo/auth";
// Typically called from @repo/auth-next's session layer, not directly
const user = await getUserFromToken(token);
// user: { id, email, first_name, last_name, title, user_id, entity_id, entity_name }
BaseAuthClientAn abstract class defining the auth provider interface:
import type { BaseAuthClient } from "@repo/auth/clients/base";
| Method | Description |
|---|---|
signUpWithEmailAndPassword(email, password) |
Register a new user |
signInWithPassword(email, password) |
Sign in, returns OAuthTokens |
signInWithGoogle() |
Initiate Google OAuth, returns { url } |
signInWithOTP(params) |
Verify an OTP code, returns OAuthTokens |
exchangeCodeForSession(code) |
Exchange OAuth/reset code for session tokens |
passwordReset(email) |
Send a password reset email |
passwordUpdate(password) |
Update the current user's password |
SupabaseAuthClientThe concrete implementation using @supabase/ssr. It creates a Supabase server client with cookie-based session management and PKCE flow.
import { SupabaseAuthClient } from "@repo/auth/clients/supabase";
const client = new SupabaseAuthClient({
cookies: async () => cookieStore,
oauthRedirectUrl: "https://myapp.com/api/auth/oauth",
passwordResetRedirectUrl: "https://myapp.com/api/auth/reset-password",
});
const tokens = await client.signInWithPassword("user@example.com", "password");
// tokens: { accessToken: AuthToken, refreshToken: RefreshToken }
To use a different auth provider, extend BaseAuthClient:
import { BaseAuthClient } from "@repo/auth/clients/base";
import type { OAuthTokens } from "@repo/auth/types";
class Auth0Client extends BaseAuthClient {
async signUpWithEmailAndPassword(email: string, password: string): Promise<void> {
// Call Auth0 Management API
}
async signInWithPassword(email: string, password: string): Promise<OAuthTokens> {
// Call Auth0 Authentication API
return { accessToken, refreshToken };
}
async signInWithGoogle(): Promise<{ url: string }> {
// Return Auth0 universal login URL with Google connection
return { url: "https://your-tenant.auth0.com/authorize?..." };
}
async signInWithOTP(params: VerifyOtpParams): Promise<OAuthTokens> { /* ... */ }
async exchangeCodeForSession(code: string): Promise<OAuthTokens> { /* ... */ }
async passwordReset(email: string): Promise<void> { /* ... */ }
async passwordUpdate(password: string): Promise<void> { /* ... */ }
}
The custom client can then be passed to any @repo/auth-next request handler factory, since they all accept BaseAuthClient.
All auth errors extend CircleError with domain: "auth".
| Error | Code | Description |
|---|---|---|
UnhandledAuthError |
UNHANDLED_AUTH_ERROR |
Catch-all for unexpected auth failures |
UnhandledSupabaseAuthError |
(from Supabase) | Wraps Supabase AuthError with its code and status |
UnauthorizedError |
UNAUTHORIZED_ERROR |
User is not authenticated or lacks permissions |
import { UnauthorizedError } from "@repo/auth/errors";
throw new UnauthorizedError("Token expired");
| Type | Description |
|---|---|
User |
Profile fields (id, email, first_name, last_name, title) plus user_id, entity_id, entity_name |
OAuthTokens |
{ accessToken: AuthToken, refreshToken: RefreshToken } |
AuthToken |
Branded string for access tokens |
RefreshToken |
Branded string for refresh tokens |
SupabaseToken |
Branded string for Supabase-specific tokens (v1 compatibility) |
import type { User, OAuthTokens, AuthToken } from "@repo/auth/types";
| Script | Description |
|---|---|
test |
Runs Vitest with coverage. |
test:watch |
Runs Vitest in watch mode. |
check-types |
Runs tsc --noEmit to typecheck the package. |