Architecture
System architecture, design decisions, and technical implementation
Last updated January 29, 2026
Architecture
Understanding the technical implementation of Feedback Pulse.
Tech Stack
Frontend
- Framework: Next.js 15 (App Router)
- Styling: Tailwind CSS
- UI Components: shadcn/ui
- State Management: React hooks
- Charts: Recharts with shadcn chart components
- Markdown: react-markdown, remark-gfm
Backend
- Runtime: Node.js
- Framework: Next.js API routes
- ORM: Drizzle ORM
- Database: PostgreSQL (Vercel Postgres)
- Authentication: NextAuth.js v5
- AI: Hugging Face Inference API
Deployment
- Platform: Vercel
- Database: Vercel Postgres
- CDN: Vercel Edge Network
Authentication Flow
mermaidsequenceDiagram participant User participant Browser participant NextAuth participant GitHub participant Database User->>Browser: Click "Sign in with GitHub" Browser->>NextAuth: Initiate OAuth flow NextAuth->>GitHub: Redirect to GitHub OAuth GitHub->>User: Request authorization User->>GitHub: Approve GitHub->>NextAuth: Return auth code NextAuth->>GitHub: Exchange code for access token GitHub->>NextAuth: Return token + profile NextAuth->>Database: Create/update user & account Database->>NextAuth: Confirm NextAuth->>Browser: Set session cookie Browser->>User: Redirect to dashboard
Implementation
Provider: GitHub OAuth
Session Management:
- JWT-based sessions
- Secure HTTP-only cookies
- 30-day expiration
Configuration: src/lib/auth/config.ts
typescriptexport const authOptions: NextAuthConfig = { providers: [ GitHub({ clientId: process.env.AUTH_GITHUB_ID!, clientSecret: process.env.AUTH_GITHUB_SECRET!, }), ], adapter: DrizzleAdapter(db, ...), session: { strategy: "jwt" }, pages: { signIn: "/login", }, };
Protected Routes
Middleware: middleware.ts
Protected routes:
/dashboard/*- Requires authentication/api/*- Most endpoints require authentication (except widget)
Authorization:
typescriptimport { auth } from "@/lib/auth/config"; export async function GET() { const session = await auth(); if (!session?.user) { return new Response("Unauthorized", { status: 401 }); } // ... authorized logic }
Database Schema
Entity Relationship Diagram
mermaiderDiagram USERS ||--o{ ACCOUNTS : has USERS ||--o{ PROJECTS : owns PROJECTS ||--o{ FEEDBACK : receives FEEDBACK ||--o{ FEEDBACK_LABELS : has USERS ||--o{ API_KEYS : owns USERS { uuid id PK string email UK string password string name string image timestamp emailVerified timestamp createdAt } ACCOUNTS { uuid userId FK string type string provider PK string providerAccountId PK string refresh_token string access_token int expires_at } PROJECTS { uuid id PK uuid userId FK string name string projectKey UK string url string description timestamp createdAt } FEEDBACK { uuid id PK uuid projectId FK enum type string message string userEmail string userName enum sentiment boolean resolved timestamp createdAt } FEEDBACK_LABELS { uuid id PK uuid feedbackId FK string label timestamp createdAt } API_KEYS { uuid id PK uuid userId FK string name string key UK timestamp createdAt }
Schema Definition
typescriptexport const users = pgTable("users", { id: uuid("id").primaryKey().defaultRandom(), email: text("email").notNull().unique(), name: text("name"), image: text("image"), emailVerified: timestamp("emailVerified", { mode: "date" }), createdAt: timestamp("created_at").defaultNow().notNull(), }); export const projects = pgTable("projects", { id: uuid("id").primaryKey().defaultRandom(), userId: uuid("user_id").references(() => users.id, { onDelete: "cascade" }).notNull(), name: text("name").notNull(), projectKey: text("project_key").notNull().unique(), url: text("url"), description: text("description"), createdAt: timestamp("created_at").defaultNow().notNull(), }); export const feedback = pgTable("feedback", { id: uuid("id").primaryKey().defaultRandom(), projectId: uuid("project_id").references(() => projects.id, { onDelete: "cascade" }).notNull(), type: feedbackTypeEnum("type").notNull(), // bug | feature | other message: text("message").notNull(), userEmail: text("user_email"), userName: text("user_name"), sentiment: sentimentEnum("sentiment"), // positive | neutral | negative resolved: boolean("resolved").default(false).notNull(), createdAt: timestamp("created_at").defaultNow().notNull(), }); export const feedbackLabels = pgTable("feedback_labels", { id: uuid("id").primaryKey().defaultRandom(), feedbackId: uuid("feedback_id").references(() => feedback.id, { onDelete: "cascade" }).notNull(), label: text("label").notNull(), createdAt: timestamp("created_at").defaultNow().notNull(), });
Enums
typescriptexport const feedbackTypeEnum = pgEnum("feedback_type", ["bug", "feature", "other"]); export const sentimentEnum = pgEnum("sentiment", ["positive", "neutral", "negative"]);
Cascade Deletes
- Deleting a user cascades to projects, accounts, and API keys
- Deleting a project cascades to all feedback
- Deleting feedback cascades to all labels
Widget Design
Client-Side Implementation
The widget is a vanilla JavaScript implementation that creates a floating feedback button and modal.
Key Features:
- ✅ Zero dependencies
- ✅ Dark mode support (auto-detects system preference)
- ✅ Accessible (keyboard navigation, ARIA labels)
- ✅ Mobile responsive
- ✅ CORS-enabled API communication
Widget Initialization
html<script src="https://pulsefeedback.vercel.app/widget.js"></script> <script> FeedbackPulse.init({ projectKey: 'your-project-key', apiUrl: 'https://pulsefeedback.vercel.app/api' }); </script>
Widget Architecture
┌─────────────────────────────┐
│ Floating Button (Pill) │
│ - Bottom right position │
│ - Pulse animation │
│ - Dark mode support │
└──────────┬──────────────────┘
│ Click
▼
┌─────────────────────────────┐
│ Feedback Modal │
│ │
│ ┌────────────────────────┐ │
│ │ Type Selection (Tabs) │ │
│ └────────────────────────┘ │
│ ┌────────────────────────┐ │
│ │ Message Textarea │ │
│ └────────────────────────┘ │
│ ┌────────────────────────┐ │
│ │ Optional: Name & Email │ │
│ └────────────────────────┘ │
│ ┌────────────────────────┐ │
│ │ Submit Button │ │
│ └────────────────────────┘ │
└─────────────────────────────┘
│ Submit
▼
POST /api/widget/[key]
Dark Mode Detection
javascriptconst prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
The widget automatically applies dark styling when the system prefers dark mode.
CORS Handling
The widget API allows cross-origin requests:
typescript// Widget API headers headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Headers": "Content-Type", }
AI Service
Sentiment Analysis
Model: cardiffnlp/twitter-roberta-base-sentiment-latest
Provider: Hugging Face Inference API
Process:
- Send feedback message to model
- Receive sentiment scores (positive, neutral, negative)
- Select highest scoring sentiment (threshold: 0.5)
- Fallback to "neutral" on error/timeout
Timeout: 10 seconds
Implementation:
typescriptexport async function analyzeSentiment(text: string): Promise<"positive" | "neutral" | "negative"> { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); try { const response = await fetch( `https://router.huggingface.co/hf-inference/models/cardiffnlp/twitter-roberta-base-sentiment-latest`, { method: "POST", headers: { "Authorization": `Bearer ${process.env.HF_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ inputs: text }), signal: controller.signal, } ); const result = await response.json(); // ... process result } catch (error) { return "neutral"; // Fallback } finally { clearTimeout(timeoutId); } }
Label Generation
Method: Keyword Extraction (Regex-based)
Process:
- Match feedback text against predefined patterns
- Score each match based on frequency
- Filter matches with score > 2%
- Return top 3 labels
Label Categories:
- UI/UX
- Performance
- Bug
- Feature Request
- Documentation
- Pricing
- Integration
- Security
- Accessibility
- Mobile
Example:
typescriptconst labelPatterns = { "ui/ux": /(ui|ux|interface|design|layout|button|menu)/gi, "performance": /(slow|fast|speed|lag|freeze|crash)/gi, "bug": /(bug|error|broken|issue|problem|fail)/gi, // ... more patterns };
AI Summary (Analytics)
Model: meta-llama/Llama-3.2-3B-Instruct
Provider: Hugging Face Inference API
Input: Analytics data (totals, trends, distributions)
Output: Markdown-formatted insights (3-5 key points)
Prompt Engineering:
typescriptconst prompt = `You are an analytical assistant... Based on the following analytics data, provide 3-5 key insights: Total Feedback: ${data.totalFeedback} Feedback by Type: ${data.feedbackByType.bug} bugs, ... Sentiment: ${data.sentimentDistribution.positive} positive, ... **Format your response in Markdown** with: - Use **bold** for key metrics - Use bullet points for lists - Keep it under 200 words Focus on: trends, concerns, positive indicators, actionable recommendations`;
Future Improvements
Planned Features
1. Email Notifications
- Notify project owners of new feedback
- Daily/weekly digest options
- Customizable notification preferences
2. Webhooks
- POST feedback data to custom URLs
- Enable integration with Slack, Discord, etc.
- Signature verification for security
3. Custom Branding
- Customize widget colors
- Upload logo
- Custom positioning options
4. Advanced Filtering
- Filter by sentiment
- Filter by date range
- Filter by resolved status
- Multi-label filtering
5. Export Functionality
- Export to CSV
- Export to JSON
- Scheduled exports
6. Team Collaboration
- Multiple users per project
- Role-based access control (owner, editor, viewer)
- Activity log
7. Public Roadmap
- Display feature requests publicly
- User voting system
- Status updates (planned, in progress, completed)
8. API Rate Limiting
- Prevent abuse
- Per-user or per-project limits
- Graceful degradation
Technical Debt
1. Testing
- Add unit tests for API routes
- Add integration tests for auth flow
- Add E2E tests for widget
2. Performance
- Implement caching for analytics
- Add database indexes
- Optimize SQL queries
3. Security
- Add CSRF protection
- Implement API rate limiting
- Add input validation middleware
4. Monitoring
- Add error tracking (Sentry)
- Add analytics (Vercel Analytics)
- Add performance monitoring
Security Considerations
Data Protection
- All passwords hashed with bcrypt (if credentials auth is added)
- Session tokens stored in HTTP-only cookies
- HTTPS enforced in production
Authorization
- All API routes check project ownership
- User can only access their own projects
- Feedback access restricted to project owner
Input Sanitization
- HTML removed from user input
- SQL injection prevented by Drizzle ORM parameterization
- Maximum input lengths enforced
API Keys
- Project keys are UUID v4 (non-guessable)
- Keys displayed as masked (****) in UI
- Full key only shown on creation
Performance Optimizations
Database
- Indexed columns:
email,projectKey,feedbackId - Cascade deletes for efficient cleanup
- Pagination for large result sets
Frontend
- Server-side rendering with Next.js
- Image optimization
- Code splitting
- Lazy loading for charts
API
- Async AI processing (doesn't block responses)
- Response streaming for large datasets
- Gzip compression
Deployment
Environment Variables
bash# Database DATABASE_URL="postgresql://..." # NextAuth AUTH_SECRET="..." AUTH_GITHUB_ID="..." AUTH_GITHUB_SECRET="..." # AI HF_API_KEY="..." # App NEXT_PUBLIC_APP_URL="https://your-domain.com" NEXT_PUBLIC_FEEDBACK_PULSE_KEY="your-key"
Build Process
bashnpm run build # Build Next.js app npm run start # Start production server
Database Migrations
bashnpx drizzle-kit generate:pg # Generate migration npx drizzle-kit push:pg # Apply migration
Project Structure
feedback-pulse/
├── public/
│ └── widget.js # Standalone widget
├── src/
│ ├── app/
│ │ ├── (auth)/ # Auth pages (login, signup)
│ │ ├── api/ # API routes
│ │ ├── dashboard/ # Dashboard pages
│ │ ├── contact/ # Contact page
│ │ └── page.tsx # Landing page
│ ├── components/
│ │ ├── ui/ # shadcn components
│ │ ├── features/ # Feature preview components
│ │ └── landing/ # Landing page components
│ ├── lib/
│ │ ├── auth/ # NextAuth config
│ │ ├── db/ # Database & schema
│ │ └── ai-service.ts # AI functions
│ └── styles/
│ └── globals.css # Global styles
├── docs/ # Documentation (markdown)
└── drizzle/ # Database migrations