A production-ready, multi-channel AI-powered product recommendation system. Guides users through a structured questionnaire and delivers personalised 3-tier product recommendations via a Next.js web chat UI, WhatsApp (Twilio), and Instagram (Meta Messenger) .
- Overview
- Features
- Architecture
- Project Structure
- Quick Start
- Environment Variables
- Authentication
- API Reference
- Questionnaire Flow
- Recommendation Engine
- Channel Integration
- Database
- Testing
- Deployment
- Tech Stack
This system is not a chatbot β it is a system-driven guided questionnaire. The system asks every question; the user only provides answers. No free-form intent recognition or topic switching is involved.
User logs in (email/password or Google OAuth)
β
βΌ
System asks Q1 β User answers β System asks Q2 β β¦ β System asks Q6
β
βΌ
Scoring engine matches answers against product catalog
β
βΌ
3-tier recommendations returned (Basic / Intermediate / Premium)
- JWT authentication β email/password with OTP email verification + Google OAuth 2.0
- Protected routes β all pages require authentication; unauthenticated users are redirected to
/login - No auth flash β pages render
nulluntil token verification completes, eliminating the 1-second flash - Guided questionnaire β 6 questions with conditional branching (Q4b appears only for $200+ budgets)
- 3-tier recommendations β Basic (top 2 affordable), Intermediate (top 4 with reasoning), Premium (best match + bundle)
- Multi-channel β Web chat (Next.js), WhatsApp via Twilio, Instagram via Meta Messenger
- Session persistence β SQLite (dev) or PostgreSQL (prod), sessions survive server restarts
- Dark-theme Next.js UI β Glassmorphism, gradient accents, profile dropdown, settings page
- Language toggle β EN / PT language support across all pages and recommendations
- Input validation β Joi schemas on all endpoints, re-asks on invalid answers (max 3 attempts)
- Rate limiting β Per-IP limits on all API routes
- Security β Twilio signature verification, Meta HMAC-SHA256 webhook validation
- Session cleanup β Cron job removes expired sessions every 10 minutes
- Graceful shutdown β SIGTERM/SIGINT handling
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Clients β
β Next.js (port 3001) WhatsApp (Twilio) Instagram β
ββββββββββ¬βββββββββββββββββββββββ¬βββββββββββββββββββββ¬ββββββββββ
β /api/* proxy β β
βΌ βΌ βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Express App (src/app.js, port 3000) β
β Middleware: Helmet Β· CORS Β· Rate Limit Β· Validation β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β AuthController (JWT + Passport Google OAuth) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β WebhookController (unified handler) β
ββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββ€
β Questionnaire β Recommendation β Offer β
β Service β Service β Service β
β (flow/parse) β (tag scoring) β (3-tier build) β
ββββββββββββββββββββ΄ββββββββββββββββββββ΄ββββββββββββββββββββββββ€
β Models: SessionModel Β· ProductModel β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Database: SQLite (dev) β PostgreSQL (prod) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
POST /api/webhook/web (requires Bearer JWT)
β requireAuth middleware (verifies JWT)
β validate(schemas.webMessage)
β WebhookController.handleWeb
β SessionModel.findActiveByUserChannel (find or create session)
β QuestionnaireService.processAnswer (validate + advance question)
β OfferService.generate (when last question answered)
β RecommendationService.score (tag + budget + keyword scoring)
β JSON response with { sessionId, response, question, offers }
project/
βββ src/
β βββ app.js Express server, routes, startup
β βββ config/
β β βββ database.js SQLite / PostgreSQL init + table creation
β β βββ passport.js Passport Google OAuth 2.0 strategy
β β βββ questionnaire.js Question definitions + branching logic
β βββ controllers/
β β βββ auth.controller.js JWT login, OTP signup, Google OAuth, logout
β β βββ session.controller.js GET /session/:id, POST /session/reset/:id
β β βββ webhook.controller.js Unified handler for web / WhatsApp / Instagram
β βββ middleware/
β β βββ auth.js requireAuth (JWT), API key, Twilio, Meta HMAC
β β βββ rateLimiter.js express-rate-limit instances
β β βββ validation.js Joi schemas for all endpoints
β βββ models/
β β βββ product.model.js Products CRUD (SQLite + PostgreSQL)
β β βββ session.model.js Sessions CRUD, expiry, cleanup
β β βββ user.model.js Users + temp_users (OTP pending) tables
β βββ services/
β β βββ email.service.js Nodemailer OTP email sender
β β βββ offer.service.js 3-tier offer builder with bundle discount
β β βββ questionnaire.service.js Flow control, answer parsing, branching
β β βββ recommendation.service.js Tag/budget/keyword scoring engine
β βββ utils/
β βββ logger.js Winston logger (file + console)
β βββ migrate.js Run DB migrations (idempotent)
β βββ seed.js Seed sample products
β βββ whatsappFormatter.js Format messages for WhatsApp/Instagram
βββ frontend/ Next.js 14 App Router frontend
β βββ app/
β β βββ page.tsx Main chat UI (protected β requires auth)
β β βββ login/page.tsx Sign-in page (email + Google)
β β βββ signup/page.tsx Registration page (email + Google)
β β βββ verify-email/page.tsx OTP verification step
β β βββ forgot-password/page.tsx Password reset request + OTP confirm
β β βββ profile/page.tsx User profile page (protected)
β β βββ settings/page.tsx Account settings (protected)
β β βββ auth/callback/page.tsx Google OAuth redirect handler
β βββ components/
β β βββ Header.tsx Sticky header with profile dropdown
β β βββ LeftPanel.tsx Auth pages marketing panel
β βββ lib/
β β βββ api.ts Typed API client (all backend calls)
β βββ next.config.js Next.js config + /api/* + /auth/* rewrites
β βββ package.json
βββ tests/
β βββ unit/
β β βββ questionnaire.test.js
β β βββ recommendation.test.js
β β βββ offer.test.js
β βββ integration/
β β βββ api.test.js
β βββ e2e/
β βββ conversation.spec.js
βββ data/
β βββ products.json
β βββ questionnaire.json
β βββ database.db SQLite database (auto-created)
βββ logs/
β βββ app.log
β βββ error.log
βββ .env.example
βββ Dockerfile
βββ docker-compose.yml
βββ ecosystem.config.js
βββ package.json
- Node.js >= 18
- npm >= 9
# Backend
cd project
npm install
# Frontend
cd frontend
npm installcp .env.example .env
# Edit .env β see Environment Variables section belownpm run migrate # creates tables (idempotent β safe to re-run)
npm run seed # inserts sample products# Run backend + frontend together
npm run dev:all
# Or separately:
npm run dev # backend on http://localhost:3000
npm run dev:frontend # frontend on http://localhost:3001Open http://localhost:3001 β you will be redirected to /login.
curl http://localhost:3000/api/health
# {"status":"ok","timestamp":"...","uptime":12,"environment":"development"}| Variable | Required | Default | Description |
|---|---|---|---|
PORT |
No | 3000 |
Backend HTTP port |
NODE_ENV |
No | development |
development / production / test |
FRONTEND_URL |
No | http://localhost:3001 |
Used for Google OAuth redirect after login |
JWT_SECRET |
Yes | β | Secret for signing JWTs (min 32 chars) |
JWT_EXPIRES_IN |
No | 7d |
JWT expiry (e.g. 7d, 24h) |
GOOGLE_CLIENT_ID |
Google OAuth | β | From Google Cloud Console |
GOOGLE_CLIENT_SECRET |
Google OAuth | β | From Google Cloud Console |
EMAIL_USER |
OTP email | β | Gmail address used to send OTP codes |
EMAIL_PASS |
OTP email | β | Gmail app password (not your account password) |
EMAIL_FROM |
No | EMAIL_USER |
Display name + address for outgoing emails |
DB_TYPE |
No | sqlite |
sqlite or postgres |
DB_PATH |
No | ./data/database.db |
SQLite file path |
DATABASE_URL |
Postgres only | β | Full PostgreSQL connection string |
DB_SSL |
No | false |
Set true to enable SSL for Postgres |
API_KEY |
Yes | β | Shared secret for admin endpoints (x-api-key header) |
APP_URL |
Prod/WhatsApp | β | Full public URL e.g. https://your-app.railway.app |
SESSION_TIMEOUT_MINUTES |
No | 30 |
Session inactivity timeout |
SESSION_CLEANUP_INTERVAL_MINUTES |
No | 10 |
How often expired sessions are purged |
TWILIO_ACCOUNT_SID |
β | From Twilio console | |
TWILIO_AUTH_TOKEN |
β | From Twilio console | |
TWILIO_WHATSAPP_FROM |
β | e.g. whatsapp:+14155238886 |
|
META_APP_SECRET |
β | From Meta developer dashboard | |
META_PAGE_ACCESS_TOKEN |
β | Long-lived page token | |
META_VERIFY_TOKEN |
β | Any secret string you choose | |
ALLOWED_ORIGINS |
No | http://localhost:3000,http://localhost:3001 |
CORS comma-separated allowed origins |
RATE_LIMIT_MAX_REQUESTS |
No | 100 |
Requests per minute per IP |
LOG_LEVEL |
No | info |
Winston log level |
BACKEND_URL=http://localhost:3000 # backend origin for Next.js rewritesOTP in dev mode: If
EMAIL_USER/EMAIL_PASSare not set, OTP codes are printed to the backend console instead of emailed.
The system uses custom JWT authentication with no external auth SDK dependency.
POST /api/auth/send-otp β sends 6-digit code to email, returns tempUserId
POST /api/auth/verify-otp β verifies code, creates user, returns { token, user }
POST /api/auth/resend-otp β resends code to same email
POST /api/auth/login β email + password login, returns { token, user }
POST /api/auth/forgot-password β sends reset OTP
POST /api/auth/reset-password β verifies OTP + sets new password
POST /api/auth/logout β invalidates server-side session
GET /api/auth/me β returns current user (requires Bearer token)
GET /auth/google β redirects to Google consent screen
GET /auth/google/callback β Passport callback β redirects to:
{FRONTEND_URL}/auth/callback?token=<jwt>
The frontend /auth/callback page extracts the token from the URL, stores it in localStorage, calls /api/auth/me, then redirects to /.
The JWT is stored in localStorage as smart_token. All authenticated API calls include it as:
Authorization: Bearer <token>
Every protected page uses the same guard pattern:
const [authChecked, setAuthChecked] = useState(false);
useEffect(() => {
const token = localStorage.getItem('smart_token');
if (!token) { router.replace('/login'); return; }
api.getMe()
.then((me) => { setUser(me); setAuthChecked(true); })
.catch(() => { router.replace('/login'); });
}, []);
if (!authChecked || !user) return null; // prevents flash of protected contentGET /api/health
Response:
{
"status": "ok",
"timestamp": "2026-04-10T10:00:00.000Z",
"uptime": 3600,
"environment": "development"
}POST /api/auth/send-otp
POST /api/auth/verify-otp
POST /api/auth/resend-otp
POST /api/auth/login
POST /api/auth/logout
GET /api/auth/me (requires Authorization: Bearer <token>)
POST /api/auth/forgot-password
POST /api/auth/reset-password
GET /auth/google
GET /auth/google/callback
POST /api/webhook/web
Authorization: Bearer <token>
Content-Type: application/json
Request body:
{
"userId": "user_abc123",
"message": "1",
"language": "en",
"sessionId": "uuid-of-existing-session"
}Response:
{
"sessionId": "550e8400-...",
"response": "π *Question 2/6*\n\nWho is this for?...",
"isComplete": false,
"question": { "id": "q2", "type": "multiple_choice", "options": { "en": [...], "pt": [...] } },
"questionNumber": 2,
"totalQuestions": 6,
"offers": null
}When all questions are answered, isComplete: true and offers is populated:
{
"isComplete": true,
"offers": {
"basic": [ { "id": 1, "name": "...", "price": 49, "score": 65 } ],
"intermediate": [ { "id": 3, "name": "...", "price": 149, "score": 85, "reason": "..." } ],
"premium": {
"product": { "id": 4, "name": "...", "price": 199, "score": 95 },
"bundle": { "items": ["Premium carry case", "3-year warranty"], "totalPrice": 228.69, "savings": 25.41 }
}
}
}POST /api/webhook/whatsapp
Twilio sends URL-encoded form data. The system responds <Response/> (TwiML) and proactively sends the reply via Twilio API.
GET /api/webhook/instagram # Hub verification challenge
POST /api/webhook/instagram # Incoming message events
GET /api/session/:id # Get full session state
POST /api/session/reset/:id # Delete session (user starts over)
GET /api/products?category=audio
Q1: What problem are you trying to solve? (multiple choice)
Q2: Who is this product for? (multiple choice)
Q3: What is most important to you? (multiple choice)
Q4: What is your budget range? (multiple choice)
ββ IF answer = "$200+" β Q4b: Bundle interest? (conditional branch)
Q5: How urgently do you need this? (multiple choice)
Q6: Any specific requirements? (text, optional)
- Invalid answer: system re-asks the same question (max 3 attempts, then skips optional questions)
- Reset: send
resetorrestartat any time (or click βΊ Reset in the UI) - Skip optional: send
skipon text questions - Language: answers accepted in EN or PT based on the session language setting
Scoring is tag-based (0β100 points total):
| Signal | Points | Logic |
|---|---|---|
| Budget compatibility | 25 | Full if price within range; partial credit within 20% overflow |
| Tag overlap | 20 | Proportional to % of user tags matched on product |
| Feature keyword match | 25 | Free-text Q6 keywords matched against product features + description |
| Category heuristic | 30 | Awarded based on tag alignment score |
Products are sorted by score descending. The 3 tiers are built from this sorted list:
| Tier | Products | Minimum Score |
|---|---|---|
| Basic | Top 2, cheapest first | 50 |
| Intermediate | Top 4, by score | 50 |
| Premium | #1 product + optional bundle | Any (flags isHighConfidence if β₯ 85) |
Bundle is added to Premium when the user selected $200+ budget and chose "Yes, bundle sounds great!" β or when the top product scores β₯ 85%.
- Create a Twilio account and activate a WhatsApp Sandbox or approved number
- Set webhook URL to
https://your-domain.com/api/webhook/whatsapp - Set env vars:
TWILIO_ACCOUNT_SID,TWILIO_AUTH_TOKEN,TWILIO_WHATSAPP_FROM - Twilio signature verification is enforced in production automatically
- Create a Meta developer app and configure a Messenger webhook
- Set webhook URL to
https://your-domain.com/api/webhook/instagram - Use
META_VERIFY_TOKENfor the hub verification step - Set env vars:
META_APP_SECRET,META_PAGE_ACCESS_TOKEN,META_VERIFY_TOKEN
- Go to Google Cloud Console β APIs & Services β Credentials
- Create an OAuth 2.0 Client ID (Web application)
- Add
http://localhost:3000/auth/google/callbackto Authorised redirect URIs (dev) - Set env vars:
GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET
The system auto-creates all tables on startup.
| Column | Type | Description |
|---|---|---|
| id | UUID | Primary key |
| name | VARCHAR | Display name |
| VARCHAR | Unique email address | |
| password_hash | VARCHAR | bcrypt hash (null for Google-only accounts) |
| google_id | VARCHAR | Google OAuth subject ID |
| created_at | TIMESTAMP | Registration date |
| last_seen | TIMESTAMP | Last /api/auth/me call |
Holds OTP-pending registrations. Automatically cleaned up after verification or expiry.
| Column | Type | Description |
|---|---|---|
| id | UUID | Primary key |
| channel | VARCHAR | web, whatsapp, instagram |
| user_id | VARCHAR | Platform user ID |
| current_question_index | INTEGER | 0-based index into question flow |
| responses | JSON | Array of { questionId, answerIndex, answerText, skipped } |
| completed | BOOLEAN | True when all questions answered |
| recommendations | JSON | Stored offer result after completion |
| metadata | JSON | Language, invalid attempt counters, etc. |
| expires_at | TIMESTAMP | Auto-extended on each interaction |
| Column | Type | Description |
|---|---|---|
| id | SERIAL | Primary key |
| name | VARCHAR | Product name |
| description | TEXT | Product description |
| price | DECIMAL | Price in USD |
| category | VARCHAR | e.g. audio |
| tags | JSON | Array of string tags used for scoring |
| features | JSON | Array of feature strings |
| stock | INTEGER | Inventory count |
| active | BOOLEAN | Only active products are recommended |
npm test # all tests (unit + integration)
npm run test:unit # unit tests only
npm run test:integration # integration tests only
npm run test:coverage # coverage reportTests use an in-memory SQLite database β no external services needed.
docker build -t smart-ai-recommendation .
docker run -d \
--name smart-ai \
-p 3000:3000 \
-e NODE_ENV=production \
-e JWT_SECRET=your-secret-min-32-chars \
-e API_KEY=your-admin-key \
-e FRONTEND_URL=https://your-frontend.com \
-v smart-ai-data:/app/data \
smart-ai-recommendationcp .env.example .env
# Set DB_TYPE=postgres, DATABASE_URL, JWT_SECRET, API_KEY, etc.
docker compose up -dThe app is 12-factor compatible. Set all required env vars in the dashboard and deploy from the repo root. Run npm run migrate as a one-off command after first deploy.
npm install -g pm2
pm2 start ecosystem.config.js
pm2 save
pm2 startupThe Next.js frontend can be deployed independently to Vercel, Netlify, or any Node.js host:
cd frontend
npm run build
npm startSet BACKEND_URL in the host's environment variables to point at your deployed backend.
| Layer | Technology |
|---|---|
| Runtime | Node.js 18+ |
| Backend framework | Express 4 |
| Frontend framework | Next.js 14 (App Router) |
| Language | TypeScript (frontend) Β· JavaScript (backend) |
| Authentication | JWT Β· Passport.js Google OAuth 2.0 Β· bcrypt |
| Nodemailer (Gmail SMTP) | |
| Database (dev) | SQLite via better-sqlite3 |
| Database (prod) | PostgreSQL via pg |
| Validation | Joi |
| Rate limiting | express-rate-limit |
| Logging | Winston |
| Security headers | Helmet |
| Compression | compression |
| Scheduled jobs | node-cron |
| Twilio | |
| Meta Messenger API | |
| Testing | Jest + Supertest + Playwright |
| Process manager | PM2 |
| Containerisation | Docker + Docker Compose |
| Dev server | Nodemon + Next.js dev server |
MIT