A self-hosted feedback analytics platform. Collect NPS-style feedback from your applications via an embeddable widget, and analyze responses through a rich dashboard with filtering, sentiment analysis, and cross-app comparison.
- Embeddable Widget — Lightweight JS plugin (~8KB gzipped) with floating button or inline embed modes. Shadow DOM for style isolation.
- NPS Analytics — Average score, NPS calculation, score distribution, trends over time.
- Sentiment Analysis — Automatic positive/negative/neutral classification using AFINN lexicon.
- Word Cloud — TF-IDF keyword extraction from feedback comments.
- Cross-App Comparison — Compare metrics across multiple registered applications.
- Top Performers — Dashboard ranking of apps by average score with medals, icons, and clickable links.
- Export — CSV and JSON export with date and application filters.
- Feedback Tagging — Tag feedback entries with categories (e.g., "performance", "bug", "onboarding"). Add/remove tags directly from the Responses table.
- Action Items — Create action items tied to tags. Track completion with checkboxes. Manage from the Application Detail page.
- NPS Alerts — Configure per-app Slack webhook alerts. Fires in real-time on every feedback submission when NPS drops below your threshold. Manual trigger via
POST /api/alerts/check. - Resolve/Archive — Mark feedback as resolved with a single click. Auto-archive entries older than 12 months.
- Weekly Digest — Auto-generated "You Said, We Did" summary page per application. Shows weekly scores, top tags, completed action items, and recent comments. Weekly cron + on-demand generation.
- App Metadata — Add a URL and icon (max 256KB) to each application. Icon stored as base64. App name links to the URL on the Applications page.
- PulseLoop Self-Feedback — Built-in system app for rating PulseLoop itself. Uses app branding, NPS visible to all users. Hidden from application management.
- Responsive Design — Sidebar collapses on mobile with a burger menu button. Overlay dismisses sidebar on tap.
- Dark Mode — System/Light/Dark theme toggle with glassmorphism UI. Full coverage for all Ant Design components.
- Security — Tenant isolation (IDOR protection), XSS-safe digest rendering, SHA-256 refresh tokens, rate limiting on auth and widget endpoints.
- Password Policy — Passwords require letters, numbers, and special characters. Change password with current password verification.
- Self-Hosted — Single
docker compose updeploys everything. No external dependencies. - Design System — Documented in DESIGN.md: color palette, spacing scale, component patterns, dark mode rules, accessibility guidelines.
Dashboard with Top Performers, NPS analytics, score trends, keywords, and recent comments.
Applications page with app icons, URLs, and descriptions.
Responses table with sentiment tags, resolve buttons, and tag management.
Application detail with embed code, action items, alert config, and digest.
Export page with format selection and date filters.
| Layer | Technology |
|---|---|
| Backend | NestJS, TypeORM, PostgreSQL 16 |
| Frontend | React 18, Ant Design 5, Recharts, TanStack Query |
| Widget | Vanilla TypeScript, Shadow DOM, Vite IIFE build |
| Infrastructure | Docker Compose, nginx |
- Docker and Docker Compose
git clone https://github.com/nikola-kovacevic/feedback.git
cd feedback
cp .env.example .envEdit .env and set a secure JWT_SECRET:
JWT_SECRET=your-secure-random-string-here
Start the application:
docker compose up --build -dOpen http://localhost in your browser.
| Service | Port |
|---|---|
| Frontend (nginx) | 80 |
| Backend API | 3000 |
| PostgreSQL | 5432 |
- Node.js 20+
- pnpm (
corepack enable) - PostgreSQL running locally
pnpm install
cp .env.example .env
# Edit .env: set DATABASE_URL to your local PostgreSQL
# Edit .env: set JWT_SECRET to any string for dev# Terminal 1: Backend
pnpm dev:backend
# Terminal 2: Frontend
pnpm dev:frontend
# Terminal 3: Widget (optional, for widget development)
pnpm dev:widgetFrontend runs at http://localhost:5173 with API proxy to the backend.
After registering an application in the dashboard, copy the embed snippet from the Application Detail page. Or use the code below:
<script src="http://your-pulseloop-server/widget/feedback.js"></script>
<script>
FeedbackWidget.init({
apiKey: 'your-api-key',
});
</script><div id="feedback-container"></div>
<script src="http://your-pulseloop-server/widget/feedback.js"></script>
<script>
FeedbackWidget.render({
apiKey: 'your-api-key',
target: '#feedback-container',
});
</script>Pass user metadata to associate feedback with specific users:
FeedbackWidget.init({
apiKey: 'your-api-key',
user: { id: '123', email: 'user@example.com', role: 'admin' },
});Swagger UI is available at http://localhost:3000/api/docs when the backend is running.
# Start the app first (Docker or local dev), then:
cd packages/backend
BASE_URL=http://localhost:3000 npx jest --config ./test/jest-e2e.json --forceExit --runInBand67 e2e tests covering: auth (register, login, refresh, password change, validation), applications CRUD, widget API, feedback submission + tagging + resolve, action items CRUD, alert config, analytics (6 endpoints), export (CSV + JSON), weekly digest, multi-user isolation, tenant scoping (403 on non-owned resources), and input validation.
pulseloop/
├── packages/
│ ├── backend/ # NestJS API server
│ ├── frontend/ # React dashboard SPA
│ └── widget/ # Embeddable JS plugin
├── docker-compose.yml
├── .env.example
├── DESIGN.md
└── pnpm-workspace.yaml
| Variable | Default | Description |
|---|---|---|
POSTGRES_DB |
feedback | Database name |
POSTGRES_USER |
feedback | Database user |
POSTGRES_PASSWORD |
changeme | Database password |
DATABASE_URL |
— | PostgreSQL connection string |
JWT_SECRET |
— | Required. Secret for signing JWTs |
JWT_EXPIRES_IN |
15m | Access token expiry |
CORS_ORIGIN |
http://localhost | Allowed origins for dashboard |
BACKEND_URL |
http://localhost:3000 | Backend URL for embed snippets |
TYPEORM_SYNC |
false | Set to true to auto-sync schema (dev only) |