AI-powered feedback intelligence for product teams.
A Discord bot + HTTP API that ingests scattered feedback (support tickets, Discord, GitHub, etc.), normalizes it using Workers AI, and provides semantic search and daily digests via AI Search RAG. Built entirely on the Cloudflare Developer Platform.
- Scattered data: Feedback comes from Discord, GitHub, support, Twitter, emailβall in different formats
- Theme extraction: Hard to spot patterns and urgency across 100+ daily feedback items
- Context retrieval: "What have customers said about D1 performance?" requires manual search
- PM insights: Daily digests with sentiment breakdowns, urgency samples, and AI-generated themes
Deployed Worker:
π https://feedback-analyzer.juelzlax.workers.dev
Discord Bot Intall Link:
π https://discord.com/oauth2/authorize?client_id=1459585837292458024
GitHub Repository:
π https://github.com/julez14/feedback-analyzer
- Accepts raw feedback from any source (Discord message, GitHub issue, support ticket JSON)
- Workers AI analyzes content and extracts:
- Product area (
workers,d1,r2,auth, etc.) - Sentiment (
positive,neutral,negative) - Urgency (
low,medium,high,p1) - Tags (
bug,feature-request,docs, etc.)
- Product area (
- Stores standardized JSON in R2 (for AI Search indexing)
- Writes canonical row to D1 (for fast stats queries)
- Discord slash command:
/ask query:"What are users saying about D1 performance?" - Queries AI Search to find relevant feedback using vector similarity
- Returns AI-generated summary with citations
- Works via Discord bot or HTTP API
- Discord slash command:
/digest date:2025-01-14(optional; defaults to today) - Combines:
- AI Search: Semantic themes and representative examples
- D1: Deterministic stats (counts by source/sentiment, high-urgency samples)
- Outputs PM-friendly digest with sentiment emoji breakdown and key insights
βββββββββββββοΏ½οΏ½οΏ½βββββ
β Discord Bot β
β Slash Commands β
ββββββββββ¬βββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββ
β Cloudflare Worker (index.ts) β
β Routes: /interactions, /ingest, β
β /ask, /digest β
βββ¬ββββββββββ¬ββββββββββ¬ββββββββββ¬βββββ
β β β β
β β β β
βΌ βΌ βΌ βΌ
ββββββ ββββββ ββββββ ββββββββββ
β AI β β D1 β β R2 β βAI Searchβ
β β β β β β β (RAG) β
ββββββ ββββββ ββββββ ββββββββββ
- Workers β Single Worker hosts Discord interaction handler + HTTP API routes
- Why: Serverless, auto-scaled, zero-config deployment
- Workers AI β Standardization and summarization (Llama 3.1 8B Instruct)
- Why: Extracts product area, sentiment, urgency from unstructured text without external LLM costs
- R2 β Object storage for standardized feedback JSON documents
- Why: Serves as the "corpus" for AI Search; durable, low-cost storage for raw documents
- AI Search (AutoRAG) β Semantic search + retrieval-augmented generation
- Why: Handles vector embeddings, indexing, and RAG automatically; no manual vectorization needed
- D1 β Serverless SQL database for structured feedback rows
- Why: Fast deterministic queries for digest stats (counts by source/sentiment/urgency)
Below are some design decisions made during development:
Decision: Implement /digest as a manual command now, with a note "cron scheduled next."
Why: Scheduling adds complexity and isn't required to demonstrate the core product value in an MVP.
Decision: Use Cloudflare AI Search for the MVP. Why: It reduces "vector plumbing" and makes it easier to ship a reliable RAG demo quickly, while still allowing differentiation via your standardization step and PM-style outputs.
Decision: Noβtreat D1 and AI Search as separate layers and explicitly index documents into AI Search (via its configured data source). Why: AI Search indexes content from connected sources (not "watching" D1 tables by default), so you need an ingestion/indexing path that feeds the RAG corpus.
Decision: Don't skip itβdo standardization (or "standardization-lite") even if R2 is your main store. Why: Without standardization, analytics and even RAG quality become inconsistent because the "true feedback text" lives in different fields across platforms; standardization is also the core product insight of the prompt.
Decision: Use per-item JSON objects with date/source prefixes for MVP. Why: NDJSON "append" patterns introduce concurrency coordination (often requiring a serializer), while per-item objects are simpler and safer under parallel ingestion.
Decision: Use slash commands for the MVP.
Why: It's simpler and more reliable to implement within the timebox; mentions add extra moving parts and risk.
Decision: One Cloudflare Worker hosts the Discord interaction handler and the HTTP API routes, and it talks to bound services (Workers AI + D1 + R2 + AI Search). Why: A single Worker keeps deployment and debugging simple and makes the architecture screenshot/description clearer for the submission.
Decision: Optimize D1 for time-window queries and breakdowns used by digests and PM analytics (by date, source, product area, sentiment, urgency), plus fast "recent items" retrieval. Why: RAG is handled by AI Search, while D1 powers deterministic counts/trends and "what changed" style analysis that makes the tool feel PM-built.
All feedback is normalized into this canonical JSON structure (TypeScript interface):
interface Feedback {
id: string; // UUID
created_at: string; // ISO 8601 timestamp
source: 'discord' | 'github' | 'support' | 'twitter' | 'email';
source_url?: string; // Original URL if available
product_area: 'auth' | 'billing' | 'workers' | 'ai' | 'workers-ai' | 'd1' | 'r2' | 'other';
title?: string; // Short summary headline (max 100 chars)
author?: string; // Username or identifier
thread_id?: string; // Thread/issue ID
body_text: string; // Main feedback text
sentiment: 'positive' | 'neutral' | 'negative';
urgency: 'low' | 'medium' | 'high' | 'p1';
tags: string[]; // e.g., ["bug", "feature-request", "performance"]
confidence?: {
product_area?: number; // 0-1 confidence score
sentiment?: number;
urgency?: number;
};
}Feedback is stored in R2 using a date-partitioned structure for easy indexing:
standardized/dt=2025-01-14/source=discord/<uuid>.json
standardized/dt=2025-01-14/source=github/<uuid>.json
standardized/dt=2025-01-15/source=support/<uuid>.json
Why this structure?
- Date prefixes (
dt=YYYY-MM-DD) enable efficient daily digest queries - Source prefixes help with analytics breakdowns
- Per-item JSON objects avoid concurrency issues with append-style writes
- Node.js: v18 or later
- Cloudflare Account: Sign up for free
- Wrangler CLI: Installed via
npm install(included in package.json)
# Clone the repository
git clone https://github.com/julez14/feedback-analyzer.git
cd feedback-analyzer
# Install dependencies
npm installEdit wrangler.jsonc to set up your bindings:
# Create D1 database
npx wrangler d1 create feedback-data
# Apply migrations (creates `feedback` table)
npx wrangler d1 migrations apply feedback-data
# Create R2 bucket
npx wrangler r2 bucket create feedback
# Create AI Search index
# Note: AI Search indexes are created via Cloudflare Dashboard at:
# https://dash.cloudflare.com/?to=/:account/ai/ai-search
# Name it "feedback-search" and connect it to your R2 bucket (prefix: standardized/)- Go to Discord Developer Portal
- Create a new application
- Copy the Public Key (hex string) and set it as
DISCORD_PUBLIC_KEYinwrangler.jsonc - Go to Bot tab β Reset Token β Copy the bot token (if needed for future features)
- Enable "Message Content Intent" (Bot settings)
- Install bot to your server:
https://discord.com/api/oauth2/authorize?client_id=YOUR-APP-ID&permissions=2147485696&scope=bot%20applications.commands - Register slash commands:
(This script uses Discord API to register
npm run register-commands
/askand/digestcommands)
Set these in wrangler.jsonc under vars:
| Variable | Description | Example |
|---|---|---|
DISCORD_PUBLIC_KEY |
Discord app public key (hex) for request verification | f2d69cf2d521... |
AI_SEARCH_NAME |
Name of your AI Search index | feedback-search |
MODEL_NORMALIZE |
Workers AI model for feedback normalization | @cf/meta/llama-3.1-8b-instruct |
MODEL_SUMMARIZE |
Workers AI model for digest summaries | @cf/meta/llama-3.1-8b-instruct |
Binding names used in code:
AIβ Workers AI bindingDBβ D1 database bindingBUCKETβ R2 bucket binding
# Start local dev server
npx wrangler dev
# Worker will be available at http://localhost:8787Note: Local dev uses remote bindings (AI, D1, R2, AI Search) by default. To test Discord interactions locally, use a tool like ngrok to expose your localhost and update the Discord Interactions URL in the Developer Portal.
# Deploy to production
npx wrangler deploy
# Your worker will be live at:
# https://feedback-analyzer.juelzlax.workers.devAfter deploying, configure Discord to send interactions to your Worker:
- Go to Discord Developer Portal β Your App β General Information
- Set Interactions Endpoint URL:
https://feedback-analyzer.juelzlax.workers.dev/interactions - Discord will send a PING request to verify the endpoint (your Worker handles this automatically)
# Health check
curl https://feedback-analyzer.juelzlax.workers.dev/
# Should return:
# {"status":"ok","service":"feedback-analyzer","endpoints":["/interactions","/ingest","/ask","/digest"]}Ask a question:
Get daily digest:
Ingest raw feedback:
curl -X POST https://feedback-analyzer.juelzlax.workers.dev/ingest \
-H "Content-Type: application/json" \
-d '{
"id": "discord-msg-123456",
"source": "discord",
"author": "alice#1234",
"content": "I love Workers AI but the @cf/meta/llama-3.1-8b-instruct model is timing out on long prompts. Can we get a fix?",
"timestamp": "2025-01-14T10:30:00Z"
}'Response:
{
"success": true,
"id": "discord-msg-123456",
"created_at": "2025-01-14T10:30:00Z",
"r2_key": "standardized/dt=2025-01-14/source=discord/discord-msg-123456.json",
"normalized": {
"source": "discord",
"product_area": "workers-ai",
"sentiment": "neutral",
"urgency": "medium",
"title": "Workers AI model timeout on long prompts",
"tags": ["bug", "workers-ai", "performance"]
}
}Query feedback (RAG):
curl -X POST https://feedback-analyzer.juelzlax.workers.dev/ask \
-H "Content-Type: application/json" \
-d '{"query": "What are the top feature requests for R2?"}'Generate digest:
curl -X POST https://feedback-analyzer.juelzlax.workers.dev/digest \
-H "Content-Type: application/json" \
-d '{"date": "2025-01-14"}'Use the provided script to populate your feedback database with realistic test data:
# Run sample ingestion script
npx tsx scripts/ingest-sample.ts
# This will create ~20-30 sample feedback items across different sources,
# product areas, and sentiment/urgency levels- Manual trigger:
/digestrequires a user to invoke it (no scheduled automation yet) - No deduplication: Same feedback from multiple sources isn't automatically merged
- Basic sentiment analysis: Single-pass LLM classification (no multi-model ensemble)
- No PII detection: User names and content are stored as-is
- Use Cloudflare Workflows or Cron Triggers to auto-generate daily digests
- Post to Discord channel every morning (e.g., 9 AM UTC)
- Why: PMs shouldn't have to remember to run
/digestmanually
- Use Vectorize to detect near-duplicate feedback across sources
- Cluster similar feedback into "topics" (e.g., "D1 Performance Issues")
- Why: 10 people saying "D1 is slow" should be treated as one theme, not 10 separate items
- Workers AI + pattern matching to detect and redact emails, API keys, etc.
- Role-based access control for
/digest(e.g., only PMs can run it) - Why: Protect customer privacy and prevent accidental leaks
- Add "Was this helpful? π π" reactions to
/askresponses - Store evaluation data in D1 and use it to tune RAG prompts
- Why: Continuously improve answer quality based on PM feedback
- Workers + Pages frontend showing trends over time
- Visualizations: sentiment over time, product area heatmap, urgency distribution
- Why: Some insights are easier to spot visually than in text digests
- Direct ingest from Discord (message listener), GitHub webhooks, Zendesk API
- Why: Currently requires manual
/ingestcalls; real integrations eliminate this step
- Cloudflare Workers Docs
- Workers AI
- AI Search (AutoRAG)
- D1 Database
- R2 Object Storage
- Design Decisions
Built for the Cloudflare PM Intern Assignment | January 2026 | Submission Document




{ "name": "feedback-analyzer", "main": "src/index.ts", "compatibility_date": "2025-09-27", // Workers AI binding "ai": { "binding": "AI" }, // D1 database "d1_databases": [ { "binding": "DB", "database_name": "feedback-data", "database_id": "YOUR-D1-DATABASE-ID", "migrations_dir": "migrations" } ], // R2 bucket "r2_buckets": [ { "binding": "BUCKET", "bucket_name": "feedback" } ], // Environment variables "vars": { "DISCORD_PUBLIC_KEY": "YOUR-DISCORD-PUBLIC-KEY-HEX", "AI_SEARCH_NAME": "feedback-search", "MODEL_NORMALIZE": "@cf/meta/llama-3.1-8b-instruct", "MODEL_SUMMARIZE": "@cf/meta/llama-3.1-8b-instruct" // Used for further query result summarization if needed } }