CATB MVP Technical Specifications
CATB MVP Technical Specifications
Table of Contents
- System Overview
- Architecture
- Frontend Architecture
- Backend Architecture
- Database Schema
- Authentication System
- AI Integration
- Infrastructure & Deployment
- Security
- API Reference
- Development & Operations
- Cost Structure
System Overview
Project Description
catb (Cat Health Chatbot) is a production-ready full-stack application providing empathetic cat health guidance through an AI-powered chat interface. The system uses Anthropic’s Claude Sonnet 4 with a custom “Mu” system prompt to deliver personalized, context-aware responses using a progressive discovery approach.
Brand Name: Ask Mü Production URL: https://askmu.live Status: ✅ Fully Operational Deployment Date: September 2025 Last Major Update: October 2025 (Session-email association fix, ProfileService integration)
Current Production Status
- Frontend: React SPA deployed in Docker container (port 3001)
- Backend: Express 5.1.0 API deployed in Docker container (port 3000)
- Database: Supabase PostgreSQL (connected and operational)
- AI: Claude Sonnet 4 with Mu system prompt v3
- Infrastructure: Hostinger VPS at 89.116.170.226
- Domain: askmu.live with SSL (Let’s Encrypt)
- Authentication: Magic link email system with 24-hour sessions
Technology Stack Summary
| Layer | Technology | Version | Purpose |
|---|---|---|---|
| Frontend | React | 18+ | UI framework |
| Tailwind CSS | 3+ | Styling | |
| Radix UI | Latest | Accessible components | |
| Framer Motion | Latest | Animations | |
| Vite | Latest | Build tool | |
| Backend | Node.js | 20 LTS | Runtime |
| Express | 5.1.0 | Web framework | |
| @anthropic-ai/sdk | 0.17.1 | Claude API client | |
| @langchain/anthropic | 0.3.28 | LangChain integration | |
| @supabase/supabase-js | 2.39.0 | Database client | |
| Resend | 2.1.0 | Email service | |
| Database | PostgreSQL | 15 | Supabase managed |
| Infrastructure | Ubuntu | 24.04 LTS | VPS OS |
| Nginx | Latest | Reverse proxy | |
| Docker | Latest | Containerization | |
| Let’s Encrypt | - | SSL certificates |
Access Points
- Production Frontend: https://askmu.live/
- Production API: https://askmu.live/api/
- Health Check: https://askmu.live/api/health
- VPS: 89.116.170.226 (SSH access)
- Email: mu@transactional.askmu.live
Architecture
High-Level System Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Internet │
│ (HTTPS - Port 443) │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Hostinger VPS │
│ (89.116.170.226) │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Nginx Reverse Proxy (Ports 80/443) │ │
│ │ • SSL Termination (Let's Encrypt) │ │
│ │ • Request Routing │ │
│ │ • Security Headers │ │
│ └──────┬────────────────────────────────┬──────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Frontend │ │ Backend API │ │
│ │ Docker Container │ │ Docker Container │ │
│ │ Port 3001 │ │ Port 3000 │ │
│ │ (React SPA) │◄──────────►│ (Express 5.1.0) │ │
│ └──────────────────┘ └────────┬─────────┘ │
│ │ │
└────────────────────────────────────────────┼─────────────────────┘
│
┌────────────────────────┼──────────────────┐
│ │ │
▼ ▼ ▼
┌────────────────┐ ┌────────────────┐ ┌──────────────┐
│ Supabase │ │ Anthropic │ │ Resend │
│ PostgreSQL │ │ Claude API │ │ Email API │
│ (Database) │ │ (AI Service) │ │ (Magic Links)│
└────────────────┘ └────────────────┘ └──────────────┘
Component Relationships
Frontend (React)
│
├──> State Management (React Hooks + localStorage)
├──> UI Components (Radix UI + Tailwind)
├──> API Client (fetch)
│
└──> Backend API (Express)
│
├──> Authentication Layer
│ ├──> Magic Link System (Resend)
│ ├──> Session Management (Supabase)
│ └──> Token Validation
│
├──> Chat Endpoint
│ ├──> Mu Prompt System
│ │ ├──> Context Analyzer
│ │ ├──> Memory System
│ │ └──> LangChain User Profiler
│ │
│ ├──> Claude AI Integration
│ ├──> ProfileService
│ └──> Conversation History
│
├──> Database Layer (db.js)
│ ├──> User Management
│ ├──> Session Management
│ ├──> Message Storage
│ └──> Context Tracking
│
└──> Enhanced Systems
├──> Pattern Detection Engine
├──> Feedback System
└──> Smart Deployment System
Data Flow
User → Frontend → Nginx → Backend API → Claude AI
↓
Supabase DB
↓
Backend API → Frontend → User
Detailed Flow:
- User sends message from React interface
- Frontend sends authenticated request to backend
- Nginx routes
/api/*requests to backend (port 3000) - Backend validates session token via Supabase
- Backend retrieves conversation history
- Mu Prompt System generates dynamic prompt
- LangChain profiler analyzes user communication style
- Backend sends context + message to Claude API
- Claude generates empathetic response
- Backend saves message pair to database
- Backend returns response to frontend
- Frontend displays reply with animations
Frontend Architecture
Technology Stack
- Framework: React 18+
- Styling: Tailwind CSS 3+
- UI Components: Radix UI (accessible primitives)
- Animations: Framer Motion
- Build Tool: Vite
- Deployment: Docker container (Nginx serving static files)
Application Structure
catb-frontend/
├── src/
│ ├── components/ # Reusable UI components
│ │ ├── ChatBubble.jsx
│ │ ├── ChatInput.jsx
│ │ ├── LoadingIndicator.jsx
│ │ └── EmergencyAlert.jsx
│ │
│ ├── pages/ # Application pages
│ │ ├── Home.jsx
│ │ ├── Chat.jsx
│ │ └── Auth.jsx
│ │
│ ├── lib/ # Utilities and helpers
│ │ ├── api.js # API client
│ │ └── auth.js # Auth helpers
│ │
│ ├── hooks/ # Custom React hooks
│ │ ├── useAuth.js
│ │ └── useChat.js
│ │
│ ├── styles/ # Global styles
│ │ └── globals.css
│ │
│ ├── App.jsx # Main app component
│ └── main.jsx # Entry point
│
├── public/ # Static assets
│ ├── favicon.svg
│ ├── mascot.png
│ └── doctor.gif
│
├── Dockerfile # Frontend Docker config
├── nginx.conf # Nginx configuration
├── tailwind.config.js # Tailwind configuration
├── vite.config.js # Vite configuration
└── package.json
Key Features
Mobile-First Responsive Design
- Optimized for mobile devices (primary use case: worried pet owners)
- Touch-friendly UI elements
- Responsive breakpoints for tablet and desktop
State Management
- React hooks for local state
- localStorage for session persistence
- No external state management library (keeping it simple)
API Integration
// API client pattern
const apiClient = {
baseURL: 'https://askmu.live/api',
async chat(message, sessionToken) {
const response = await fetch(`${this.baseURL}/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message, sessionToken })
});
return response.json();
}
};
Docker Containerization
Dockerfile (Multi-stage build):
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Deployment:
- Container name:
catb-frontend - Internal port: 80
- External port: 3001
- Served by Nginx within container
- Static assets included in image
Backend Architecture
Express 5.1.0 Application Structure
catb-backend/
├── server.js # Main Express application
├── db.js # Database abstraction layer
├── auth-routes.js # Authentication endpoints
├── auth-utils.js # Auth utilities
├── email-service.js # Magic link email service
├── profile-service.js # User/cat profile management
│
├── lib/ # Core libraries
│ ├── mu-prompt-system.js # Main prompt orchestrator
│ ├── conversation-context-analyzer.js
│ ├── conversation-memory-system.js
│ ├── simple-template-engine.js
│ ├── enhanced-feedback-system.js
│ ├── pattern-detection-engine.js
│ ├── smart-deployment-system.js
│ └── langchain/
│ └── langchain-user-profiler.js
│
├── prompts/ # PromptL templates
│ ├── base-prompt.promptl
│ └── [additional templates]
│
├── migrations/ # Database migrations
│ └── 000-complete-fresh-schema.sql
│
├── scripts/ # Utility scripts
│ ├── deploy-to-vps.sh
│ ├── db-connection-helper.js
│ ├── execute-sql.js
│ └── run-daily-workflow.js
│
├── test/ # Test files
│ ├── test-session-email-fix.js
│ └── comprehensive-auth-test.js
│
├── Dockerfile
├── docker-compose.yml
├── package.json
└── .env
Core Dependencies
{
"dependencies": {
"express": "^5.1.0",
"@anthropic-ai/sdk": "^0.17.1",
"@langchain/anthropic": "^0.3.28",
"@langchain/core": "^0.3.77",
"@langchain/langgraph": "^0.4.9",
"@supabase/supabase-js": "^2.39.0",
"cors": "^2.8.5",
"helmet": "^7.1.0",
"express-rate-limit": "^7.1.5",
"resend": "^2.1.0",
"redis": "^5.8.2",
"pg": "^8.16.3",
"validator": "^13.15.15",
"zod": "^3.25.76"
}
}
API Endpoints
Public Endpoints
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
| GET | /health |
System health check | No |
| POST | /session/create |
Create authenticated session | No |
| POST | /auth/request-invite |
Request magic link | No |
| GET | /auth/verify |
Verify magic link token | No |
Protected Endpoints (Require sessionToken)
| Method | Endpoint | Description | Rate Limit |
|---|---|---|---|
| POST | /chat or /api/chat |
Main chat endpoint | 10 req/min |
| POST | /api/feedback/submit |
Submit user feedback | 10 req/min |
| GET | /api/dashboard/metrics |
System metrics | 10 req/min |
| GET | /api/patterns/recent |
Recent patterns | 10 req/min |
Middleware Stack
// server.js middleware chain
app.use(helmet()); // Security headers
app.use(cors(corsOptions)); // CORS policy
app.use(express.json()); // JSON body parser
app.use(rateLimiter); // Rate limiting (Redis + memory fallback)
app.use(inputValidator); // Input sanitization
Session Management
Modern Authentication Flow (October 2025 Update):
- ✅ Uses
user_sessionstable with proper foreign keys - ✅ All sessions linked to email addresses
- ❌ Legacy
sessionstable fallback removed - Session validation:
db.validateUserSession(sessionToken)
// Session validation (server.js)
const userSession = await db.validateUserSession(sessionToken);
if (!userSession.valid) {
return res.status(401).json({
error: 'Invalid or expired session'
});
}
sessionId = userSession.sessionId;
userId = userSession.userId; // Always available!
Claude AI Integration
Configuration:
import Anthropic from '@anthropic-ai/sdk';
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY
});
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 300,
messages: conversationHistory
});
Cost Control:
- Max tokens per response: 300
- Input tokens: $3 per 1M tokens
- Output tokens: $15 per 1M tokens
- Average cost per interaction: $0.001-0.005
ProfileService Integration
Features:
- User profile management (name, email, preferences)
- Cat profile management (name, age, breed, gender, medical history)
- Profile restoration across sessions
- Validation with Zod schemas
- LangChain integration for behavioral profiling
// ProfileService usage
const profiles = await profileService.restoreProfileForSession(userId);
// Returns: { userProfile, catProfile }
Security Features
- Input Validation: All inputs sanitized with
validatorlibrary - Rate Limiting: Redis-backed with in-memory fallback (10 req/min)
- CORS: Strict origin policy (
https://askmu.live) - Helmet: Security headers (CSP, HSTS, X-Frame-Options)
- Container Security: Non-root user, read-only filesystem
- Port Binding: Backend only accessible via nginx (127.0.0.1:3000)
Database Schema
Supabase PostgreSQL Configuration
Connection:
- URL:
https://PROJECT_REF.supabase.co - Client: @supabase/supabase-js (JavaScript CRUD)
- Direct: PostgreSQL client (DDL operations)
- Authentication: Service role key
Core Tables
users
CREATE TABLE users (
user_id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
last_login TIMESTAMP WITH TIME ZONE,
is_active BOOLEAN DEFAULT true,
invitation_sent_at TIMESTAMP WITH TIME ZONE,
first_login_at TIMESTAMP WITH TIME ZONE,
CONSTRAINT email_format CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$')
);
CREATE INDEX idx_users_email ON users(email);
magic_links
CREATE TABLE magic_links (
link_id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
token TEXT UNIQUE NOT NULL,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
used_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ip_address INET,
user_agent TEXT,
CONSTRAINT token_length CHECK (length(token) >= 32)
);
user_sessions
CREATE TABLE user_sessions (
session_id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
session_token TEXT UNIQUE NOT NULL,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
last_activity TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ip_address INET,
user_agent TEXT,
CONSTRAINT session_token_length CHECK (length(session_token) >= 32)
);
CREATE INDEX idx_user_sessions_token ON user_sessions(session_token);
CREATE INDEX idx_user_sessions_user ON user_sessions(user_id, is_active);
CREATE INDEX idx_user_sessions_active ON user_sessions(is_active, expires_at);
messages
CREATE TABLE messages (
message_id SERIAL PRIMARY KEY,
session_id INTEGER REFERENCES user_sessions(session_id) ON DELETE CASCADE,
user_id INTEGER REFERENCES users(user_id) ON DELETE CASCADE,
sender VARCHAR(20) NOT NULL CHECK (sender IN ('user', 'assistant')),
message_text TEXT NOT NULL,
token_count INTEGER,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
message_metadata JSONB DEFAULT '{}'::jsonb
);
CREATE INDEX idx_messages_user ON messages(user_id, created_at DESC);
CREATE INDEX idx_messages_session ON messages(session_id, created_at DESC);
conversation_context
CREATE TABLE conversation_context (
context_id SERIAL PRIMARY KEY,
session_id INTEGER REFERENCES user_sessions(session_id) ON DELETE CASCADE,
message_id INTEGER,
context_type VARCHAR(50) NOT NULL DEFAULT 'general',
context_data JSONB NOT NULL DEFAULT '{}',
user_id INTEGER REFERENCES users(user_id),
cat_name VARCHAR(100),
cat_age INTEGER,
cat_breed VARCHAR(100),
cat_gender VARCHAR(20),
current_symptoms TEXT[],
anxiety_level VARCHAR(20),
experience_level VARCHAR(20),
communication_style VARCHAR(20),
emotional_state VARCHAR(30),
conversation_phase VARCHAR(30),
question_count INTEGER DEFAULT 0,
topics_discussed TEXT[],
urgency_indicators TEXT[],
information_completeness JSONB DEFAULT '{}',
confidence_score DECIMAL(3,2),
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT conversation_context_session_message_type_key
UNIQUE(session_id, message_id, context_type)
);
CREATE INDEX idx_conversation_context_session ON conversation_context(session_id);
CREATE INDEX idx_conversation_context_user ON conversation_context(user_id);
Profile Tables
user_profiles
CREATE TABLE user_profiles (
profile_id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
full_name VARCHAR(255),
timezone VARCHAR(50),
notification_preferences JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_user_profile UNIQUE(user_id)
);
cat_profiles
CREATE TABLE cat_profiles (
cat_id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
cat_name VARCHAR(100) NOT NULL,
cat_age INTEGER,
cat_breed VARCHAR(100),
cat_gender VARCHAR(20),
medical_history JSONB DEFAULT '[]',
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
Enhanced Tables
feedback_submissions
CREATE TABLE feedback_submissions (
feedback_id SERIAL PRIMARY KEY,
session_id INTEGER REFERENCES user_sessions(session_id),
user_id INTEGER REFERENCES users(user_id),
feedback_type VARCHAR(50) NOT NULL,
feedback_data JSONB NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
detected_patterns
CREATE TABLE detected_patterns (
pattern_id SERIAL PRIMARY KEY,
pattern_type VARCHAR(100) NOT NULL,
pattern_data JSONB NOT NULL,
confidence_score DECIMAL(5,4),
detected_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
Cleanup Functions
-- Cleanup expired sessions
CREATE OR REPLACE FUNCTION cleanup_expired_user_sessions()
RETURNS void AS $$
BEGIN
UPDATE user_sessions
SET is_active = false
WHERE expires_at < CURRENT_TIMESTAMP AND is_active = true;
END;
$$ LANGUAGE plpgsql;
-- Cleanup expired magic links
CREATE OR REPLACE FUNCTION cleanup_expired_magic_links()
RETURNS void AS $$
BEGIN
DELETE FROM magic_links
WHERE expires_at < CURRENT_TIMESTAMP;
END;
$$ LANGUAGE plpgsql;
Authentication System
Magic Link Authentication Flow
┌─────────┐ ┌──────────┐
│ User │ │ Backend │
└────┬────┘ └────┬─────┘
│ │
│ 1. Request Access (email) │
├─────────────────────────────────────────────────────────────►
│ │
│ 2. Create user
│ 3. Generate token
│ 4. Save magic_link
│ │
│ 5. Email with magic link ◄───────────────────────────────────┤
├──────────────────────────┐ │
│ │ 6. Click link │
│◄─────────────────────────┘ │
│ │
│ 7. Verify token │
├─────────────────────────────────────────────────────────────►
│ │
│ 8. Validate token
│ 9. Create session
│ 10. Generate sessionToken
│ │
│ 11. Redirect with sessionToken ◄─────────────────────────────┤
│ │
│ 12. Store in localStorage │
├──────────────────────────┐ │
│◄─────────────────────────┘ │
│ │
│ 13. Chat with sessionToken │
├─────────────────────────────────────────────────────────────►
Endpoints
1. Request Magic Link
POST /auth/request-invite
// Request
{
"email": "user@example.com"
}
// Response
{
"success": true,
"message": "Check your email for the invitation link"
}
2. Verify Magic Link
GET /auth/verify?token=MAGIC_LINK_TOKEN
- Validates token from database
- Checks expiration (1 hour)
- Creates user session (24 hours)
- Redirects to frontend with sessionToken
3. Create Session (Direct API)
POST /session/create
// Request
{
"email": "user@example.com"
}
// Response
{
"sessionToken": "64-character-hex-string",
"expiresAt": "2025-10-02T18:30:00.000Z",
"sessionId": 171,
"userId": 425,
"isNewUser": false,
"profiles": {
"userProfile": { ... },
"catProfile": { ... }
}
}
Session Management
Session Lifecycle:
- Creation: Generated when user verifies magic link or via
/session/create - Duration: 24 hours from creation
- Storage:
user_sessionstable in database - Client Storage: localStorage in browser
- Validation: Every API request validates via
db.validateUserSession() - Expiration: Automatic after 24 hours
- Cleanup: Background job marks expired sessions inactive
Token Format:
- Length: 64 characters
- Type: Hexadecimal string
- Generation:
crypto.randomBytes(32).toString('hex') - Uniqueness: Database constraint ensures uniqueness
Session Validation (server.js):
// Modern session validation (October 2025)
const userSession = await db.validateUserSession(sessionToken);
if (!userSession.valid) {
return res.status(401).json({
error: 'Invalid or expired session',
message: 'Please request a new invitation to access Ask Mü'
});
}
// userSession contains:
// - valid: true/false
// - sessionId: integer
// - userId: integer (always present!)
// - email: string (linked via users table)
Email Service (Resend)
Configuration:
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
await resend.emails.send({
from: 'mu@transactional.askmu.live',
to: userEmail,
subject: 'Your invitation to Ask Mü',
html: magicLinkEmailTemplate
});
DNS Records (configured at Namecheap):
- SPF:
v=spf1 include:_spf.resend.com ~all - DKIM: Resend-provided keys
- DMARC:
v=DMARC1; p=none
AI Integration
Anthropic Claude API
Model: claude-sonnet-4-20250514
API Version: @anthropic-ai/sdk 0.17.1
Max Tokens: 300 per response
Temperature: 0.7 (balanced creativity)
API Call Pattern:
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 300,
messages: conversationHistory, // Array of {role, content}
system: generatedPrompt // Dynamic Mu prompt
});
Mu Prompt System
Architecture:
MuPromptSystem (Orchestrator)
│
├──> ConversationContextAnalyzer
│ ├── Analyzes user messages
│ ├── Extracts cat profile info
│ ├── Detects urgency signals
│ └── Identifies communication style
│
├──> ConversationMemorySystem
│ ├── Retrieves conversation history
│ ├── Updates context in database
│ └── Manages conversation phases
│
├──> LangChainUserProfiler
│ ├── Behavioral profiling (2+ messages)
│ ├── Communication style detection
│ ├── Anxiety level assessment
│ └── Experience level classification
│
└──> SimpleTemplateEngine
├── Loads PromptL templates
├── Interpolates context data
└── Generates final prompt
Progressive Discovery Protocol:
- Max 2 Questions Per Response: Core constraint to avoid overwhelming users
- Context-Aware: Analyzes existing information before asking
- Urgency-Sensitive: Prioritizes emergency symptoms
- Adaptive Tone: Adjusts based on user’s communication style
Example Prompt Structure:
You are Mu, an empathetic cat health assistant.
CURRENT CONTEXT:
- Cat: Luna, 3 years old, female
- Symptoms: vomiting, lethargy
- Anxiety Level: high
- Communication Style: emotional support seeker
CONVERSATION PHASE: symptom_gathering
QUESTIONS ASKED: 1/2
INSTRUCTIONS:
- Ask maximum 2 questions
- Prioritize emergency symptoms
- Use warm, empathetic tone
- Provide actionable guidance
LangChain Integration
User Profiling (lib/langchain/langchain-user-profiler.js):
// Triggered after 2+ user messages
const profile = await langchainProfiler.analyzeUserProfile(
conversationHistory,
currentMessage
);
// Returns:
{
profile: {
communicationStyle: 'emotional' | 'factual' | 'low-literacy',
anxietyLevel: 'low' | 'moderate' | 'high' | 'panic',
experienceLevel: 'first-time' | 'beginner' | 'experienced',
preferredResponseStyle: 'concise' | 'detailed',
confidenceLevel: 'low' | 'moderate' | 'high',
learningPreference: 'step-by-step' | 'overview'
},
confidence: {
overall: 0.85,
factorScores: { ... }
},
personalityProfile: {
name: 'Anxious First-Timer',
traits: [...],
responseGuidance: '...'
},
adaptationTriggers: [...]
}
Early Profiling (October 2025):
- Enabled by default
- Activates at 2nd user message (previously 3rd)
- Provides faster personalization
- Fallback to basic analysis if LangChain fails
Conversation Context
Tracked Data:
- Cat Profile: name, age, breed, gender
- Symptoms: current and historical
- Urgency Signals: emergency, urgent, routine indicators
- User Traits: anxiety level, experience level, communication style
- Conversation State: phase, question count, topics discussed
- Information Completeness: gaps in critical information
Context Updates:
await memorySystem.updateConversationContext(sessionId, {
catProfile: { name: 'Luna', age: 3 },
symptoms: ['vomiting', 'lethargy'],
anxietyLevel: 'high',
conversationPhase: 'symptom_gathering',
questionCount: 1
});
Infrastructure & Deployment
VPS Configuration
Provider: Hostinger IP Address: 89.116.170.226 Operating System: Ubuntu 24.04 LTS Resources: Shared VPS plan (~$20-50/month)
Installed Software:
- Docker & Docker Compose
- Nginx
- Certbot (Let’s Encrypt)
- UFW (Uncomplicated Firewall)
- fail2ban (brute force protection)
Port Configuration
| Port | Service | Binding | Purpose |
|---|---|---|---|
| 22 | SSH | 0.0.0.0 | Server access |
| 80 | Nginx | 0.0.0.0 | HTTP → HTTPS redirect |
| 443 | Nginx | 0.0.0.0 | HTTPS termination |
| 3000 | Backend | 127.0.0.1 | Internal only (via nginx) |
| 3001 | Frontend | 127.0.0.1 | Internal only (via nginx) |
Nginx Configuration
File: /etc/nginx/sites-enabled/askmu-live
Routing Rules:
# API endpoints → Backend (port 3000)
location ~ ^/api/(.*)$ {
proxy_pass http://127.0.0.1:3000/$1; # Rewrite /api/* → /*
}
location /auth/ {
proxy_pass http://127.0.0.1:3000;
}
location /session/ {
proxy_pass http://127.0.0.1:3000;
}
location /chat {
proxy_pass http://127.0.0.1:3000;
}
location /health {
proxy_pass http://127.0.0.1:3000;
}
# Frontend → React SPA (port 3001)
location / {
proxy_pass http://127.0.0.1:3001;
try_files $uri $uri/ /index.html; # SPA routing
}
# Static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
proxy_pass http://127.0.0.1:3001;
expires 1y;
add_header Cache-Control "public, immutable";
}
Docker Configuration
Backend (docker-compose.yml)
version: '3.8'
services:
app:
build: .
container_name: catb-api
restart: unless-stopped
ports:
- "127.0.0.1:3000:3000" # Bind to localhost only
environment:
- NODE_ENV=production
- PORT=3000
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
- SUPABASE_URL=${SUPABASE_URL}
- SUPABASE_SERVICE_KEY=${SUPABASE_SERVICE_KEY}
- RESEND_API_KEY=${RESEND_API_KEY}
- FRONTEND_URL=https://askmu.live
volumes:
- ./logs:/app/logs
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://localhost:3000/health')"]
interval: 30s
timeout: 10s
retries: 3
Frontend (Dockerfile)
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Deployment Commands:
# Backend
cd /root/catb/
docker-compose down
docker-compose build --no-cache
docker-compose up -d
# Frontend
cd /root/catb-frontend/
docker stop catb-frontend
docker rm catb-frontend
docker build -t catb-frontend . --no-cache
docker run -d --name catb-frontend --restart unless-stopped -p 127.0.0.1:3001:80 catb-frontend
SSL/TLS Configuration
Provider: Let’s Encrypt
Auto-Renewal: Certbot cron job
Certificate Path: /etc/letsencrypt/live/askmu.live/
Initial Setup:
certbot --nginx -d askmu.live -d www.askmu.live
Renewal:
certbot renew # Auto-scheduled via cron
Deployment Script
File: scripts/deploy-to-vps.sh
#!/bin/bash
# Deployment script for catb backend
VPS="root@89.116.170.226"
APP_DIR="/root/catb"
# Sync files
rsync -av --exclude 'node_modules' --exclude '.git' \
./ ${VPS}:${APP_DIR}/
# Deploy on VPS
ssh ${VPS} << 'EOF'
cd /root/catb
docker-compose down
docker-compose build --no-cache
docker-compose up -d
docker ps | grep catb
EOF
echo "✅ Deployment complete"
Directory Structure (VPS)
/root/
├── catb/ # Backend directory
│ ├── server.js
│ ├── db.js
│ ├── lib/
│ ├── prompts/
│ ├── migrations/
│ ├── scripts/
│ ├── docker-compose.yml
│ ├── Dockerfile
│ ├── .env
│ └── package.json
│
├── catb-frontend/ # Frontend directory
│ ├── src/
│ ├── public/
│ ├── Dockerfile
│ ├── nginx.conf
│ └── package.json
│
└── scripts/ # System scripts
├── setup-ssl.sh
└── setup-domain.sh
Security
Authentication Security
-
Magic Link Tokens:
- 64-character cryptographically secure random strings
- 1-hour expiration
- Single-use only (marked as
used_atafter verification) - Stored with user_agent and IP for audit trail
-
Session Tokens:
- 64-character cryptographically secure random strings
- 24-hour expiration
- Validated on every request
- Linked to user_id (email association enforced)
-
Password-less Authentication:
- No passwords to leak or crack
- Email-based access control
- Reduces attack surface
Input Validation
Library: validator v13.15.15
import validator from 'validator';
// Email validation
const isValidEmail = validator.isEmail(email);
// Message sanitization
const sanitizedMessage = validator.escape(message);
validator.isLength(message, { max: 2000 });
// Token validation
const isValidToken = /^[a-f0-9]{64}$/.test(token);
Rate Limiting
Strategy: Redis-backed with in-memory fallback
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
const chatLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 10, // 10 requests per minute
store: new RedisStore({
client: redisClient,
prefix: 'rl:chat:'
}),
// Fallback to memory if Redis unavailable
skipFailedRequests: false,
standardHeaders: true,
legacyHeaders: false
});
app.post('/chat', chatLimiter, chatHandler);
Current Status:
- Redis not deployed (falls back to in-memory)
- Sufficient for single-instance VPS deployment
- Consider Redis when scaling horizontally
CORS Configuration
import cors from 'cors';
const corsOptions = {
origin: 'https://askmu.live', // Strict origin
credentials: true, // Allow cookies
methods: ['GET', 'POST', 'OPTIONS'],
allowedHeaders: ['Content-Type']
};
app.use(cors(corsOptions));
Security Headers (Helmet)
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"]
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
},
frameguard: {
action: 'deny'
}
}));
Container Security
-
Non-Root User:
RUN addgroup -g 1001 nodejs && \ adduser -S chatbot -u 1001 USER chatbot -
Read-Only Filesystem: Most directories read-only
-
Resource Limits:
deploy: resources: limits: cpus: '1' memory: 512M -
Network Isolation: Backend not exposed to internet (only via nginx)
Environment Variables
Location: /root/catb/.env
Permissions: 600 (owner read/write only)
Required Variables:
NODE_ENV=production
PORT=3000
ANTHROPIC_API_KEY=sk-ant-...
SUPABASE_URL=https://...supabase.co
SUPABASE_SERVICE_KEY=eyJ...
DATABASE_URL=postgresql://postgres:...
RESEND_API_KEY=re_...
FRONTEND_URL=https://askmu.live
Security Practices:
- Never commit
.envto git (.gitignore) - Use service role keys for backend (not anon keys)
- Rotate keys periodically
- Audit access logs
Firewall Configuration
UFW Rules:
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp # SSH
ufw allow 80/tcp # HTTP
ufw allow 443/tcp # HTTPS
ufw enable
API Reference
Authentication Endpoints
POST /session/create
Create authenticated session with email.
Request:
{
"email": "user@example.com"
}
Response (200):
{
"sessionToken": "a1b2c3...",
"expiresAt": "2025-10-02T18:30:00.000Z",
"sessionId": 171,
"userId": 425,
"isNewUser": false,
"profiles": {
"userProfile": {
"full_name": "Jane Doe",
"timezone": "America/New_York"
},
"catProfile": {
"cat_name": "Luna",
"cat_age": 3,
"cat_breed": "Domestic Shorthair"
}
}
}
Error Responses:
400: Invalid email format500: Database error
POST /auth/request-invite
Request magic link via email.
Request:
{
"email": "user@example.com"
}
Response (200):
{
"success": true,
"message": "Check your email for the invitation link"
}
GET /auth/verify
Verify magic link token.
Query Parameters:
token: Magic link token (64 characters)
Response:
- Redirects to frontend with sessionToken
- Error page if token invalid/expired
Chat Endpoints
POST /chat
Main chat endpoint for user messages.
Authentication: Required (sessionToken in body)
Request:
{
"message": "My cat has been vomiting",
"sessionToken": "a1b2c3..."
}
Response (200):
{
"reply": "I'm sorry to hear Luna isn't feeling well. How long has the vomiting been going on?",
"timestamp": "2025-10-01T18:30:00.000Z",
"sessionActive": true,
"usage": {
"input_tokens": 142,
"output_tokens": 183,
"estimated_cost": "0.000264"
}
}
Error Responses:
400: Missing message or invalid format401: Invalid or expired session429: Rate limit exceeded500: Server error
Rate Limit: 10 requests per minute
Health Check
GET /health
System health check (no authentication required).
Response (200):
{
"status": "healthy",
"timestamp": "2025-10-01T18:30:00.000Z",
"service": "catb-backend",
"database": {
"connected": true,
"status": "operational"
},
"anthropic": {
"configured": true,
"status": "ready"
},
"promptSystem": {
"status": "operational"
},
"security": {
"status": "HEALTHY",
"alerts": 0
}
}
Error Codes
| Code | Meaning | Common Causes |
|---|---|---|
| 400 | Bad Request | Missing required fields, invalid format |
| 401 | Unauthorized | Invalid/expired session token |
| 403 | Forbidden | Session expired (>24 hours) |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Database error, Claude API error |
| 503 | Service Unavailable | Database connection failed |
Development & Operations
Local Development Setup
Prerequisites
- Node.js 20 LTS
- npm 10+
- PostgreSQL 15+ (optional, can use Supabase)
Backend Setup
# Clone repository
git clone <repo-url>
cd catb-backend
# Install dependencies
npm install
# Configure environment
cp .env.example .env
# Edit .env with your API keys
# Run locally
npm start
# Server runs on http://localhost:3000
Frontend Setup
cd catb-frontend
# Install dependencies
npm install
# Run development server
npm run dev
# Frontend runs on http://localhost:5173
Testing
Test Scripts
# Backend tests
npm test # Run all tests
npm run test:watch # Watch mode
npm run test:coverage # Coverage report
# Integration tests
node test/test-session-email-fix.js
node test/comprehensive-auth-test.js
Testing Without curl
Important: curl commands are denied in this environment.
Alternative: Use Node.js test scripts:
# Start local server
PORT=3002 npm start
# Run test in another terminal
node test/test-session-email-fix.js
Example Test Script:
import fetch from 'node-fetch';
const API_BASE = 'http://localhost:3002';
async function testChat() {
// Create session
const sessionRes = await fetch(`${API_BASE}/session/create`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'test@example.com' })
});
const { sessionToken } = await sessionRes.json();
// Send chat message
const chatRes = await fetch(`${API_BASE}/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: 'Test message',
sessionToken
})
});
const { reply } = await chatRes.json();
console.log('Reply:', reply);
}
testChat();
Deployment Procedures
Backend Deployment
# Option 1: Use deployment script
./scripts/deploy-to-vps.sh
# Option 2: Manual deployment
rsync -av --exclude 'node_modules' ./ root@89.116.170.226:/root/catb/
ssh root@89.116.170.226 << 'EOF'
cd /root/catb
docker-compose down
docker-compose build --no-cache
docker-compose up -d
EOF
Critical: Always rebuild Docker images after code changes:
# ❌ WRONG - doesn't pick up code changes
docker-compose restart
# ✅ CORRECT - rebuilds with fresh code
docker-compose down && docker-compose build --no-cache && docker-compose up -d
Frontend Deployment
# Build and deploy frontend
cd ~/dev/catb-frontend/
npm run build
# Transfer to VPS
rsync -av --exclude 'node_modules' --exclude '.git' \
./ root@89.116.170.226:/root/catb-frontend/
# Rebuild container on VPS
ssh root@89.116.170.226 << 'EOF'
cd /root/catb-frontend
docker stop catb-frontend
docker rm catb-frontend
docker build -t catb-frontend . --no-cache
docker run -d --name catb-frontend --restart unless-stopped \
-p 127.0.0.1:3001:80 catb-frontend
EOF
Monitoring & Logging
View Logs
# Backend logs
ssh root@89.116.170.226 "docker logs catb-api --tail 100"
ssh root@89.116.170.226 "docker logs -f catb-api" # Follow
# Frontend logs
ssh root@89.116.170.226 "docker logs catb-frontend --tail 100"
# Nginx logs
ssh root@89.116.170.226 "tail -f /var/log/nginx/access.log"
ssh root@89.116.170.226 "tail -f /var/log/nginx/error.log"
Container Status
# Check all containers
ssh root@89.116.170.226 "docker ps -a"
# Check specific container
ssh root@89.116.170.226 "docker inspect catb-api"
Common Troubleshooting
Issue: Port Binding Conflict
Error: bind: address already in use
Solution: Remove port 80/443 from docker-compose.yml (nginx handles these):
ports:
- "127.0.0.1:3000:3000" # ✅ Correct
# - "80:80" # ❌ Remove this
Issue: Changes Not Appearing
Cause: Docker using cached images
Solution:
docker-compose down
docker-compose build --no-cache
docker-compose up -d
Issue: Authentication Failing
Common Mistakes:
- Token in Authorization header (should be in body)
- Using wrong field name (use
sessionToken, nottoken) - Session expired (>24 hours)
Correct Format:
// ✅ CORRECT
body: {
message: "user message",
sessionToken: "TOKEN"
}
// ❌ WRONG
headers: {
'Authorization': 'Bearer TOKEN'
}
Issue: Database Connection Error
Checks:
- Verify
.envhas correctSUPABASE_URLandSUPABASE_SERVICE_KEY - Check Supabase project status (dashboard)
- Test connection:
node scripts/test-db-connection.js - Ensure tables exist:
node scripts/execute-sql.js list
Issue: Blank Frontend Page
Symptoms: Page loads but shows blank screen, assets return HTML
Cause: Nginx misconfiguration or missing SPA routing
Solution: Ensure nginx.conf has:
location / {
proxy_pass http://127.0.0.1:3001;
try_files $uri $uri/ /index.html;
}
Database Management
Run Migrations
# Using Supabase Dashboard (recommended)
# 1. Go to SQL Editor
# 2. Copy contents of migrations/000-complete-fresh-schema.sql
# 3. Click "Run"
# Using direct PostgreSQL access (if DATABASE_URL configured)
node scripts/execute-sql.js file migrations/000-complete-fresh-schema.sql
Execute SQL Queries
# List tables
node scripts/execute-sql.js list
# Describe table
node scripts/execute-sql.js describe user_sessions
# Execute query
node scripts/execute-sql.js exec "SELECT COUNT(*) FROM messages"
Cost Structure
Monthly Costs (Production)
| Service | Tier | Cost | Notes |
|---|---|---|---|
| Hostinger VPS | Shared | $20-50 | Dual container deployment |
| Supabase | Free | $0 | 50k MAU, 500MB database |
| Resend Email | Free | $0 | 3k emails/month |
| Claude API | Pay-as-you-go | $30-100 | Depends on usage |
| Domain (askmu.live) | Namecheap | ~$1/mo | Paid annually (~$10/year) |
| SSL Certificate | Let’s Encrypt | $0 | Free auto-renewal |
| Total | - | $50-150/mo | Most variable is Claude API |
Claude API Cost Breakdown
Pricing (Sonnet 4):
- Input tokens: $3 per 1M tokens
- Output tokens: $15 per 1M tokens
Typical Chat Interaction:
- Input: ~200 tokens (conversation history + user message)
- Output: ~200 tokens (Mu response, 300 token max)
- Cost per interaction: ~$0.003-0.005
Monthly Estimates:
- 100 conversations/day: ~$15-30/month
- 500 conversations/day: ~$75-150/month
- 1000 conversations/day: ~$150-300/month
Cost Control Measures:
- Max tokens per response: 300 (hard limit)
- Conversation history pruning (last 10 messages)
- Efficient prompt design (minimal system prompt)
Scaling Considerations
Current Capacity (Single VPS):
- Concurrent users: ~50-100
- Daily conversations: ~500-1000
- Database: 500MB free tier (plenty of room)
When to Scale:
- VPS CPU consistently >70%
- Database approaching 500MB
- Email sending approaching 3k/month
- API costs exceeding budget
Scaling Options:
- Vertical: Upgrade VPS plan ($50-100/month)
- Horizontal: Add load balancer + multiple VPS instances
- Database: Upgrade Supabase to Pro ($25/month)
- Email: Upgrade Resend to paid tier ($20/month for 50k emails)
Appendix
Quick Reference
Production URLs:
- Frontend: https://askmu.live/
- API: https://askmu.live/api/
- Health: https://askmu.live/api/health
SSH Access:
ssh root@89.116.170.226
Common Commands:
# Restart backend
ssh root@89.116.170.226 "cd /root/catb && docker-compose restart"
# Rebuild backend
ssh root@89.116.170.226 "cd /root/catb && docker-compose down && docker-compose build --no-cache && docker-compose up -d"
# View logs
ssh root@89.116.170.226 "docker logs catb-api --tail 100"
# Check health
# Note: curl commands denied - use browser or test scripts
Environment Files:
- Backend:
/root/catb/.env - Frontend: Build-time only (Vite)
Recent Updates
October 2025:
- ✅ Session-email association fix (removed legacy fallback)
- ✅ ProfileService integration with LangChain
- ✅ Early profiling enabled (2-message threshold)
- ✅ Direct PostgreSQL access configured
- ✅ Test suite reorganization
September 2025:
- ✅ Express 5.1.0 upgrade (security remediation)
- ✅ Local asset deployment (replaced CDN)
- ✅ Enhanced pattern detection system
- ✅ Smart deployment system
- ✅ Feedback collection system
Support
For issues or questions:
- Check logs:
docker logs catb-api - Review health endpoint: https://askmu.live/api/health
- Check database connection:
node scripts/test-db-connection.js - Verify environment variables in
/root/catb/.env
Document Version: 1.0 Last Updated: October 1, 2025 System Status: ✅ Fully Operational