AI Lawyer Platform -
Comprehensive Interview
Guide
Project Name: LwLand (AI Lawyer)
Purpose: A full-stack SaaS platform providing AI-powered legal assistance specialized in Indian Law
Stack: [Link] + Express | React + Vite | PostgreSQL + Prisma | Stripe | Google Gemini AI
Deployment: Render (Backend) | Vercel (Frontend)
Executive Summary
LwLand is a tiered SaaS legal consultation platform that leverages Google's Gemini AI to provide specialized Indian
law guidance. Users authenticate, select pricing tiers (Free/Premium/Enterprise), make payments via Stripe, and interact
with an AI legal assistant through a chat interface. The system tracks usage, enforces rate limits, and maintains chat
history.
Key Innovation: Specialized legal prompt engineering for Indian law context + usage-based rate limiting with Redis.
Architecture Overview
System Design Diagram
┌─────────────────────────────────────────────────────────────┐
│ USER BROWSER │
│ (React + Vite Frontend) │
└──────────────────────────┬──────────────────────────────────┘
│ HTTP/REST
┌──────────┴──────────┐
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Auth APIs │ │ Chat APIs │
│ (JWT tokens) │ │ (Messages) │
└──────┬───────┘ └──────┬───────┘
│ │
┌──────┴──────────────────────┴─────────┐
│ [Link] Server ([Link]) │
│ Port: 5000 (Render deployment) │
├───────────────────────────────────────┤
│ Middlewares: │
│ • CORS (Frontend origin) │
│ • JWT Auth Verification │
│ • Usage Rate Limiting (Redis) │
│ • Request Logging │
└──────┬──────────────────────────────┬──┘
│ │
┌────────▼──────────┐ ┌─────────▼──────────┐
│ PostgreSQL DB │ │ Redis Cache │
│ (Prisma ORM) │ │ (Upstash) │
│ • Users │ │ • Rate Limits │
│ • Chats │ │ • Session Cache │
│ • Messages │ └────────────────────┘
│ • Payments │
│ • Subscriptions │
└───────────────────┘
│
┌────────▼──────────────────┐
│ Stripe Payment Gateway │
│ • Checkout Sessions │
│ • Payment Processing │
│ • Webhook Notifications │
└──────────────────────────┘
│
┌────────▼──────────────────┐
│ Google Gemini API │
│ • Legal Prompt Injection │
│ • Message Processing │
│ • Streaming Response │
└──────────────────────────┘
Design Decisions & Rationale
Decision Why
[Link] Lightweight, fast, perfect for REST APIs with middleware support
PostgreSQL + Type-safe ORM, auto migrations, great for relational data (Users →
Prisma Chats → Messages)
Redis (Upstash) Fast in-memory rate limiting, session caching, no DB overhead
Google Gemini Free tier generous, good for legal text generation, latest capabilities
Stripe Industry standard, secure payment processing, webhook support
JWT Tokens Stateless auth, no server sessions needed, scales horizontally
React + Vite Fast builds, HMR, modern DX, small bundle size
Render + Vercel Auto-deployments from Git, free tiers, serverless scaling
Database Schema (Prisma)
Entity-Relationship Diagram
┌──────────────────┐
│ USER │
├──────────────────┤
│ id (UUID, PK) │
│ email (unique) │
│ password (hash) │
│ role (enum) │◄─────┐
│ createdAt │ │
└────────┬─────────┘ │
│ │
┌────┴────┬────────┬──┴─────────┐
│ │ │ │
▼ ▼ ▼ ▼
┌────────┐ ┌──────────┐ ┌─────────┐ ┌──────────────┐
│ CHAT │ │PAYMENT │ │SUBSCRIP.│ │ SUBSCRIPTION│
├────────┤ ├──────────┤ ├─────────┤ ├──────────────┤
│ id (PK)│ │ id (PK) │ │ id (PK) │ │ id (PK) │
│ userId │ │ userId │ │ userId │ │ userId (FK) │
│ title │ │ amount │ │ plan │ │ plan (enum) │
│created │ │ currency │ │ active │ │ active │
└────┬───┘ │ status │ │created │ │ createdAt │
│ │ plan │ └─────────┘ └──────────────┘
│ │ stripeId │
│ │ created │
│ └──────────┘
│
▼
┌──────────────┐
│ MESSAGE │
├──────────────┤
│ id (PK) │
│ chatId (FK) │
│ role │◄── "user" or "assistant"
│ content │
│ createdAt │
└──────────────┘
Role Enum & Usage Limits
enum Role {
FREE // Default: 5 queries/day
PREMIUM // Paid: 100 queries/day
ENTERPRISE // Paid: Unlimited
}
Why This Schema?
Table Reasoning
User Core identity, role determines feature access and API rate limits
Chat Groups messages logically, allows users to manage multiple conversations
Preserves conversation history, tracks role (user vs AI assistant) for prompt
Message
building
Payment Tracks Stripe transactions, links to user upgrade, status for reconciliation
Subscription Tracks active subscriptions, enables billing features later
Authentication Flow
JWT Token-Based Auth
┌─────────────────┐
│ User Input │
│ Email + Pass │
└────────┬────────┘
│ POST /auth/register or /auth/login
▼
┌─────────────────┐
│ [Link] │
│ • Hash password │
│ (bcryptjs) │
│ • Create JWT │
└────────┬────────┘
│
┌──────▼───────────────┐
│ Generate Token: │
│ [Link]({ │
│ id: userId, │
│ role: userRole │
│ }, SECRET, { │
│ expiresIn: '7d' │
│ }) │
└──────┬───────────────┘
│
┌────────▼─────────┐
│ Return token to │
│ frontend │
└─────────┬────────┘
│
┌─────────▼──────────────────┐
│ Frontend stores in │
│ localStorage('token') │
│ │
│ AuthContext hydrates on │
│ app load │
└─────────┬──────────────────┘
│
┌─────────▼──────────────┐
│ Subsequent requests: │
│ Headers: { │
│ Authorization: │
│ 'Bearer <token>' │
│ } │
└──────────┬─────────────┘
│
┌──────────▼────────────────┐
│ [Link] │
│ • Extract token │
│ • Verify signature │
│ • Decode user data │
│ • Attach to [Link] │
└──────────┬────────────────┘
│
┌──────────▼──────────┐
│ Request processed │
│ with user context │
└─────────────────────┘
Security Considerations
// Password Hashing
[Link](password, 10); // 10 rounds, takes ~100ms
// Prevents plaintext exposure in DB
// JWT Secret
[Link].JWT_SECRET; // Never hardcoded
// Store in environment variables
// Token Expiry
expiresIn: "7d"; // Tokens expire, forcing re-login
// Reduces window of exposure if token leaked
// Secure Storage
[Link]("token"); // Client-side only
// Never send in URL parameters (logged in server logs)
// CORS Validation
origin: [Link].CLIENT_URL; // Whitelist specific origin
// Prevents cross-site attacks
Why JWT Over Sessions?
Feature JWT Sessions
Stateless
Scalable (no server memory) ⚠ (needs shared store)
Mobile-friendly ⚠
Feature JWT Sessions
CSRF attacks ⚠ (mitigated with SameSite) (vulnerable)
Chat & AI Integration
Message Flow Architecture
┌─────────────────────────────┐
│ User Types Legal Question │
│ "What is IPC Section 420?" │
└────────────┬────────────────┘
│
┌────────▼─────────────────────────┐
│ Frontend: sendChatToBackend() │
│ POST /api/chat │
│ { │
│ message: "...", │
│ chatId?: "optional" │
│ } │
└────────┬────────────────────────┘
│
┌────────▼───────────────────────┐
│ Backend: [Link] │
│ │
│ 1. Verify auth (JWT) │
│ 2. Enforce rate limit (Redis) │
│ 3. Get/Create chat + save msg │
└────────┬───────────────────────┘
│
┌────────▼──────────────────────────┐
│ Fetch chat history from DB: │
│ • All previous messages in chat │
│ • Maintains conversation context │
│ • Ordered by timestamp │
└────────┬─────────────────────────┘
│
┌────────▼────────────────────────────────┐
│ Build Gemini Prompt: │
│ [SYSTEM_PROMPT] (Indian Law Expert) │
│ + [Chat History] (Previous messages) │
│ + [User Message] (New question) │
└────────┬───────────────────────────────┘
│
┌────────▼────────────────────────┐
│ Call Gemini API: │
│ [Link]() │
│ [Link]() │
│ │
│ Returns: AI legal guidance │
└────────┬───────────────────────┘
│
┌────────▼──────────────────────────┐
│ Save AI Response to DB: │
│ Message { │
│ role: "assistant", │
│ content: "<AI response>" │
│ } │
└────────┬─────────────────────────┘
│
┌────────▼────────────────┐
│ Return to Frontend: │
│ { │
│ response: "<content>", │
│ chatId: "<id>" │
│ } │
└────────┬─────────────────┘
│
┌────────▼──────────────────┐
│ Frontend Updates UI: │
│ • Display AI response │
│ • Add to message history │
│ • Enable new input │
└──────────────────────────┘
System Prompt Engineering
const SYSTEM_PROMPT = `You are an expert legal assistant specializing in Indian Law.
Your role is to:
1. Provide accurate information about Indian laws, acts, sections
2. Explain complex concepts in simple language
3. Reference specific sections (IPC, CrPC, CPC, Constitution, etc.)
4. Always disclaimer: "This is legal information, not legal advice"
5. Encourage consulting qualified lawyers
6. Focus ONLY on Indian legal matters
Remember: Maintain context from previous messages.`;
Why Specialized Prompt?
Narrows scope (prevents hallucinations about other jurisdictions)
Adds expertise (references correct acts/sections)
Sets expectations (legal info vs. legal advice)
Enables compliance (disclaimers built-in)
Retry Logic for Database Resilience
const withRetry = async (fn, maxRetries = 3) => {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (i === maxRetries - 1) throw error;
await new Promise(
(resolve) => setTimeout(resolve, [Link](2, i) * 100) // Exponential backoff
);
}
}
};
Why Retry?
Database connections can be temporarily unavailable
Exponential backoff prevents overwhelming the DB
Fail-fast: gives up after 3 attempts (100ms, 200ms, 400ms)
Message Context Window
// Build conversation for Gemini
const messages = await [Link]({
where: { chatId: [Link] },
orderBy: { createdAt: "asc" },
});
// Combine into single context
const conversationContext = messages
.map((m) => `${[Link]}: ${[Link]}`)
.join("\n\n");
Limitation: No context size limit checking (could exceed Gemini's token limit on long chats). Future optimization:
implement message summarization or sliding window.
Payment Integration (Stripe)
Payment Lifecycle
┌─────────────────┐
│ User Selects │
│ Premium Plan │
└────────┬────────┘
│ POST /create-checkout-session
│ { plan: "premium" }
▼
┌──────────────────────────┐
│ Backend Validates: │
│ • User authenticated │
│ • Plan exists │
│ • Price configured │
└────────┬─────────────────┘
│
┌────────▼──────────────────────────┐
│ Stripe Creates Session: │
│ • payment_method_types: ['card'] │
│ • metadata: { userId, plan } │
│ • success_url, cancel_url │
│ • unit_amount: plan price * 100 │
└────────┬───────────────────────┘
│
┌────────▼─────────────────┐
│ Returns session URL │
└────────┬──────────────────┘
│
┌────────▼────────────────────────┐
│ Frontend Redirects User to │
│ Stripe Checkout ([Link]) │
└────────┬─────────────────────────┘
│
┌────────▼──────────────────┐
│ User Enters Card Details │
│ on Stripe Hosted Page │
│ (PCI Compliant) │
└────────┬──────────────────┘
│
┌────────▼──────────────────────────┐
│ Stripe Processes Payment │
├──────────────────────────────────┤
│ On Success: │
│ • Redirects to /success │
│ │
│ On Failure: │
│ • Redirects to /cancel │
└──────────────────────────────────┘
Plan Pricing Model
const PLAN_PRICES = {
free: 0, // 5 queries/day
premium: 999, // $9.99/month - 100 queries/day
enterprise: 2999, // $29.99/month - Unlimited
};
Pricing Strategy:
Freemium model: Hook users with free tier
Premium: 20x more queries (attracts power users)
Enterprise: Unlimited + could add features (white-label, API, priority support)
Security & Compliance
// Stripe Secret Key
[Link].STRIPE_SECRET_KEY; // Never exposed to frontend
// Metadata Tracking
metadata: {
userId, plan;
} // Links payment to user account
// Idempotent Checkout
// Multiple identical requests = same session (Stripe handles this)
// Free Plan Handling
if (plan === "free") {
return [Link](200).json({
url: [Link].CLIENT_URL + "/success",
});
// No Stripe session = instant "success"
}
Webhook Handling (Incomplete)
Current Status: Payment routes exist but webhook validation incomplete. In production:
// Should be implemented:
[Link]("/webhook", [Link]({ type: "application/json" }), (req, res) => {
const sig = [Link]["stripe-signature"];
const event = [Link](
[Link],
sig,
[Link].STRIPE_WEBHOOK_SECRET
);
if ([Link] === "[Link]") {
const { metadata, payment_intent } = [Link];
// Update user role in DB
// Create subscription record
// Send confirmation email
}
});
⚡ Rate Limiting & Usage Tracking
Redis-Based Rate Limiting
// Middleware: [Link]
const usageLimit = (feature) => {
return async (req, res, next) => {
const user = [Link];
const limit = PLAN_LIMITS[[Link]];
if (limit === Infinity) return next(); // Enterprise = unlimited
const key = `usage:${[Link]}:${feature}`;
const current = await [Link](key);
if (current === 1) {
await [Link](key, 60 * 60 * 24); // 24-hour window
}
if (current > limit) {
return [Link](429).json({
error: "Daily usage limit exceeded. Upgrade plan.",
});
}
next();
};
};
Limit Configuration
// config/[Link]
export const PLAN_LIMITS = {
FREE: 5, // 5 requests per 24h
PREMIUM: 100, // 100 requests per 24h
ENTERPRISE: Infinity, // Unlimited
};
How It Works
User sends chat request
│
▼
GET redis: "usage:user123:chat"
│
├─ Doesn't exist? INCR = 1, SET expiry 24h
├─ Exists? INCR = 2, 3, 4, ...
│
▼
Compare current count vs. PLAN_LIMIT[[Link]]
│
├─ Within limit? Proceed
└─ Exceeded? Return 429 (Too Many Requests)
Why Redis vs. Database?
Aspect Redis Database
Latency <5ms 10-50ms
Throughput High Limited
Operations/sec 1M+ 10K-100K
TTL support Native ⚠ Requires cleanup
Fail-open behavior Allowed Risk of false rejections
Critical Feature: If Redis fails, request is allowed to proceed (fail-open). Prevents cascading outages. Downside: Users
might exceed limits during Redis downtime.
try {
const current = await [Link](key);
// ...
} catch (redisErr) {
[Link]("Redis error, allowing request:", [Link]);
// Allow request to proceed
next();
}
Frontend Architecture
React Component Hierarchy
┌─────────────────────────────┐
│ [Link] (Router) │
│ ├─ LandingPage (/) │
│ ├─ ChatPage (/chat) │
│ ├─ LoginPage (/login) │
│ ├─ RegisterPage (/register)
│ ├─ PaymentSuccessPage │
│ └─ PaymentCancelPage │
│ │
│ Providers: │
│ • AuthProvider │
│ • Router (React Router v7) │
└─────────────────────────────┘
Authentication Context ([Link])
// State Management
const [token, setToken] = useState(null);
const [user, setUser] = useState(null);
const [authReady, setAuthReady] = useState(false);
// 2-Stage Hydration
// Stage 1: Check localStorage for token
useEffect(() => {
const storedToken = [Link]("token");
if (storedToken) setToken(storedToken);
else setAuthReady(true);
}, []);
// Stage 2: Validate token with backend
useEffect(() => {
const fetchUser = async () => {
if (!token) return;
const res = await fetch(`${API_BASE}/auth/me`, {
headers: { Authorization: `Bearer ${token}` },
});
if (![Link]) {
[Link]("token");
setToken(null);
setUser(null);
return;
}
const data = await [Link]();
setUser([Link]);
setAuthReady(true); // Only after backend validation
};
fetchUser();
}, [token]);
Why 2-Stage Hydration?
Stage 1: Fast app load (check localStorage synchronously)
Stage 2: Validate token (network round-trip) - prevents using expired tokens
Protected Routes
// [Link]
export const ProtectedRoute = ({ children }) => {
const { isAuth, authReady } = useContext(AuthContext);
if (!authReady) return <LoadingScreen />;
if (!isAuth) return <Navigate to="/login" />;
return children;
};
// Usage
<Route
path="/chat"
element={
<ProtectedRoute>
<ChatPage />
</ProtectedRoute>
}
/>;
Styling Approach
Tailwind CSS: Utility-first, small bundle, responsive
PostCSS: Autoprefixer for browser compatibility
Design System: No component library (custom components)
Deployment
Backend (Render)
# [Link]
services:
- type: web
name: lwland-backend
env: node
buildCommand: npm install
startCommand: npm start
envVars:
- key: DATABASE_URL
scope: web
sync: false
- key: STRIPE_SECRET_KEY
scope: web
sync: false
- key: GEMINI_API_KEY
scope: web
sync: false
- key: JWT_SECRET
scope: web
sync: false
- key: CLIENT_URL
value: [Link]
Deployment Flow:
1. Push to GitHub
2. Render detects changes
3. Runs npm install
4. Runs npm start (node [Link])
5. Health checks confirm running
6. Routes traffic
Frontend (Vercel)
{
"buildCommand": "npm run build",
"outputDirectory": "dist",
"env": {
"VITE_BACKEND_URL": "[Link]
},
"redirects": [
{
"source": "/:path*",
"destination": "/[Link]"
}
]
}
Key Points:
Vite builds to /dist
Environment variables injected at build time
SPA redirect: all routes → [Link] (React Router handles)
CDN caching for static assets
Environment Variables
Backend:
DATABASE_URL=postgresql://...@aws-postgres:5432/lwland
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
GEMINI_API_KEY=AIzaSy...
JWT_SECRET=super-secret-key-min-32-chars
REDIS_URL=redis://:password@[Link]
CLIENT_URL=[Link]
PORT=5000
Frontend:
VITE_BACKEND_URL=[Link]
Never hardcode secrets! Render/Vercel handle this automatically.
Common Issues & Troubleshooting
Issue 1: "401 Unauthorized"
// Problem: Token invalid/expired
// Solution:
if ([Link] === 401) {
[Link]("token");
// Redirect to login
}
// Prevent: Refresh token implementation (not yet implemented)
Issue 2: "429 Too Many Requests"
// Problem: Hit rate limit
// Solution:
if ([Link] === 429) {
// Show: "You've used your daily queries. Upgrade to Premium"
// Wait 24 hours OR upgrade plan
}
Issue 3: "503 Database Temporarily Unavailable"
// Problem: Database connection failed
// Cause: Retry exhausted (3 attempts)
// Solution:
// - Check DATABASE_URL in .env
// - Verify PostgreSQL is running
// - Check Render service status
Issue 4: "Stripe Webhook Not Firing"
// Problem: Payment doesn't update user role
// Debug:
// 1. Check stripe logs: [Link]/events
// 2. Verify webhook endpoint configured: /webhook
// 3. Verify STRIPE_WEBHOOK_SECRET matches
// 4. Check webhook signature validation
// Fix: Implement proper webhook handler (currently incomplete)
Issue 5: "CORS Error: Origin Not Allowed"
// Problem: Frontend and backend origins don't match
// Solution:
const corsOrigin = [Link].CLIENT_URL; // Must match frontend domain
[Link](cors({ origin: corsOrigin }));
// Example:
// Frontend: [Link]
// Backend: [Link]
// Must add to env: CLIENT_URL=[Link]
Issue 6: "Gemini API Returns Empty Response"
// Problem: Rate limited or API quota exceeded
// Solution:
// 1. Check quota at [Link]
// 2. Free tier has rate limits
// 3. Implement exponential backoff retry
// 4. Add error handling for empty responses
Key Implementation Details
Error Handling Strategy
// Pattern used across backend:
try {
// Attempt operation
const result = await operation();
[Link](200).json(result);
} catch (error) {
[Link]("Specific context:", [Link]);
[Link](500).json({ error: [Link] });
}
Issues:
All errors return 500 (should differentiate)
Error messages exposed to client (security risk)
No logging service (just [Link])
Better Approach:
const errorHandler = (err, req, res, next) => {
const statusCode = [Link] || 500;
const message =
[Link].NODE_ENV === "production"
? "Internal server error"
: [Link];
[Link](statusCode).json({ error: message });
// Log to service
[Link](err, { userId: [Link]?.id });
};
Input Validation
Current Status: ⚠ Minimal validation
// Chat controller only checks:
if (!message || [Link]() === "") {
return [Link](400).json({ error: "Message is required" });
}
// Missing:
// - Max message length (prevent DoS)
// - Email validation (register/login)
// - SQL injection prevention (Prisma helps, but add input sanitization)
// - Rate limit bypass prevention
CORS Configuration
[Link](
cors({
origin: corsOrigin, // Whitelist specific origin
methods: ["GET", "POST", "DELETE", "PUT", "PATCH"],
credentials: true, // Allow cookies
})
);
Security Notes:
Specific origin (not *)
Limited methods
⚠ No preflight caching (add maxAge)
⚠ All methods allowed (consider restricting)
Data Flow Examples
Complete Chat Flow (User Perspective)
1. User visits [Link]
├─ AuthContext checks localStorage for token
├─ If token exists, validates with /auth/me
├─ If invalid, clears token and shows login
└─ If valid, shows ChatPage
2. User asks "What is dowry?"
├─ Frontend: POST /api/chat { message: "..." }
├─ Backend verifies JWT token
├─ Backend checks Redis rate limit
│ ├─ Within limit? Continue
│ └─ Exceeded? Return 429
├─ Creates/gets Chat record
├─ Saves user message to DB
├─ Fetches chat history (for context)
├─ Calls Gemini with:
│ ├─ System prompt (Indian law expert)
│ ├─ Chat history (previous messages)
│ └─ User message
├─ Saves AI response to DB
└─ Returns to frontend
3. Frontend displays response
├─ Shows AI answer about dowry laws
├─ Displays in chat UI
└─ User can ask follow-up questions
Complete Payment Flow (User Perspective)
1. User clicks "Upgrade to Premium"
├─ Frontend: POST /create-checkout-session { plan: "premium" }
├─ Backend verifies authentication
├─ Backend checks plan validity
└─ Backend calls Stripe API
2. Stripe Creates Checkout Session
├─ Generates unique session ID
├─ Stores metadata: { userId, plan }
├─ Returns session URL
└─ Session valid for 24 hours
3. Frontend redirects to Stripe
├─ [Link] = [Link]
└─ Stripe hosted checkout (PCI compliant)
4. User enters payment details
├─ Stripe validates card
├─ Processes $9.99 charge
└─ Returns payment_intent
5. Stripe Redirects
├─ On success → [Link]/success
│ └─ User sees "Payment successful"
└─ On failure → [Link]/cancel
└─ User sees "Payment canceled"
6. Webhook fires (asynchronous)
├─ Stripe → Backend: /webhook
├─ Update [Link] = "PREMIUM"
├─ Create Subscription record
└─ Send confirmation email (optional)
⚠ ISSUE: Webhook not fully implemented
→ User redirected to success page
→ But role might not update immediately
→ User needs page refresh to see premium features
Interview Question Preparation
Q1: "Walk me through the entire authentication flow"
Expected Answer Structure:
1. User registers → password hashed with bcryptjs → stored in DB
2. Login → password verified → JWT generated with userId + role
3. Token stored in localStorage on frontend
4. Subsequent requests include Authorization: Bearer <token>
5. Backend middleware verifies signature + expiration
6. If valid: [Link] populated → proceed
7. If invalid: return 401 → frontend clears token → redirect to login
Follow-up: "How would you implement refresh tokens?"
Short-lived access tokens (5min) + long-lived refresh tokens (7d)
On 401, use refresh token to get new access token
If refresh fails, user must re-login
Q2: "How does rate limiting work in your system?"
Expected Answer:
1. Redis tracks usage per user per feature (24h window)
2. [Link](key) atomically increments counter
3. First increment: set 24h TTL
4. Check if count > PLAN_LIMITS[role]
5. If exceeded: return 429
6. If Redis unavailable: allow request (fail-open)
Follow-up: "What if someone spoofs their role?"
JWT signature would be invalid → request rejected
Role embedded in token → can't modify without signing key
If signing key compromised: critical issue → rotate secret
Q3: "Explain the chat system architecture"
Expected Answer:
1. User message → saved to DB immediately
2. Fetch full chat history (for context)
3. Build prompt: system + history + user message
4. Call Gemini API
5. Save AI response to DB
6. Return to frontend
Follow-up: "What if chat history is too long?"
Current: no size limit (could exceed token limits)
Better: implement context window management
Option A: Summarize old messages
Option B: Sliding window (last 10 messages)
Option C: Message pruning (delete oldest)
Q4: "How do you prevent unauthorized access?"
Expected Answer:
1. JWT verification on all protected routes
2. CORS whitelist (specific origin, not *)
3. Password hashing (bcryptjs 10 rounds)
4. Chat ownership verification (chatId validation)
5. Rate limiting (prevents brute force)
Follow-up: "What are the remaining vulnerabilities?"
Input validation lacking
SQL injection (mitigated by Prisma, but sanitize anyway)
XSS (React escapes by default, but validate user input)
CSRF (add CSRF tokens if using cookies)
Missing email verification
No 2FA
Q5: "How would you scale this to 1M users?"
Expected Answer:
Component Scaling Strategy
Database Read replicas, connection pooling, sharding by userId
Cache Redis cluster, CDN for static assets
API Horizontal scaling with load balancer
Storage S3 for images/documents, CDN
Monitoring APM tools (DataDog, New Relic), alerts
Cost Database query optimization, cache TTL tuning
Specific Example:
// Current: Single Stripe instance
const stripe = new Stripe(key);
// Scaled: Connection pooling
const stripePool = new Map(); // Cache instances
const getStripe = () => [Link](key) || new Stripe(key);
Q6: "Why Prisma over raw SQL?"
Expected Answer:
Type safety (auto-generated types)
Migration management (database versioning)
No SQL injection (parameterized queries)
Relationship handling (automatic JOINs)
Migration rollback support
Trade-off: Slight performance overhead (negligible for this scale)
Q7: "Why Redis over just using the database?"
Expected Answer:
Rate limiting requires microsecond latency
Database queries: 10-50ms per check
Redis: <5ms (1000x faster)
TTL support: Redis native, DB requires cleanup jobs
Example:
1M users/day, 100 queries each = 100M rate limit checks
Database: 50ms * 100M = 5,000,000 seconds = 57 days of CPU!
Redis: 5ms * 100M = 500,000 seconds = 5.7 days of CPU
Q8: "Walk me through payment flow and webhook handling"
Expected Answer:
1. User clicks upgrade → checkout session created
2. Stripe returns URL → user redirected
3. User enters card → Stripe processes
4. On success:
Immediate: Redirect to /success
Async: Webhook fires
Webhook: Update [Link] = "PREMIUM"
5. If webhook fails: retry 3x, then manual intervention
Problem in Current Code:
Checkout session created correctly
Webhook handler incomplete
No webhook signature verification
No database update on success
How to Fix:
[Link]('/webhook', [Link]({type: 'application/json'}), (req, res) => {
const sig = [Link]['stripe-signature'];
// Verify signature (prevents tampering)
let event;
try {
event = [Link](
[Link],
sig,
[Link].STRIPE_WEBHOOK_SECRET
);
} catch (err) {
return [Link](400).send(`Webhook Error: ${[Link]}`);
}
// Handle event
if ([Link] === '[Link]') {
const session = [Link];
const { userId, plan } = [Link];
// Update database
await [Link]({
where: { id: userId },
data: { role: [Link]() }
});
// Create subscription record
await [Link]({
data: { userId, plan: [Link]() }
});
// Send email (future)
}
[Link]({received: true});
});
Q9: "What's your biggest challenge in this project?"
Good Answer:
1. Incomplete Webhook: Payments redirect but don't update user roles (fixable)
2. No Context Management: Long chats might exceed token limits
3. Input Validation: Missing comprehensive validation
4. Error Handling: Generic 500 errors (should be specific)
5. Monitoring: No logging/APM in production
Then offer solutions:
// Current workaround: Manual DB update after payment
// Better: Implement webhook handler (as shown above)
// Current: Store ALL messages (unlimited context)
// Better: Implement sliding window or message summarization
// Current: Minimal validation
// Better: Add joi/zod schemas for all inputs
// Current: [Link]()
// Better: Integrate Winston/Pino logging + DataDog
Q10: "How would you handle concurrent chat requests?"
Expected Answer:
// Problem: User sends 2 messages rapidly
// ├─ Message 1: POST /api/chat
// └─ Message 2: POST /api/chat
// Current behavior: Both might create new chat (race condition)
// Fix: Use database unique constraint + transactions
// Better approach with race condition handling:
try {
const chat = await [Link]({
where: { id: chatId }
});
if (!chat) {
// Potential race: another request created chat
// Solution: unique constraint + retry
}
// Use transaction to ensure atomicity
const [userMsg, aiMsg] = await prisma.$transaction([
[Link]({ ... }),
// Wait for AI response
[Link]({ ... })
]);
} catch (error) {
if ([Link] === 'P2002') { // Unique constraint
// Retry once
}
}
Q11: "How would you add email notifications?"
Expected Answer:
// Technology: SendGrid, Mailgun, or Resend
// After successful payment:
await sendEmail({
to: [Link],
subject: "Payment Successful",
template: "payment-success",
data: {
plan: "PREMIUM",
expiryDate: calculateExpiry(),
},
});
// Implementation:
// 1. Define email templates (HTML)
// 2. Integrate email service API
// 3. Add to webhook handler (after payment processed)
// 4. Add confirmation emails on signup
// 5. Add forgot password flow
Q12: "How would you handle user roles/permissions?"
Expected Answer:
// Current: role determines rate limits
// Extend to feature access:
const checkPermission = (requiredRole) => {
return (req, res, next) => {
const userRole = [Link];
const roleHierarchy = {
FREE: 0,
PREMIUM: 1,
ENTERPRISE: 2,
};
if (roleHierarchy[userRole] < roleHierarchy[requiredRole]) {
return [Link](403).json({
error: "Upgrade required",
});
}
next();
};
};
// Usage:
[Link](
"/api/advanced-feature",
authMiddleware,
checkPermission("PREMIUM"),
handler
);
Code Quality & Best Practices
What's Done Well
JWT authentication properly implemented
Prisma migrations tracked (3 versions)
Rate limiting with Redis (fail-open)
Specialized AI prompts (Indian law focus)
Database retry logic with exponential backoff
CORS properly configured (not *)
Modular code structure
What Needs Improvement ⚠
1. Webhook Handling: Payment completion not triggering user role update
2. Error Handling: Generic error responses, should be specific
3. Input Validation: Missing comprehensive validation
4. Logging: Only [Link], no persistent logs
5. Testing: No test files visible
6. Documentation: This guide is needed because code comments are sparse
7. Context Management: No checks for chat context size
8. Environment Config: Limits hardcoded, should be env variables
Improvements to Discuss in Interview
// Current
if (!message || [Link]() === "") {
return [Link](400).json({ error: "Message is required" });
}
// Better
import { z } from "zod";
const messageSchema = [Link]({
message: [Link]().min(1).max(10000),
chatId: [Link]().uuid().optional(),
});
[Link]("/api/chat", authMiddleware, async (req, res) => {
try {
const validated = [Link]([Link]);
// Proceed with validation passed
} catch (error) {
return [Link](400).json({
error: "Invalid input",
details: [Link],
});
}
});
Quick Reference
API Endpoints
Endpoint Method Auth Rate Limit Purpose
/auth/register POST None Register new user
/auth/login POST None Login user
/auth/me GET None Get current user
/api/chat POST Yes Send message to AI
/api/chat/messages GET No Get chat history
/create-checkout-session POST None Create payment session
/webhook POST None Stripe webhook (incomplete)
Environment Variables Needed
DATABASE_URL - PostgreSQL connection string
STRIPE_SECRET_KEY - Stripe API key
STRIPE_WEBHOOK_SECRET - Webhook signing secret
GEMINI_API_KEY - Google Gemini API key
JWT_SECRET - Token signing secret (min 32 chars)
REDIS_URL - Upstash Redis connection
CLIENT_URL - Frontend domain for CORS
PORT - Server port (default 5000)
NODE_ENV - production/development
Tech Stack Summary
Frontend:
- React 19
- Vite (build tool)
- React Router v7
- Tailwind CSS
- JavaScript (ES6+)
Backend:
- [Link]
- [Link]
- PostgreSQL
- Prisma ORM
- Google Gemini AI
- Stripe
- Redis (Upstash)
- JWT tokens
Deployment:
- Render (backend)
- Vercel (frontend)
Final Interview Tips
Before Interview
1. Know your code cold: Be able to explain every file
2. Understand trade-offs: Why this tech over alternatives?
3. Know limitations: What would you improve?
4. Practice deployment: Understand Render/Vercel setup
5. Trace flows mentally: Can you walk through request/response completely?
During Interview
1. Start high-level: System design diagram first
2. Then zoom in: Specific implementation details
3. Be honest about gaps: Webhook incomplete? Say so and suggest fix
4. Show improvement mindset: "I would implement X differently because..."
5. Ask clarifying questions: "Which part of authentication interests you?"
Common Traps to Avoid
Claiming features that aren't implemented (webhook)
Not understanding why you chose technologies
Not being able to explain error handling
Overestimating code quality
Not knowing deployment process
Things That Impress Interviewers
Honest assessment of code
Understanding security implications
Scaling consciousness ("If we had 1M users...")
Specific examples ("Our rate limiting uses Redis because...")
Reading between the lines (understanding business)
Good luck! You've got solid fundamentals. Focus on understanding WHY each decision was made.