Vibe Coding a Full-Stack AI Chatbot Platform (Part 5): Authentication with Better Auth
8 min read
tutorial ai chatbot llm full-stack cursor typescript react nestjs better-auth authentication oauth vibe-coding

Vibe Coding a Full-Stack AI Chatbot Platform (Part 5): Authentication with Better Auth

This is Part 5 of the tutorial series. If you haven’t read the previous parts, I recommend starting with Part 1: Introduction.

In Parts 1–4, we set up the foundation mostly by hand: the monorepo structure, tooling, apps, and database. Now it’s time to actually vibe code. From this point forward, I’ll be showing you the prompts I use, which AI model I’m working with, summarized results of what the AI generated and provide a link to a branch on github with the snapshot of the code at this point.

The goal for this part: implement authentication with Better Auth allowing for signup and login with email/password, enable Google OAuth, handle session management, and create protected routes in the backend.

The Vibe Coding Workflow

Here’s how I’ll structure each section from now on:

  1. Goal: What we’re trying to accomplish
  2. Prompt: The actual prompt I give to the AI
  3. Model: Which model I’m using (I’ll be switching between Claude Opus 4 and GPT 5.2 depending on the task)
  4. Summary: What the AI generated, summarized
  5. Adjustments: Any manual tweaks I made

Let’s get into it.


Setting Up Google OAuth Credentials

Before we start coding, we need OAuth credentials from Google. This is a manual step — no AI needed.

  1. Go to Google Cloud Console
  2. Create a new project or select an existing one
  3. Navigate to APIs & Services > Credentials
  4. Click Create Credentials > OAuth client ID
  5. Select Web application
  6. Add authorized JavaScript origins: http://localhost:5173
  7. Add authorized redirect URIs: http://localhost:3000/api/auth/callback/google
  8. Copy the Client ID and Client Secret — we’ll need these shortly

Step 1: Update the Database Schema for Auth

Goal: Add the tables Better Auth needs (sessions, accounts, verifications) and update the User model with auth-related fields.

Prompt:

I need to update my Prisma schema to support Better Auth. Right now I only have a minimal User model. Add the tables Better Auth requires for email/password auth and OAuth providers (in my case i will be using Google OAuth but want to keep the schema flexible for other providers) and update the User model with auth-related fields.

Use the same conventions I have (@@map for snake_case table names, @map for column names), UUIDv7 for IDs (@default(uuid(7)) @db.Uuid), and use PostgreSQL timestamptz (@db.Timestamptz) for timestamps.

Model: GPT 5.2 Extra High Reasoning

Summary: The AI updated packages/db/prisma/schema.prisma with:

  • User model — Added emailVerified, image fields and relations to Session, Account
  • Session model — New model with token, expiresAt, ipAddress, userAgent
  • Account model — New model for OAuth providers and password storage with providerId, accountId, tokens, password field
  • Verification model — New model for email verification and password reset tokens

The schema includes standard Better Auth tables: User (with email verification and profile fields), Session (for managing user sessions with IP tracking), Account (for OAuth providers and password storage), and Verification (for email verification tokens).


Step 2: Install Better Auth

Add the better-auth dependency to the api and web packages:

# In apps/api
pnpm add better-auth

# In apps/web
pnpm add better-auth

Step 3: Configure Better Auth on the Backend

Goal: Set up Better Auth with Prisma adapter, email/password auth, and Google OAuth in our NestJS app.

Prompt:

Set up Better Auth in my NestJS app at apps/api. I need:

  • Prisma adapter using my db package
  • Email/password authentication enabled
  • Google OAuth provider
  • Session cookie caching
  • CORS support for my React client at localhost:5173 (use an env variable)

Create the auth configuration, a NestJS module, and a controller that handles all /api/auth/* routes. Also update main.ts for CORS and app.module.ts to include the auth module.

Model: GPT 5.2 High Reasoning

Summary: The AI created several files:

FilePurpose
src/auth/auth.tsBetter Auth configuration with Prisma adapter, providers, session settings
src/auth/auth.module.tsNestJS module registering the controller
src/auth/auth.controller.tsCatch-all controller for /api/auth/* using toNodeHandler
src/main.tsUpdated with CORS config (credentials: true)
src/app.module.tsAdded ConfigModule and AuthModule imports

🔧 Adjustments: I added the db package as a workspace dependency in apps/api/package.json:

{
  "dependencies": {
    "db": "workspace:*"
  }
}

Then ran pnpm install from the root.

A Follow-Up: Making packages/db Buildable

Right after Step 3, I hit a runtime issue: the API was consuming packages/db directly from TypeScript source (src/index.ts), and Node couldn’t reliably resolve Prisma’s generated client imports in that setup.

So I asked GPT 5.2 High to make packages/db a buildable package that outputs JavaScript and type declarations, and then had the API consume that built output.

What we did:

  • Created a shared TS config package (packages/tsconfig) so apps/packages can reuse consistent compiler settings.
  • Updated Prisma generation so the generated client uses .js import specifiers (more compatible once compiled).
  • Added a proper build pipeline for packages/db:
    • pnpm --filter db build generates Prisma client and compiles to packages/db/dist/
    • packages/db/package.json now points main/types/exports to dist
  • Updated the API dev flow to ensure db is built before starting the Nest dev server.

Step 4: Environment Variables

Create apps/api/.env with the following environment variables:

DATABASE_URL="postgresql://ai_chatbot_user:ai_chatbot_password@localhost:5434/ai_chatbot"
BETTER_AUTH_SECRET="your-random-secret-here"
BETTER_AUTH_URL="http://localhost:3000"
CLIENT_URL="http://localhost:5173"
GOOGLE_CLIENT_ID="your-google-client-id"
GOOGLE_CLIENT_SECRET="your-google-client-secret"

Setup Instructions:

  1. Generate a secure random secret: openssl rand -base64 32
  2. Use Google OAuth credentials created before in the Google Cloud Console
  3. Update the placeholder values with your actual credentials

Step 5: Create the Auth Client for React

Goal: Set up the Better Auth client in the React app.

Prompt:

Create the Better Auth client for my React app at apps/web. Export the auth functions (signIn, signUp, signOut, useSession) that I’ll need for the UI.

Model: Claude Opus 4.5

Summary: Created src/lib/auth-client.ts:

import { createAuthClient } from "better-auth/react"

export const authClient = createAuthClient({
  baseURL: import.meta.env.VITE_API_URL || "http://localhost:3000",
})

export const { signIn, signUp, signOut, useSession, getSession } = authClient

Also created apps/web/.env:

VITE_API_URL=http://localhost:3000

Step 6: Build the Auth UI

Goal: Create login and signup pages with a clean, modern design.

Prompt:

Create login and signup pages for my React app. Requirements:

  • Clean, minimal design with dark mode support
  • Use Tailwind CSS for styling, following mobile-first design principles
  • Email/password form fields
  • Google OAuth button with the Google logo SVG
  • Loading states and error handling
  • Links to switch between login and signup
  • Use the auth functions from src/lib/auth-client.ts

Also create reusable Button and Input components using shadcn/ui components.

Create a cn utility function that combines clsx and tailwind-merge for merging Tailwind classes, and use it consistently for classnames across all components.

Also create a design system base in Tailwind CSS with custom color palette, typography scale, spacing values, and component-specific design tokens.

Model: Claude Opus 4.5 Thinking (switched for UI work as it tends to produce cleaner designs than GPT 5.2)

Key features in the generated UI:

  • Neutral color palette that works in both light and dark modes
  • Proper form validation and error display
  • Loading spinners during auth operations
  • Google logo as inline SVG
  • Responsive layout centered on screen

Adjustments:

  • Named the AI Chatbot Platform as ‘Ask Cosmos’.
  • Asked the AI to generate a logo for it and use it as favicon.

Step 7: Wire Up the App Component

Goal: Update the main App component to show login when unauthenticated and a welcome screen when authenticated.

Prompt:

Update src/App.tsx to:

  • Use the useSession hook to check auth state
  • Show a loading spinner while checking session
  • Show the LoginPage if not authenticated
  • Show a welcome message with the user’s name and a sign out button if authenticated

Model: Claude Sonnet 4

Summary: Updated src/App.tsx to conditionally render based on session state:

  • isPending → Loading spinner
  • !session<LoginPage />
  • session → Welcome message + sign out button

Step 8: Create an Auth Guard for Protected Endpoints

Goal: Create a NestJS guard that protects API routes and attaches the session to the request.

Prompt:

Create a NestJS guard that checks if the user is authenticated using Better Auth. If not authenticated, throw UnauthorizedException. If authenticated, attach the session to the request object so controllers can access it.

Model: GPT 5.2 High Reasoning

Summary: Created src/auth/auth.guard.ts:

@Injectable()
export class AuthGuard implements CanActivate {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const req = context.switchToHttp().getRequest<Request>();

    const session = await auth.api.getSession({
      headers: fromNodeHeaders(req.headers),
    });

    if (!session || !session.user) {
      throw new UnauthorizedException();
    }

    req.session = session;
    return true;
  }
}

Now any controller can use @UseGuards(AuthGuard) to require authentication.


Testing the Full Flow

Time to verify everything works. Make sure Docker is running:

docker compose up -d

Start the dev servers:

pnpm dev

Open http://localhost:5173 and test:

  1. Sign up — Create a new account with email/password
  2. Sign out — Click the sign out button
  3. Sign in — Log back in with your credentials
  4. Google OAuth — Try the Google button (requires valid OAuth credentials)

Check the browser DevTools → Application → Cookies. You should see an HTTP-only session cookie from localhost:3000.


Troubleshooting

A few issues I ran into and how I fixed them:

“CORS error” when calling auth endpoints

  • Made sure credentials: true was set in the NestJS CORS config
  • Verified trustedOrigins in Better Auth config included http://localhost:5173

Tables issues with better-auth ids

  • I had to create a migration to update the type of the id column from UUID to String since Better Auth uses String for the id column. So current tables which rely on better-auth ids use String for the id column but other tables to be added in the future will use UUIDv7 with UUID type as expected.

What We’ve Accomplished

  • Database schema updated with auth tables (Session, Account, Verification)
  • Better Auth configured on NestJS with Prisma adapter
  • Email/password auth working with signup and login
  • Google OAuth integrated as a social provider
  • React auth client with hooks for session management
  • Login/Signup UI with clean, dark-mode-ready design
  • Auth guard for protecting API endpoints

Vibe Coding Observations

A few notes on the AI-assisted development experience for this part:

What worked well:

  • The Better Auth integration was straightforward; the AI knew the library well
  • UI components came out clean with proper TypeScript types and design looks clean.

What needed manual work:

  • Environment variables always need manual handling (secrets, OAuth credentials), although cursor can help with the generation of the .env file and even add the content when the values are known.

Next Steps

Users can now sign up and sign in, but the app doesn’t do much yet. In Part 6, we’ll set up tRPC for end-to-end type safety between our React client and NestJS server:

  • Create a shared tRPC package with the router definition
  • Wire tRPC into NestJS with authentication context
  • Set up the tRPC client in React with TanStack Query
  • Build our first authenticated procedures (create chat, list chats)

The auth foundation is solid. Time to build the API layer.

📁 Repository State: The current state of the codebase described in this article is available in the feat/auth-setup branch on GitHub.

Comments