Skip to content

Sadonim/user-feedback

Repository files navigation

User Feedback

User Feedback

Standalone embeddable user feedback & ticket management system
REST API + Admin Dashboard + Lightweight Widget

TypeScript Next.js 16 Prisma Tailwind CSS Vercel Vitest Coverage


Table of Contents


Overview

User Feedback is a self-hosted feedback collection and ticket management system designed to be embedded into any website. Users submit feedback (bug reports, feature requests, general inquiries) via a zero-dependency widget or a standalone page. Admins manage tickets, track analytics, and receive real-time updates through a dedicated dashboard.

The system communicates exclusively through a versioned REST API, making it interoperable with any frontend framework or static site.


Key Features

Category Feature Description
Widget Embeddable JS widget Zero-dependency vanilla TS bundle (~27 kB, ~7 kB gzipped) with Shadow DOM style isolation
Widget Configurable via data-* attributes Theme (auto/light/dark), position, button label, button color, z-index
Widget 3-step submission flow Select type → Write feedback → Submit with instant tracking ID
API Versioned REST API (/api/v1/) Public feedback submission & tracking, admin ticket management
API Consistent response envelope { success, data, error, meta } on every endpoint
API Zod validation Schema-based request validation on all endpoints
Admin Ticket dashboard List view with filtering by status, type, priority, and assignee
Admin Ticket detail view Full status history timeline, priority management, assignee panel
Admin Analytics dashboard Summary cards, status funnel chart, type breakdown, trend chart
Admin Real-time updates (SSE) Server-Sent Events stream for live ticket creation, updates, and deletions
Admin Role-based access ADMIN and MANAGER roles with JWT-based authentication (NextAuth v5)
Notifications Email notifications Status change alerts to submitters, new feedback alerts to admins (Resend)
Security Rate limiting Upstash Redis sliding window: public (5/10min), tracking (30/10min), admin (120/min)
Security CORS management Configurable origin allowlist with optional public open mode for widget embedding
Security Soft delete Tickets are soft-deleted (deletedAt field), never permanently removed
Accessibility WCAG 2.1 AA Skip navigation, keyboard navigation, ARIA labels, focus trap, axe-core tests

Tech Stack

Layer Technology Purpose
Framework Next.js 16 (App Router) SSR for admin, Route Handlers for REST API
Language TypeScript 5 (strict) End-to-end type safety
ORM Prisma 5 Type-safe database access + migrations
Database PostgreSQL (Supabase) Primary data store with connection pooling
Auth NextAuth v5 (JWT) Admin dashboard authentication
Validation Zod Request/response schema validation
UI shadcn/ui + Tailwind CSS v4 Accessible, composable admin components
Charts Recharts Analytics dashboard visualizations
State Zustand Client-side state management
Email Resend Transactional email notifications
Rate Limiting Upstash Redis Sliding window rate limiting
Widget Vanilla TypeScript (Vite IIFE bundle) Zero-dependency embeddable widget
Testing Vitest + Testing Library + jest-axe Unit, integration, and accessibility tests
Deploy Vercel Serverless, edge-ready deployment

Getting Started

Prerequisites

  • Node.js >= 18
  • PostgreSQL database (or a Supabase project)
  • npm (included with Node.js)

Installation

# Clone the repository
git clone https://github.com/Sadonim/user-feedback.git
cd user-feedback

# Install dependencies
npm install

# Copy environment variables
cp .env.example .env

Database Setup

# Generate Prisma client
npx prisma generate

# Run database migrations
npx prisma migrate deploy

# (Optional) Seed with sample data
npm run seed

The seed script creates two admin accounts and seven sample tickets:

Account Email Default Password Role
Admin admin@example.com admin1234 ADMIN
Manager manager@example.com manager1234 MANAGER

Warning: Change default seed passwords before any non-local deployment. Override via SEED_ADMIN_PASSWORD and SEED_MANAGER_PASSWORD environment variables.

Development

# Start the Next.js dev server
npm run dev

# Build the embeddable widget (outputs to public/widget.js)
npm run widget:build

# Watch mode for widget development
npm run widget:dev

Production Build

# Build for production
npm run build

# Start production server
npm start

Widget Integration

Embed the feedback widget on any website with a single <script> tag:

<script
  src="https://your-domain.com/widget.js"
  data-api-url="https://your-domain.com"
></script>

Configuration Attributes

Attribute Default Description
data-api-url (required) Base URL of your User Feedback instance
data-theme auto Widget theme: auto, light, or dark
data-position bottom-right Widget position: bottom-right, bottom-left, top-right, top-left
data-button-label Feedback Trigger button label (max 50 characters)
data-button-color #4F46E5 Trigger button hex color
data-z-index 9999 CSS z-index of the widget

Full Example

<script
  src="https://your-domain.com/widget.js"
  data-api-url="https://your-domain.com"
  data-theme="dark"
  data-position="bottom-left"
  data-button-label="Send Feedback"
  data-button-color="#3182f6"
></script>

SPA Cleanup

For single-page applications, the widget exposes a global API for teardown:

// Remove the widget programmatically
window.UserFeedbackWidget.destroy();

API Reference

All endpoints return a consistent JSON envelope:

{
  "success": true,
  "data": { ... },
  "error": null,
  "meta": { "total": 100, "page": 1, "limit": 20, "hasNextPage": true }
}

Public Endpoints

Method Endpoint Description
POST /api/v1/feedback Submit new feedback
GET /api/v1/feedback/:trackingId Track feedback status by tracking ID
GET /api/v1/status Health check

Admin Endpoints (authentication required)

Method Endpoint Description
GET /api/v1/tickets List tickets (paginated, filterable)
GET /api/v1/tickets/:id Ticket detail with status history
PATCH /api/v1/tickets/:id Update status or priority
DELETE /api/v1/tickets/:id Soft-delete a ticket
POST /api/v1/tickets/:id/assign Assign/unassign a ticket
GET /api/v1/tickets/stats Ticket statistics (count by status, type)
GET /api/v1/tickets/stream SSE stream for real-time updates
GET /api/v1/analytics/summary Aggregated analytics (funnel, distribution, rates)
GET /api/v1/analytics/timeseries Daily ticket counts (7/14/30/90 days)
GET /api/v1/admin/users List admin users (for assignee dropdown)

Ticket Filters (query parameters for GET /api/v1/tickets)

Parameter Type Default Options
status string all OPEN, IN_PROGRESS, RESOLVED, CLOSED
type string all BUG, FEATURE, GENERAL
priority string all LOW, MEDIUM, HIGH, CRITICAL
assigneeId string all CUID or unassigned
page number 1 >= 1
limit number 20 1-100
sort string createdAt createdAt, updatedAt
order string desc asc, desc

Project Structure

user-feedback/
├── prisma/
│   ├── schema.prisma              # Database schema (Feedback, User, AdminUser, StatusHistory)
│   ├── seed.ts                    # Dev seed script
│   └── migrations/                # SQL migration files
├── public/
│   └── widget.js                  # Built widget bundle (generated)
├── src/
│   ├── app/
│   │   ├── api/
│   │   │   ├── v1/
│   │   │   │   ├── feedback/      # Public: submit + track
│   │   │   │   ├── tickets/       # Admin: CRUD + stats + SSE stream
│   │   │   │   ├── analytics/     # Admin: summary + timeseries
│   │   │   │   ├── admin/users/   # Admin: user list for assignee
│   │   │   │   └── status/        # Health check
│   │   │   └── auth/[...nextauth]/ # NextAuth route handler
│   │   ├── admin/
│   │   │   ├── (protected)/       # Dashboard, tickets, analytics (auth required)
│   │   │   └── login/             # Admin login page
│   │   └── (public)/
│   │       ├── submit/            # Feedback submission page
│   │       └── track/             # Ticket tracking page
│   ├── components/
│   │   ├── admin/                 # Dashboard components + charts
│   │   ├── auth/                  # Login form
│   │   ├── feedback/              # Submission form, tracking view
│   │   ├── layout/                # Skip navigation
│   │   └── ui/                    # shadcn/ui base components
│   ├── lib/
│   │   ├── api/                   # Response helpers, CORS, auth middleware
│   │   ├── sse/                   # SSE format utilities
│   │   ├── validators/            # Zod schemas
│   │   ├── rate-limit.ts          # Upstash Redis rate limiting
│   │   └── tracking.ts            # Tracking ID generator (nanoid)
│   ├── server/
│   │   ├── db/prisma.ts           # Prisma client singleton
│   │   └── services/
│   │       ├── analytics.ts       # Analytics aggregation queries
│   │       ├── ticket-stats.ts    # Ticket statistics service
│   │       └── email/             # Email service (adapter pattern)
│   │           ├── adapters/      # Resend + Null (dev/test) adapters
│   │           └── templates/     # Email templates
│   ├── widget/                    # Embeddable widget (vanilla TS)
│   │   ├── index.ts               # Entry point + SPA destroy API
│   │   ├── config.ts              # data-* attribute parser + validation
│   │   ├── api.ts                 # Widget API client
│   │   ├── state.ts               # Immutable state machine
│   │   ├── styles.ts              # Scoped CSS-in-JS
│   │   ├── utils/focus-trap.ts    # Accessibility focus trap
│   │   └── ui/                    # DOM rendering (button, popup, overlay)
│   ├── types/                     # Shared TypeScript types
│   └── __tests__/
│       ├── unit/                  # 19 unit test files
│       ├── integration/           # 12 integration test files
│       └── a11y/                  # 8 accessibility test files (jest-axe)
├── auth.ts                        # NextAuth configuration
├── middleware.ts                   # Admin route protection
├── next.config.ts                 # CORS headers + widget serving
├── vite.widget.config.ts          # Widget build config (IIFE bundle)
├── vitest.config.ts               # Test configuration (80% coverage threshold)
└── package.json

Environment Variables

Copy .env.example and fill in the values:

cp .env.example .env
Variable Required Description
DATABASE_URL Yes PostgreSQL connection string (pooled)
DIRECT_URL Yes PostgreSQL direct connection (for migrations)
AUTH_SECRET Yes NextAuth secret key (generate with openssl rand -base64 32)
NEXTAUTH_URL Yes Application URL (e.g., http://localhost:3000)
NEXT_PUBLIC_APP_URL Yes Public-facing application URL
CORS_ALLOWED_ORIGINS Prod Comma-separated allowed origins for CORS
CORS_PUBLIC_OPEN No Set to true to allow all origins on public feedback endpoint (default: false)
UPSTASH_REDIS_REST_URL No Upstash Redis URL for rate limiting (disabled if absent)
UPSTASH_REDIS_REST_TOKEN No Upstash Redis token
RESEND_API_KEY No Resend API key for email notifications (NullAdapter used if absent)
EMAIL_FROM No Sender address (e.g., Feedback <noreply@yourapp.com>)
ADMIN_NOTIFICATION_EMAILS No Comma-separated admin emails for new feedback alerts

Rate limiting and email notifications gracefully degrade when their respective environment variables are not set. The system remains fully functional without them.


Testing

The project uses Vitest with an 80% coverage threshold enforced across lines, functions, branches, and statements.

# Run all tests
npm test

# Run unit tests only
npm run test:unit

# Run integration tests only
npm run test:integration

# Run tests in watch mode
npm run test:watch

# Run with coverage report
npm run test:coverage

Test Structure

Type Count Location Scope
Unit 19 files src/__tests__/unit/ Validators, API helpers, widget logic, email templates
Integration 12 files src/__tests__/integration/ API endpoints, CORS, notifications, SSE stream
Accessibility 8 files src/__tests__/a11y/ WCAG 2.1 AA compliance (jest-axe)

FAQ

How do tracking IDs work?

Each submitted feedback receives a unique tracking ID in the format FB-xxxxxxxx (8 random alphanumeric characters generated with nanoid). Users can look up their submission status using this ID on the tracking page or via the GET /api/v1/feedback/:trackingId endpoint. Collision handling retries up to 3 times.

Can I use a database other than Supabase?

Yes. The system uses Prisma with a standard PostgreSQL datasource. Any PostgreSQL-compatible database works. Update DATABASE_URL and DIRECT_URL in your .env file. No Supabase-specific features are used beyond the connection string.

What happens if Redis (Upstash) is not configured?

Rate limiting is disabled gracefully. All requests are allowed through. A console warning is logged on first use. This is the expected behavior for local development.

What happens if the Resend API key is not set?

The email service falls back to a NullAdapter that silently accepts all send calls without delivering emails. The API continues to function normally. This is intended for development and testing environments.

How does the SSE real-time stream work?

The GET /api/v1/tickets/stream endpoint opens a Server-Sent Events connection that polls the database every 3 seconds for changes. It emits ticket.created, ticket.updated, and ticket.deleted events. A keepalive comment is sent every 15 seconds to prevent proxy timeouts. The stream supports automatic reconnection via the standard Last-Event-ID header.

Is the widget safe from CSS conflicts?

Yes. The widget renders inside a Shadow DOM, which provides full style isolation from the host page. The host page's CSS cannot leak into the widget, and the widget's styles cannot affect the host page.

How does soft delete work?

When a ticket is deleted via the admin API, its deletedAt field is set to the current timestamp instead of removing the row. All queries automatically filter out soft-deleted records (WHERE deletedAt IS NULL). The SSE stream detects soft deletions and notifies connected clients.


Contributing

Contributions are welcome. Please follow these steps:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feat/your-feature)
  3. Write tests for your changes
  4. Ensure all tests pass (npm test) and lint is clean (npm run lint)
  5. Commit using conventional commits (e.g., feat:, fix:, refactor:)
  6. Open a pull request

License

This project is open source. See the repository for license details.

About

Standalone embeddable user feedback & ticket management system — REST API, admin dashboard, lightweight widget

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors