User Authentication
Relevant source files
The following files were used as context for generating this wiki page:
The user authentication system for Adfluens is built using the better-auth library, providing a comprehensive solution for user management, session handling, and access control. It supports both traditional email/password registration and social OAuth sign-in with providers like Google and Facebook. The system is integrated across the monorepo, with a dedicated backend configuration, a shared database schema, and frontend components for a seamless user experience.
A key feature is the automatic creation of a personal organization (workspace) for each new user upon sign-up, ensuring every user has an environment to work in immediately. This is handled via a database hook in the authentication configuration.
Architecture Overview
The authentication system is distributed across several packages within the Turborepo monorepo. The core logic is encapsulated in the @repo/auth package, which is then consumed by the apps/api backend and the apps/web frontend.
The following diagram illustrates the high-level architecture of the authentication system.
apps/web: Contains the React components for the sign-in and sign-up user interfaces. It uses the@repo/auth/clientpackage to make authentication requests.apps/api: The Express.js backend that serves the API. It usesbetter-auth/nodeto create an authentication endpoint at/api/auth/*and integrates with tRPC to protect routes by verifying sessions.@repo/auth: A shared package that contains the centralbetter-authconfiguration. It defines providers, session settings, database adapters, and custom logic like hooks.@repo/db: A shared package that defines the Drizzle ORM schema for the database, including all authentication-related tables.
Sources: apps/api/src/index.ts:10,25, apps/web/src/features/auth/views/sign-in.view.tsx:4, packages/auth/src/auth.config.ts:1,12
Backend Configuration
The entire authentication behavior is defined in a single configuration file using the betterAuth function. This configuration is located in the @repo/auth package.
Sources: packages/auth/src/auth.config.ts
Core Configuration
The main configuration object sets up the database adapter, authentication methods, session management, and social providers.
| Option | Description | Value/Setting |
|---|---|---|
database | The adapter to connect better-auth to the database. | drizzleAdapter with a PostgreSQL provider. |
baseURL | The explicit URL where the Better Auth API runs. | env.BETTER_AUTH_URL |
trustedOrigins | A list of trusted origins for CORS requests, typically the frontend application URL. | [env.APP_URL] |
emailAndPassword | Configuration for email/password authentication. | Enabled, with a password length of 8-128 characters and auto sign-in. |
socialProviders | Configuration for OAuth providers. | Google and Facebook are enabled, using client IDs/secrets from env vars. |
session | Defines session lifetime and update frequency. | Sessions expire in 7 days and are updated every 24 hours. |
accountLinking | Allows users to link multiple social accounts to a single user profile. | Enabled for Google and Facebook. |
plugins | Extends better-auth with additional features. | The organization plugin is used for multi-tenancy and role-based access. |
Sources: packages/auth/src/auth.config.ts:12-59
Automatic Organization Creation
A databaseHooks block is configured to automatically create a personal workspace for every new user. This is achieved using the after hook on the user.create event. If the organization creation fails, the error is logged, but the user sign-up process is not blocked.
// packages/auth/src/auth.config.ts:86-109
databaseHooks: {
user: {
create: {
after: async (user) => {
try {
await auth.api.createOrganization({
body: {
name: `${user.name}'s Workspace`,
slug: `${user.id}-workspace`,
userId: user.id,
createdBy: user.id,
} as {
name: string;
slug: string;
userId: string;
createdBy: string;
},
});
} catch (error) {
// Log the error but don't block user creation
console.error(
`[auth] Failed to create personal organization for user ${user.id}:`,
error instanceof Error ? error.message : error
);
}
},
},
},
},
Sources: packages/auth/src/auth.config.ts:86-109
Database Schema
The authentication system relies on several tables in the PostgreSQL database, managed by Drizzle ORM. The primary tables are user, account, and session. The organization and member tables are part of the organization plugin.
This ER diagram shows the relationships between the core authentication and organization tables.
user Table
Stores the core user profile information.
| Column | Type | Constraints | Description |
|---|---|---|---|
id | varchar(36) | PRIMARY KEY, NOT NULL | Unique identifier for the user. |
name | text | NOT NULL | User's full name. |
email | varchar(255) | UNIQUE, NOT NULL | User's email address. |
email_verified | boolean | NOT NULL, DEFAULT false | Flag indicating if the email is verified. |
image | text | URL to the user's profile picture. |
Sources: packages/db/drizzle/meta/0002_snapshot.json
account Table
Stores credentials for different authentication providers, linking them to a user. An account can be an email/password credential or an OAuth provider credential.
| Column | Type | Constraints | Description |
|---|---|---|---|
id | varchar(36) | PRIMARY KEY, NOT NULL | Unique identifier for the account. |
account_id | text | NOT NULL | The user's ID from the OAuth provider. |
provider_id | text | NOT NULL | The name of the provider (e.g., email, google, facebook). |
user_id | varchar(36) | FOREIGN KEY, NOT NULL | Links to the user table. |
password | text | Hashed password for email provider. NULL for social providers. |
Sources: packages/db/drizzle/meta/0002_snapshot.json
session Table
Stores active user sessions.
| Column | Type | Constraints | Description |
|---|---|---|---|
id | varchar(36) | PRIMARY KEY, NOT NULL | Unique identifier for the session. |
token | text | UNIQUE, NOT NULL | The session token used for authentication. |
expires_at | timestamp | NOT NULL | The timestamp when the session expires. |
user_id | varchar(36) | FOREIGN KEY, NOT NULL | Links to the user table. |
Sources: packages/db/drizzle/meta/0002_snapshot.json
Frontend Implementation
The frontend in apps/web contains dedicated views and a reusable component for handling authentication flows.
Sources: apps/web/src/features/auth/views/sign-in.view.tsx, apps/web/src/features/auth/views/sign-up.view.tsx, apps/web/src/features/auth/components/auth-form.component.tsx
AuthForm Component
A reusable AuthForm component is used for both sign-in and sign-up to ensure a consistent UI. It adapts its fields and labels based on the mode prop ("sign-in" or "sign-up"). It handles form state, displays errors, and provides inputs for name (sign-up only), email, and password.
Sources: apps/web/src/features/auth/components/auth-form.component.tsx:16-23
Authentication Flow
The sign-in and sign-up views (SignInView.tsx, SignUpView.tsx) wrap the AuthForm and provide the handler functions for form submission and social OAuth clicks. These handlers call the client-side functions from @repo/auth/client.
The following sequence diagram shows the email sign-in process.
The flow for social sign-in is similar but redirects the user to the OAuth provider's page.
// apps/web/src/features/auth/views/sign-in.view.tsx:36-47
const handleGoogleSignIn = async () => {
setIsLoading(true);
setError(null);
try {
await signIn.social({
provider: "google",
callbackURL: "/",
});
} catch (err) {
setError(
err instanceof Error ? err.message : "Failed to sign in with Google"
);
setIsLoading(false);
}
};
API Integration
The backend API exposes the authentication endpoints and uses the session data to protect tRPC procedures.
Auth Endpoint
The Express server in apps/api/src/index.ts mounts the better-auth handler on the /api/auth/*splat route. This single line of code exposes all necessary endpoints for sign-in, sign-up, sign-out, session management, and OAuth callbacks. It must be placed before express.json() middleware.
// apps/api/src/index.ts:25-26
app.all("/api/auth/*splat", toNodeHandler(auth));
Sources: apps/api/src/index.ts:25-26
Protected tRPC Procedures
The tRPC context is created for every incoming request. The createContext function uses auth.api.getSession to retrieve the current user and session from the request headers.
// apps/api/src/trpc/init.ts:50-54
export const createContext = async ({
req,
}: CreateExpressContextOptions): Promise<Context> => {
const sessionData = await auth.api.getSession({
headers: fromNodeHeaders(req.headers),
});
// ...
This context is then used in middleware to create protected procedures. Any tRPC procedure built with protectedProcedure will automatically throw a TRPCError with code UNAUTHORIZED if the user is not logged in.
// apps/api/src/trpc/init.ts:89-98
const isAuthenticated = t.middleware(async ({ ctx, next }) => {
if (!ctx.session || !ctx.user) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You must be logged in to access this resource",
});
}
// ...
});
export const protectedProcedure = t.procedure.use(isAuthenticated);