CATB MVP Technical Specifications

Technical specifications for the CATB MVP implementation

CATB MVP Technical Specifications

Table of Contents

  1. System Overview
  2. Architecture
  3. Frontend Architecture
  4. Backend Architecture
  5. Database Schema
  6. Authentication System
  7. AI Integration
  8. Infrastructure & Deployment
  9. Security
  10. API Reference
  11. Development & Operations
  12. 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


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:

  1. User sends message from React interface
  2. Frontend sends authenticated request to backend
  3. Nginx routes /api/* requests to backend (port 3000)
  4. Backend validates session token via Supabase
  5. Backend retrieves conversation history
  6. Mu Prompt System generates dynamic prompt
  7. LangChain profiler analyzes user communication style
  8. Backend sends context + message to Claude API
  9. Claude generates empathetic response
  10. Backend saves message pair to database
  11. Backend returns response to frontend
  12. 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_sessions table with proper foreign keys
  • ✅ All sessions linked to email addresses
  • ❌ Legacy sessions table 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

  1. Input Validation: All inputs sanitized with validator library
  2. Rate Limiting: Redis-backed with in-memory fallback (10 req/min)
  3. CORS: Strict origin policy (https://askmu.live)
  4. Helmet: Security headers (CSP, HSTS, X-Frame-Options)
  5. Container Security: Non-root user, read-only filesystem
  6. 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);
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

┌─────────┐                                                    ┌──────────┐
│  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

POST /auth/request-invite

// Request
{
  "email": "user@example.com"
}

// Response
{
  "success": true,
  "message": "Check your email for the invitation 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:

  1. Creation: Generated when user verifies magic link or via /session/create
  2. Duration: 24 hours from creation
  3. Storage: user_sessions table in database
  4. Client Storage: localStorage in browser
  5. Validation: Every API request validates via db.validateUserSession()
  6. Expiration: Automatic after 24 hours
  7. 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:

  1. Max 2 Questions Per Response: Core constraint to avoid overwhelming users
  2. Context-Aware: Analyzes existing information before asking
  3. Urgency-Sensitive: Prioritizes emergency symptoms
  4. 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

  1. Magic Link Tokens:

    • 64-character cryptographically secure random strings
    • 1-hour expiration
    • Single-use only (marked as used_at after verification)
    • Stored with user_agent and IP for audit trail
  2. Session Tokens:

    • 64-character cryptographically secure random strings
    • 24-hour expiration
    • Validated on every request
    • Linked to user_id (email association enforced)
  3. 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

  1. Non-Root User:

    RUN addgroup -g 1001 nodejs && \
        adduser -S chatbot -u 1001
    USER chatbot
    
  2. Read-Only Filesystem: Most directories read-only

  3. Resource Limits:

    deploy:
      resources:
        limits:
          cpus: '1'
          memory: 512M
    
  4. 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 .env to 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 format
  • 500: 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 format
  • 401: Invalid or expired session
  • 429: Rate limit exceeded
  • 500: 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, not token)
  • Session expired (>24 hours)

Correct Format:

// ✅ CORRECT
body: {
  message: "user message",
  sessionToken: "TOKEN"
}

// ❌ WRONG
headers: {
  'Authorization': 'Bearer TOKEN'
}

Issue: Database Connection Error

Checks:

  1. Verify .env has correct SUPABASE_URL and SUPABASE_SERVICE_KEY
  2. Check Supabase project status (dashboard)
  3. Test connection: node scripts/test-db-connection.js
  4. 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:

  1. Vertical: Upgrade VPS plan ($50-100/month)
  2. Horizontal: Add load balancer + multiple VPS instances
  3. Database: Upgrade Supabase to Pro ($25/month)
  4. Email: Upgrade Resend to paid tier ($20/month for 50k emails)

Appendix

Quick Reference

Production URLs:

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:

  1. Check logs: docker logs catb-api
  2. Review health endpoint: https://askmu.live/api/health
  3. Check database connection: node scripts/test-db-connection.js
  4. Verify environment variables in /root/catb/.env

Document Version: 1.0 Last Updated: October 1, 2025 System Status: ✅ Fully Operational

Last modified October 5, 2025: add projectile (1827c6a)