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

mermaid
sequenceDiagram
    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

typescript
export 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:

typescript
import { 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

mermaid
erDiagram
    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

src/lib/db/schema.ts

typescript
export 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

typescript
export 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

public/widget.js

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

javascript
const 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

src/lib/ai-service.ts

Sentiment Analysis

Model: cardiffnlp/twitter-roberta-base-sentiment-latest

Provider: Hugging Face Inference API

Process:

  1. Send feedback message to model
  2. Receive sentiment scores (positive, neutral, negative)
  3. Select highest scoring sentiment (threshold: 0.5)
  4. Fallback to "neutral" on error/timeout

Timeout: 10 seconds

Implementation:

typescript
export 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:

  1. Match feedback text against predefined patterns
  2. Score each match based on frequency
  3. Filter matches with score > 2%
  4. Return top 3 labels

Label Categories:

  • UI/UX
  • Performance
  • Bug
  • Feature Request
  • Documentation
  • Pricing
  • Integration
  • Security
  • Accessibility
  • Mobile

Example:

typescript
const 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:

typescript
const 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

bash
npm run build    # Build Next.js app
npm run start    # Start production server

Database Migrations

bash
npx 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