Next.js 16 Starter Kit with WhatsApp AI Agent
- Inbound webhook at
/api/v1/webhooks/ycloud— receives WhatsApp messages from customers via YCloud - yCloud signature verification — HMAC-SHA256 with
YCloud-Signature: t=...,s=...header - Mark as read + typing indicator — calls yCloud API to show double-check and typing status
- Message debounce — 10s window collects burst messages via Redis queue, only processes the last one
- AI agent with
gpt-5-nanovia Vercel AI Gateway — responds to customer inquiries - Multimodal: audio transcription (Whisper via OpenAI), image analysis with compression, PDF text extraction
- Protected chat API at
/api/v1/chat— same agent logic, requiresx-api-keyheader - Agent tools: products, product detail, pages, blog, company info, long-term memory, derivation to human
- Phone anonymization — HMAC-SHA256 with
WS_ENCRYPTION_KEY, no raw numbers in Redis - Session history — 7 days in Redis, persists across serverless instances
- Long-term memory — 1 year in Redis, agent persists customer preferences/data
- Memory management —
deleteMemorytool for GDPR compliance (deletes all customer keys) - Web UI integration — button clicks from the store (e.g. "Pedir por WhatsApp") are recorded as
systemmessages in session history so the agent has context - Phone cookie — after first WhatsApp send, the phone is saved in a cookie (15 min TTL). Subsequent clicks send directly without showing the dialog, with auto-reload of the phone number when the cookie expires
- Redis-based (cross-instance): 1 msg per 30s per IP, 1 msg per 30s per recipient
- In-memory fallback: 2/min per IP, 10/hr per IP, 10/hr per recipient, 50/hr global
- Next.js 16 with App Router and Turbopack
- shadcn/ui components for modern UI
- REST API with 12 public GET endpoints + chat + webhook
- Scalar API Documentation at
/api - WhatsApp API (YCloud) — send messages via YCloud SDK with country code dialog, with feature flag toggle (wa.me fallback)
- Product Gallery with carousel and fallback images
- Reviews system with star rating
- Inventory tracking by location
- Geolocation service with Vercel headers
- Bilingual EN/ES with
[lang]routes,getConfig(),LangProvider, automatic locale detection - Cookie Consent banner with optional webhook notification
- Notifications with Sonner toasts via
notificationService - Dialog service centralized with
DialogProvider+useDialog()hook - User dropdown menu with language switcher + theme (system/light/dark) selector
- Product filtering — search + category filter
Públicas (cualquier usuario) — 11 tools:
| Tool | Params | Description |
|---|---|---|
getProducts |
query?, category? |
Product catalog from hardcoded data |
getProductDetail |
slug |
Full product info with variants and prices |
getPages |
query?, category? |
Institutional pages (terms, privacy, etc.) |
getPageDetail |
slug |
Full page content |
getBlog |
query?, category? |
Blog posts |
getPostDetail |
slug |
Full article content |
getCompanyInfo |
— | Contact/social data from brand config |
saveLongMemory |
key, value, override? |
1-year persistent memory in Redis |
searchMyHistory |
limit? |
Recent conversation history from Redis session |
deleteMemory |
confirm="BORRAR" |
Deletes ALL customer data (GDPR). Two-step confirmation. |
deriveToHuman |
reason |
Admin-only (requiere WS_ADMIN en env):
| Tool | Params | Description |
|---|---|---|
getDerivedConversations |
— | Lists all chats pending human attention (SCAN wa:derived:*) |
resolveDerivation |
phone |
Frees a derived chat so the agent resumes responding |
findChatByPhone |
phone |
Looks up session history for a given phone number |
- Páginas module — legal & policy pages, config-driven
- Blog module — list + detail with reading time, author
- OG/Twitter images per page and post
- JSON-LD structured data
- Weekly calendar with responsive grid (frontend only)
- Time slots config-driven per day
- Booking requires human intervention via agent derivation
- Dynamic OG/Twitter images (Satori) per locale
- JSON-LD: BreadcrumbList, Product, BlogPosting, WebPage, Event, Organization, WebSite — all with
inLanguagedynamic per locale - hreflang alternates in all pages for bilingual SEO
- Bilingual sitemap with per-locale entries
- Dynamic brand colors via
NEXT_PUBLIC_BRAND_COLOR(32 Radix UI palettes) - PWA manifest, sitemap.xml, robots.txt, llms.txt
- Dark mode toggle
- CORS proxy via middleware
- Upstash Redis for production — sessions, long-term memory, rate limiting, dedup, message queue, derivation flags
- In-memory Map fallback when Redis is not configured — same APIs, no persistence across restarts (derivation always returns false)
- Phone anonymization via HMAC-SHA256 with
WS_ENCRYPTION_KEY— no raw phone numbers in Redis keys
Redis solo almacena datos de infraestructura del agente. No hay datos de tienda (productos, pedidos, reseñas, citas, stock) en Redis.
| Key Pattern | TTL | Purpose |
|---|---|---|
wa:session:{hash} |
7d | Chat history + session state |
wa:longmemory:{hash} |
365d | Agent-persisted customer data |
wa:queue:{hash} |
— | Message debounce (cleared after drain) |
wa:dedup:{wamid} |
1h | Webhook deduplication |
wa:ratelimit:ip:{ip} |
30s | Per-IP rate limit |
wa:ratelimit:to:{to} |
30s | Per-recipient rate limit |
wa:derived:{hash} |
24h | Derivation flag — blocks agent, requires admin |
El agente es solo informativo: responde con datos de las tools de consulta. Si el usuario necesita una acción (comprar, agendar, cancelar, etc.) y no existe tool, el agente llama deriveToHuman y un administrador retoma el chat.
- 305 tests (Vitest) — 8 test files
- Organized by domain: webhook, session-store, tools, API endpoints, config coverage, console guards
- Framework: Next.js 16 + Turbopack
- Language: TypeScript 5.7+
- UI: shadcn/ui + Tailwind CSS
- API: Next.js Route Handlers (REST)
- AI: Vercel AI SDK + OpenAI (gpt-5-nano via Gateway, Whisper for audio)
- WhatsApp: YCloud API (outbound SDK + inbound webhooks)
- Redis: Upstash (sessions, memory, rate limiting, dedup)
- Testing: Vitest (305 tests)
- CI/CD: GitHub Actions + Semantic Release
- Hosting: Vercel
| Variable | Description | Default |
|---|---|---|
NEXT_PUBLIC_BRAND_COLOR |
Radix UI palette name | nla |
NEXT_PUBLIC_BASE_URL |
Base URL | http://localhost:3000 |
NEXT_PUBLIC_WHATSAPP_NUMBER |
Business WhatsApp number | 1234567890 |
NEXT_PUBLIC_INDEXING |
Enable indexing | false |
NEXT_PUBLIC_WEEK_MAX |
Max agenda weeks | 4 |
| Variable | Description |
|---|---|
WS_ADMIN |
Admin WhatsApp number (private, for admin tools detection), fallback NEXT_PUBLIC_WHATSAPP_NUMBER |
API_KEY |
API key for protected endpoints |
YCLOUD_ENABLED |
Enable YCloud integration (default: false, uses wa.me link) |
YCLOUD_API_KEY |
YCloud API key (send + media download) |
YCLOUD_WEBHOOK_SECRET |
Webhook secret for HMAC verification |
WS_ENCRYPTION_KEY |
HMAC key for phone anonymization in Redis |
COOKIE_WEBHOOK_URL |
Cookie consent webhook URL (optional) |
AI_GATEWAY_API_KEY |
Vercel AI Gateway key (chat + images via gateway) |
AI_GATEWAY_ZDR |
Zero Data Retention (default: false, requires Vercel Pro/Enterprise) |
OPENAI_API_KEY |
OpenAI API key (audio transcription via Whisper) |
AI_PROVIDER |
Provider mode: mixed (gateway+openai) or openai (all via openai) |
KV_REST_API_URL |
Upstash Redis REST URL |
KV_REST_API_TOKEN |
Upstash Redis REST token |
| Route | Methods | Data source | Auth |
|---|---|---|---|---|
| /api/v1/products | GET | Seed (hardcoded TS) | Público |
| /api/v1/products/[slug] | GET | Seed (hardcoded TS) | Público |
| /api/v1/categories | GET | Seed (hardcoded TS) | Público |
| /api/v1/paginas | GET | Seed (hardcoded TS) | Público |
| /api/v1/paginas/[slug] | GET | Seed (hardcoded TS) | Público |
| /api/v1/blog | GET | Seed (hardcoded TS) | Público |
| /api/v1/blog/[slug] | GET | Seed (hardcoded TS) | Público |
| /api/v1/resenas/[productSlug] | GET | Seed (hardcoded TS) | Público |
| /api/v1/agenda | GET | Seed (hardcoded TS) | Público |
| /api/v1/chat | POST | Redis (sessions) + AI | Requiere key |
| /api/v1/webhooks/ycloud | GET, POST | YCloud WhatsApp | Público (HMAC) |
| /api/v1/whatsapp/send | POST | YCloud SDK | Público (rate-limited) |
The /api/v1/chat endpoint requires header: x-api-key: your_api_key.
When Upstash Redis is not configured (KV_REST_API_URL empty), all storage functions fall back to local Map objects:
- Sessions & long memory —
Map<string, string>in process memory - Message queue —
Map<string, string[]>for debounce - Dedup —
Set<string>withsetTimeoutcleanup (1h TTL) - Rate limit —
Map<string, number>with expiration timestamps
This allows full local development without Redis. Restarting the server clears all in-memory data.
| Key Pattern | Type | TTL | Purpose |
|---|---|---|---|
wa:session:{hash} |
STRING | 7 days | Conversation history (user/assistant/system) |
wa:longmemory:{hash} |
STRING | 1 year | Agent-persisted customer data |
wa:queue:{hash} |
LIST | — | Message debounce (cleared after processing) |
wa:dedup:{wamid} |
STRING | 1 hour | yCloud retry dedup |
wa:ratelimit:ip:{ip} |
STRING | 30s | Per-IP rate limit |
wa:ratelimit:to:{number} |
STRING | 30s | Per-recipient rate limit |
wa:derived:{hash} |
STRING | 24h | Derivation flag — blocks agent, flags chat for admin |
src/
├── app/api/v1/
│ ├── chat/ → chat with AI agent (protected)
│ ├── webhooks/ycloud/ → inbound WhatsApp webhook
│ ├── whatsapp/send/ → outbound WhatsApp sender
│ └── [products|blog|...] → CRUD endpoints
├── components/
│ ├── ui/
│ │ ├── whatsapp-dialog.tsx → phone input + send dialog
│ │ ├── dialog-provider.tsx → centralized dialog renderer
│ │ └── ...
│ ├── whatsapp-provider.tsx → useWhatsApp() hook
│ └── layout/
│ ├── navbar.tsx → dynamic nav with dropdowns
│ ├── lang-switcher.tsx → language dropdown (ES/EN)
│ ├── theme-toggle.tsx → theme dropdown (system/light/dark)
│ └── user-menu.tsx → unified settings menu
├── lib/
│ ├── locale/
│ │ ├── config.ts → getConfig(), getLocaleFromLang()
│ │ ├── context.tsx → LangProvider, useLang()
│ │ ├── seo.ts → getAlternateLanguages()
│ │ └── slug-resolver.ts → cross-locale slug redirect
│ ├── internal/dialog/ → DialogService, useDialog() hook
│ ├── external/
│ │ ├── ai/ → Vercel AI SDK (gateway, openai, transcription, image/pdf analysis)
│ │ ├── whatsapp/ → YCloud send service
│ │ └── upstash/ → Redis client
│ └── modules/agents/ → Agent service, session-store, tools, schemas
│ ├── config/data/ → Seed data (products, paginas, blog, agenda...)
│ └── test/ → 282 tests organized by domain
└── ...
src/lib/test/
├── agents/
│ ├── webhook.test.ts → signature, payload, URL
│ ├── session-store.test.ts → anonymize, session, dedup, multimodal
│ └── tools.test.ts → products, reviews, pages, blog, agenda, form
├── api.test.ts → REST endpoint auth + CRUD
├── config-keys.test.ts → UI config coverage
├── console-isdev.test.ts → console.* guards
└── no-hardcoded-locale.test.ts → i18n anti-regression
| Script | Description |
|---|---|
pnpm dev |
Start dev server |
pnpm build |
Build for production |
pnpm start |
Start production server |
pnpm lint |
ESLint |
pnpm test |
Vitest (305 tests) |
pnpm format |
Prettier |
MIT License — Copyright (c) 2026 NATULEADAN SAS BIC
Developed by Leonardo Jara.