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:
- Goal: What we’re trying to accomplish
- Prompt: The actual prompt I give to the AI
- Model: Which model I’m using (I’ll be switching between Claude Opus 4 and GPT 5.2 depending on the task)
- Summary: What the AI generated, summarized
- 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.
- Go to Google Cloud Console
- Create a new project or select an existing one
- Navigate to APIs & Services > Credentials
- Click Create Credentials > OAuth client ID
- Select Web application
- Add authorized JavaScript origins:
http://localhost:5173 - Add authorized redirect URIs:
http://localhost:3000/api/auth/callback/google - 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
Usermodel. 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 (
@@mapfor snake_case table names,@mapfor column names), UUIDv7 for IDs (@default(uuid(7)) @db.Uuid), and use PostgreSQLtimestamptz(@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,imagefields and relations toSession,Account - Session model — New model with
token,expiresAt,ipAddress,userAgent - Account model — New model for OAuth providers and password storage with
providerId,accountId, tokens,passwordfield - 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:
| File | Purpose |
|---|---|
src/auth/auth.ts | Better Auth configuration with Prisma adapter, providers, session settings |
src/auth/auth.module.ts | NestJS module registering the controller |
src/auth/auth.controller.ts | Catch-all controller for /api/auth/* using toNodeHandler |
src/main.ts | Updated with CORS config (credentials: true) |
src/app.module.ts | Added 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
.jsimport specifiers (more compatible once compiled). - Added a proper build pipeline for
packages/db:pnpm --filter db buildgenerates Prisma client and compiles topackages/db/dist/packages/db/package.jsonnow pointsmain/types/exportstodist
- Updated the API dev flow to ensure
dbis 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:
- Generate a secure random secret:
openssl rand -base64 32 - Use Google OAuth credentials created before in the Google Cloud Console
- 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
cnutility 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:
- Sign up — Create a new account with email/password
- Sign out — Click the sign out button
- Sign in — Log back in with your credentials
- 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: truewas set in the NestJS CORS config - Verified
trustedOriginsin Better Auth config includedhttp://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-setupbranch on GitHub.