A production-quality AI customer support chat application built for the Spur founding engineer take-home. Features a real-time chat interface with an AI agent (Luna) that answers questions about a fictional artisan home goods store (Lunara).
- π¬ Real-time chat UI β Polished dark/light theme with message bubbles, typing indicator, auto-scroll
- π€ AI-powered responses β Groq Llama 3.3 70B with contextual conversation history
- π FAQ knowledge base β Seeded domain knowledge (shipping, returns, payments, support hours)
- πΎ Persistent conversations β SQLite-backed, survives page reloads via session ID in localStorage
- π‘οΈ Robust error handling β Graceful LLM failures, input validation, rate limit handling
- π¨ Beautiful design β Glass-morphism effects, gradient accents, smooth animations, responsive layout
- β¨οΈ Great UX β Enter to send, Shift+Enter for newline, typing indicator, suggested questions, character limit
- Node.js 18+
- A Groq API key (get one here)
git clone <your-repo-url>
cd splur
# Create environment file
cp .env.example backend/.envEdit backend/.env and add your Groq API key:
GROQ_API_KEY=your-actual-key-here
cd backend
npm install
npm run devThe backend starts on http://localhost:3001. On first run, it automatically:
- Creates the SQLite database (
./data/chat.db) - Runs schema migrations
- Seeds FAQ data for the Lunara store
cd frontend
npm install
npm run devThe frontend starts on http://localhost:5173. Open it in your browser and start chatting!
No manual setup required. The database is automatically created and migrated on first backend start. FAQ data is auto-seeded when the faq_entries table is empty.
To manually re-seed FAQ data:
cd backend
npm run db:seed| Variable | Required | Default | Description |
|---|---|---|---|
GROQ_API_KEY |
β | β | Your Groq API key |
PORT |
3001 |
Backend server port | |
DATABASE_PATH |
./data/chat.db |
SQLite database file path | |
MAX_TOKENS |
500 |
Max tokens per LLM response | |
MAX_MESSAGE_LENGTH |
2000 |
Max characters per user message | |
MAX_HISTORY_MESSAGES |
20 |
Max messages sent as LLM context | |
FRONTEND_URL |
http://localhost:5173 |
CORS allowed origin | |
NODE_ENV |
development |
Environment mode | |
VITE_API_URL |
http://localhost:3001 |
Backend URL (frontend env) |
splur/
βββ backend/ # Express + TypeScript API server
β βββ src/
β βββ index.ts # Entry point β Express app bootstrap
β βββ config.ts # Zod-validated environment config
β βββ types.ts # Shared TypeScript types
β βββ routes/
β β βββ chat.ts # Chat endpoints (POST /message, GET /history)
β βββ services/
β β βββ llm.ts # Groq integration (generateReply)
β βββ models/
β β βββ conversation.ts # Conversation CRUD
β β βββ message.ts # Message CRUD
β βββ middleware/
β β βββ validation.ts # Zod request validation
β β βββ errorHandler.ts # Global error handler
β βββ db/
β βββ index.ts # SQLite connection manager
β βββ schema.ts # Table migrations
β βββ seed.ts # FAQ seed data
β
βββ frontend/ # SvelteKit 5 chat UI
β βββ src/
β βββ routes/
β β βββ +layout.svelte # Root layout (global CSS)
β β βββ +page.svelte # Main page with chat widget
β βββ lib/
β βββ components/
β β βββ ChatWidget.svelte # Main container
β β βββ ChatInput.svelte # Input + send button
β β βββ MessageBubble.svelte # User/AI message bubbles
β β βββ TypingIndicator.svelte # "Luna is typing..."
β βββ stores/
β β βββ chat.ts # Svelte stores for state management
β βββ api.ts # Backend API client
β βββ types.ts # Frontend type definitions
β
βββ .env.example
βββ .gitignore
βββ README.md
- Separation of Concerns β Routes handle HTTP, services handle business logic, models handle data access
- Extensibility β Adding a new channel (WhatsApp, Instagram) means adding a new route that calls the same
generateReply()service - Encapsulated LLM β The entire Groq integration lives in
services/llm.ts. Swapping to another provider requires changing only this file - Schema-first β Clean relational schema with conversations β messages, ready for PostgreSQL migration
conversations messages
ββββββββββββββββββββββββ ββββββββββββββββββββββββββββββ
β id (PK) βββββ β id (PK) β
β created_at β β β conversation_id (FK) βββββββ
β metadata (JSON) β ββββββ sender ('user' | 'ai') β
ββββββββββββββββββββββββ β text β
β created_at β
ββββββββββββββββββββββββββββββ
faq_entries
ββββββββββββββββββββββββββββββ
β id (PK) β
β category β
β question β
β answer β
ββββββββββββββββββββββββββββββ
| Method | Endpoint | Description |
|---|---|---|
POST |
/chat/message |
Send a message, get AI reply |
GET |
/chat/:sessionId/history |
Fetch conversation history |
GET |
/health |
Health check |
POST /chat/message
// Request
{ "message": "What's your return policy?", "sessionId": "optional-uuid" }
// Response
{ "reply": "We offer a 30-day return policy...", "sessionId": "uuid" }Groq Llama-3.3-70B-Versatile β chosen for being blazing fast and completely free (up to generous rate limits). The integration is fully encapsulated in backend/src/services/llm.ts.
- System prompt defines the agent persona (Luna) with clear behavioral guidelines
- FAQ knowledge base is dynamically loaded from the database and injected into the system prompt
- Conversation history (last 20 messages) is included for contextual responses
- Guardrails:
- Agent won't make up policies not in the knowledge base
- Redirects to human support for questions it can't answer
- Stays on-topic (won't discuss unrelated subjects)
- Responses capped at ~150 words for conciseness
- 401 (Invalid Key): "Having trouble connecting to my brain..."
- 429 (Rate Limited): "A bit overwhelmed with requests..."
- 500/503 (Server Error): "Experiencing technical difficulties..."
- Timeout (30s): "Taking too long to think..."
- All errors surface as friendly messages in the chat UI
MAX_TOKENS=500caps each responseMAX_HISTORY_MESSAGES=20limits context windowMAX_MESSAGE_LENGTH=2000prevents oversized inputs- Request body limited to 10KB at Express level
- Streaming responses β Use SSE/WebSocket to stream LLM tokens for a more responsive feel
- Authentication β Add session-based auth so conversations are tied to users
- Rate limiting β Add per-IP rate limiting with Redis to prevent abuse
- Redis caching β Cache FAQ data and recent conversations for faster responses
- WebSocket β Replace polling with real-time bidirectional communication
- Testing β Add comprehensive unit tests (Vitest) and E2E tests (Playwright)
- RAG β Use vector embeddings + semantic search instead of dumping all FAQs into the prompt
- Multi-channel β Add WhatsApp/Instagram webhook handlers that call the same
generateReply()service - Admin dashboard β View all conversations, analytics, agent performance metrics
- Markdown rendering β Parse AI responses for bold, lists, links
- Message reactions β Thumbs up/down feedback on AI responses for quality tracking
- Docker Compose β One-command setup for the full stack
- CI/CD β GitHub Actions for lint, test, and deploy on push
- SQLite over PostgreSQL: Simpler for the exercise, but the schema is designed to port to Postgres with zero changes to the SQL
- All FAQs in system prompt: Works fine for a small knowledge base (~12 entries). For a larger KB, I'd use RAG with vector search
- No auth: Keeps the scope focused. Sessions are identified by UUID stored in localStorage
- Synchronous DB (better-sqlite3): Chosen for simplicity and speed. For production, I'd use an async driver like
drizzle-orm+libsql