This repository contains a Voice of Customer (VoC) platform with separate frontend and backend folders.
backend/- Express backend API, ingestion services, theme analysis, action recommendation, and static file serving.frontend/- Astro-based dashboard UI for loading VoC actions, clusters, and emerging issue spikes.
cd backend
npm install
npm run devThe backend runs on http://localhost:4000 and serves the built frontend output (frontend/dist).
cd frontend
npm install
npm run dev # local dev server at http://localhost:4321, /api proxied to :4000
npm run build # static build → frontend/dist (served by the backend)Node.js note: The frontend pins
[email protected], which supports Node>=18.14.1(current project toolchain is Node 20.x). If you upgrade the machine's Node to>=20.3.0you may bump to a newer Astro. After changing frontend sources, runnpm run buildso the backend serves fresh output.
API endpoints:
GET /api/healthPOST /api/feedback/ingestPOST /api/themes/estimateGET /api/themesGET /api/actionsPOST /api/actionsPATCH /api/actions/:actionIdGET /api/clustersGET /api/detection/spikesGET /api/overviewPOST /api/chatGET /api/chat/history/:sessionIdGET /api/auth/userGET /api/pixel— 1×1 tracking pixel (public, no auth)GET /api/pixel.gif— alias of/api/pixelGET /api/pixel.js— embeddable tracking snippet (public, no auth)
This backend supports OpenID Connect with Keycloak, local JSON role mapping for dev, and can be adapted to any other JWT provider that exposes role claims.
KEYCLOAK_ISSUER/OIDC_ISSUER— issuer URL for your OpenID Connect providerKEYCLOAK_AUDIENCE/OIDC_AUDIENCE— expected audience claimKEYCLOAK_CLIENT_ID/OIDC_CLIENT_ID— client ID used by the backendREQUIRE_AUTH—falsedisables mandatory token validation for local devLOCAL_AUTH_ENABLED—trueenables local JSON role fallback when auth is disabledLOCAL_ROLES_FILE— path to a local role definitions file (default:backend/data/roles.json)
The system reads roles from:
- JWT token claims
realm_access.roles - JWT token claims
resource_access[clientId].roles - local JSON role mappings when
LOCAL_AUTH_ENABLED=true - header-based local roles using
x-local-user,x-local-email, andx-local-roles
Example roles:
admin— full accessmanager— action creation and updatesingest— feedback and ingestion operationsanalyst— theme estimation
Use local role mapping when you cannot install Docker or run Keycloak.
- Create or update
backend/data/roles.json.
{
"users": [
{
"username": "admin",
"email": "[email protected]",
"roles": ["admin", "manager", "ingest", "analyst"]
},
{
"username": "analyst",
"email": "[email protected]",
"roles": ["analyst"]
},
{
"username": "ingester",
"email": "[email protected]",
"roles": ["ingest"]
}
]
}- Start the backend with local auth enabled:
export REQUIRE_AUTH=false
export LOCAL_AUTH_ENABLED=true- Call protected endpoints with a local identity header.
curl -H "x-local-user: admin" -H "x-local-roles: admin,manager" http://localhost:4000/api/themes/estimateIf you can use Docker and Keycloak, the current backend supports OIDC role enforcement with the environment variables above.
- Run Keycloak locally with Docker:
docker run -p 8080:8080 --name keycloak -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:21.1.1 start-dev- Create a realm named
vocand a clientvoc-client. - Assign realm roles like
admin,manager,ingest, andanalystto test users. - Set backend env vars:
export KEYCLOAK_ISSUER=http://localhost:8080/auth/realms/voc
export KEYCLOAK_AUDIENCE=voc-backend
export KEYCLOAK_CLIENT_ID=voc-client
export REQUIRE_AUTH=trueIf you integrate a different OpenID Connect provider or authorization tool, make sure it issues a JWT with:
realm_access.roles- or
resource_access[<clientId>].roles
If your provider cannot issue roles in those claims, use the local JSON fallback for development and add a small adapter later.
After running npm run build in frontend/, the generated dashboard is served by the backend at:
http://localhost:4000
It contains an Astro-based dashboard that loads actions, theme clusters, and spikes from the backend.
RVoice goes beyond traditional VoC platforms by turning raw feedback into prioritized, actionable insights:
- PII Detection & Masking — emails, phones, names, and addresses are detected and masked at ingestion time, so raw PII is never persisted.
- Sentiment Analysis — rule-based lexicon scoring with confidence, aspects, and negation/intensifier handling.
- Theme Extraction & Clustering — groups feedback into themes, then clusters related themes across products and journey stages.
- Spike Detection — flags emerging issues based on issue score thresholds and trend detection.
- AI Prioritization — ranks issues by impact × frequency × sentiment, with recommended actions and assigned owners.
- Chat Assistant — conversational Q&A over the feedback corpus.
GET /api/ai/insights— prioritized list of top issues with recommendationsPOST /api/ai/prioritize— score and rank a set of themes
| Capability | Primary (Rule-Based) | Optional (ML) |
|---|---|---|
| Sentiment | compromise + sentiment lexicon |
@xenova/transformers (DistilBERT SST-2) |
| Theme Extraction | Keyword matching | @xenova/transformers (MobileBERT-MNLI zero-shot) |
| Issue Categorization | Issue score threshold | @xenova/transformers (MobileBERT-MNLI zero-shot) |
| Clustering | Metadata grouping | TF-IDF + cosine similarity |
| Detection | Issue score threshold | Trend-based (time windows) |
| Prioritization | Impact × frequency scoring | — |
The rule-based pipeline works offline with zero dependencies. The optional ML layer can be enabled via environment variable for improved accuracy.
RVoice ships with an optional on-device ML layer powered by @xenova/transformers — open-source ONNX models that run locally with no external API calls or data leaving the server.
Models used:
| Model | Size | Purpose |
|---|---|---|
Xenova/distilbert-base-uncased-finetuned-sst-2-english |
~66 MB | Sentiment analysis (text-classification) |
Xenova/mobilebert-uncased-mnli |
~25 MB | Theme extraction & issue categorization (zero-shot classification) |
How it works:
- The ML layer is always additive — the rule-based pipeline remains the default and works fully offline.
- When
ML_ENABLED=true, the enhanced services (analyzeSentimentEnhanced,categorizeFeedbackItemEnhanced,detectSpikesEnhanced) blend ML results with rule-based output, preferring ML when its confidence is high. - Models are downloaded on first use and cached locally in
.cache/models/(configurable viaML_CACHE_DIR). - All ML inference runs on masked text — PII is stripped before any model sees it.
Environment variables:
| Variable | Default | Description |
|---|---|---|
ML_ENABLED |
false |
Set to true to enable the ML layer |
ML_CACHE_DIR |
backend/.cache/models |
Local model cache directory |
ML_SENTIMENT_MODEL |
Xenova/distilbert-base-uncased-finetuned-sst-2-english |
Sentiment model ID |
ML_ZERO_SHOT_MODEL |
Xenova/mobilebert-uncased-mnli |
Zero-shot classification model ID |
ML_TIMEOUT_MS |
10000 |
Inference timeout in milliseconds |
New services:
backend/src/services/aiModelService.js— ML facade (lazy-loaded singleton pipelines, timeout guard)backend/src/services/aiSentimentService.js— ML sentiment analysisbackend/src/services/aiThemeService.js— ML theme extractionbackend/src/services/aiIssueService.js— ML issue categorization
Enhanced functions (rule-based + ML blend):
analyzeSentimentEnhanced(text)innlpService.jscategorizeFeedbackItemEnhanced(item)incategorizationService.jsdetectSpikesEnhanced(themes)indetectionService.js
Note:
@xenova/transformersis an optional dependency. If it is not installed orML_ENABLEDis not set, all ML functions returnnulland the rule-based pipeline continues to work unchanged.
PII_MASKING_ENABLED— enable/disable PII masking (default:true)- Masked feedback is flagged with
piiFlaggedandpiiTypesfor auditability - No PII is sent to external AI models — only masked text
RVoice ships with a tracking pixel so customer websites can stream anonymous page-view analytics straight into the platform. Every hit is ingested through the canonical feedback pipeline (PII-masked, deduplicated, then persisted) and shows up in the dashboard as a web-pixel source alongside Zendesk, App Store, Intercom, and manual entries.
Add a single <script> tag to any page — no SDK, no build step, works on any stack:
<script src="https://YOUR_HOST/api/pixel.js"
data-source="web-pixel"
data-product="checkout"
data-store="store-42"
data-journey-stage="checkout"></script>The script auto-detects the backend base URL from its own src (no hardcoded domain), so it works from any host. It captures browser context (viewport, screen, color depth, language, cookies enabled, referrer, page title, URL), keeps a persistent visitor id in the _rv_pixel_cid cookie, and fires an invisible 1×1 image to GET /api/pixel.
| Attribute | Description | Default |
|---|---|---|
data-source |
Source name recorded on the feedback item | web-pixel |
data-product |
Product dimension (e.g. checkout) |
— |
data-store |
Store/location dimension (e.g. store-42) |
— |
data-journey-stage |
Journey stage (e.g. checkout, delivery) |
— |
data-rating |
Optional 1–5 rating | — |
data-pixel-endpoint |
Override the analytics base URL | auto-detected |
GET /api/pixel— returns the 1×1 transparent GIF and asynchronously ingests the hitGET /api/pixel.gif— alias of/api/pixel(for pixels that require a.gifextension)GET /api/pixel.js— the embeddable tracking snippet
These endpoints are public by design (no auth): they are embedded on third-party websites that cannot attach JWTs. The response is sent immediately and ingestion runs in the background, so tracking never slows down the host page. Disable ingestion entirely with PIXEL_ENABLED=false (the GIF still responds).
Each hit becomes a canonical feedback item:
| Field | Value |
|---|---|
externalId |
pixel_<clientId>_<urlHash8>_<minuteBucket> (deduplication key) |
source |
data-source or web-pixel |
origin |
pixel |
customer.externalId |
persistent visitor id from _rv_pixel_cid |
text |
Pixel visit: /checkout from google.com |
product / store / journeyStage |
from data-* attributes |
metadata |
userAgent, clientIp, pageUrl, pageTitle, referrer, language, viewport, screen, colorDepth, cookiesEnabled |
- Only non-PII browser hints and URL/referrer are collected; no keystrokes or form data.
- Hits flow through the standard PII-masking layer before persistence.
- The visitor id is a random, non-identifying cookie value.
- For fully cookieless / minimum-data tracking, hardcode the query params on an
<img>tag instead:<img src="https://YOUR_HOST/api/pixel?cid=demo-1&url=https://example.com&source=web-pixel" alt="" />
- Stakeholder Mapping — personas, role-to-feature matrix, role-based access
- Requirements — MVP prioritization, data sources, AI workflow, privacy
- Use
npm run test:integrationinsidebackend/to validate backend flows. - The backend currently stores data in
backend/data/. - The new chat assistant is available via the browser UI and the backend endpoints
POST /api/chatandGET /api/chat/history/:sessionId. - To enable optional OpenAI fallback, set
OPENAI_API_KEYin the backend environment.