A customizable feedback widget for web apps that allows users to submit bug reports, ideas, and general feedback.
Add the widget script to your HTML:
<script src="https://nimiq-feedback.je-cf9.workers.dev/widget.js" defer></script>Add the widget styles to your HTML:
<link rel="stylesheet" href="https://nimiq-feedback.je-cf9.workers.dev/widget.css" />For better performance, load both CSS and JS with deferred loading:
<!-- Preload for better performance -->
<link
rel="preload"
href="https://nimiq-feedback.je-cf9.workers.dev/widget.css"
as="style"
onload="
this.onload = null
this.rel = 'stylesheet'
"
/>
<noscript><link rel="stylesheet" href="https://nimiq-feedback.je-cf9.workers.dev/widget.css" /></noscript>
<!-- Load JavaScript deferred -->
<script src="https://nimiq-feedback.je-cf9.workers.dev/widget.js" defer></script>Mount the widget to any DOM element:
<div id="feedback-widget"></div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const widget = window.mountFeedbackWidget('#feedback-widget', {
app: 'nimiq-wallet', // 'nimiq-wallet' | 'nimiq-pay' | 'playground'
lang: 'en', // 'en' | 'es' (optional, defaults to 'en')
feedbackEndpoint: 'https://nimiq-feedback.je-cf9.workers.dev/api/feedback', // optional, defaults to '/api/feedback' on the current origin
tags: ['beta', 'mobile'], // string[] (optional, defaults to []) - custom tags for submissions
initialForm: 'bug', // optional - directly show a specific form: 'bug' | 'idea' | 'feedback'
dark: false, // boolean (optional, defaults to false) - enables dark theme
})
// Use widget methods
widget.showFormGrid() // Show the form selection grid
// widget.showForm('bug'); // Show specific form: 'bug' | 'idea' | 'feedback'
// widget.closeWidget(); // Close the widget
// widget.goBack(); // Go back to form grid
// widget.destroy(); // Cleanup and unmount
})
</script>Add these type declarations to your Vue 3 project (e.g., in env.d.ts or types.d.ts):
// widget.types.d.ts
export type FormType = 'bug' | 'idea' | 'feedback'
export interface WidgetProps {
app: string
lang?: string
feedbackEndpoint?: string
tags?: string[]
initialForm?: FormType
dark?: boolean
}
export interface WidgetEvents {
'form-selected': FormType
'go-back': void
'form-submitted': { success: true, data: any }
'form-error': { success: false, error: string, details?: any }
'before-submit': { formData: FormData, type: FormType, app: string }
}
export interface WidgetInstance {
showFormGrid: () => void
showForm: (type: FormType) => void
closeWidget: () => void
goBack: () => void
destroy: () => void
communication?: {
on: <K extends keyof WidgetEvents>(event: K, callback: (data: WidgetEvents[K]) => void) => void
off: <K extends keyof WidgetEvents>(event: K, callback?: (data: WidgetEvents[K]) => void) => void
emit: <K extends keyof WidgetEvents>(event: K, data: WidgetEvents[K]) => void
}
}
declare global {
interface Window {
mountFeedbackWidget: MountFeedbackWidgetFn
}
}
export type MountFeedbackWidgetFn = (selector: string, props: WidgetProps) => WidgetInstance
export {}Make sure to include the type declarations in your TypeScript configuration so they are recognized globally.
feedbackEndpointdefaults to/api/feedbackon the current origin. If the widget is embedded from another domain, pass the full backend URL explicitly.
Listen to widget events using the communication API:
const widget = window.mountFeedbackWidget('#feedback-widget', {
app: 'nimiq-wallet'
})
// Listen to events
widget.communication?.on('form-selected', (formType) => {
console.log('Form selected:', formType)
})
widget.communication?.on('form-submitted', (data) => {
console.log('Form submitted successfully:', data)
})
widget.communication?.on('form-error', (error) => {
console.error('Form submission error:', error)
})
widget.communication?.on('before-submit', ({ formData, type, app }) => {
console.log('Before form submission:', { type, app })
// You can modify formData here to add additional data like debug logs
formData.append('debugInfo', JSON.stringify({ userAgent: navigator.userAgent }))
})Attach custom JSON metadata to submissions via the meta field. Metadata is stored in the database and included in GitHub issues.
widget.communication?.on('before-submit', ({ formData }) => {
formData.append('meta', JSON.stringify({
path: window.location.href,
screenSize: { width: window.innerWidth, height: window.innerHeight },
}))
})// Show specific form directly
widget.showForm('bug')
// Show form selection grid
widget.showFormGrid()
// Close the widget
widget.closeWidget()
// Navigate back to form grid
widget.goBack()
// Clean up when done
widget.destroy()The backend can also create Linear issues through the official @linear/sdk. The connector is opt-in and is controlled through query parameters on feedbackEndpoint.
Configure one or more Linear workspaces on the backend:
NUXT_LINEAR_DEFAULT_WORKSPACE=product
NUXT_LINEAR_WORKSPACES='{
"product": {
"apiKey": "lin_api_xxx",
"team": "ENG",
"labels": ["customer-feedback"]
}
}'Then point the widget at an endpoint that includes the Linear options you want:
<script>
window.mountFeedbackWidget('#feedback-widget', {
app: 'nimiq-wallet',
feedbackEndpoint:
'https://nimiq-feedback.je-cf9.workers.dev/api/feedback?linearWorkspace=product&linearTeam=ENG&linearLabels=customer-feedback,triage&linearTitle=Wallet%20feedback',
})
</script>Supported query parameters:
linearWorkspace: selects one configured backend workspacelinearTeam: required unless the workspace config already defines a default teamlinearProject: optional project selectorlinearState: optional workflow state selectorlinearAssignee: optional assignee selectorlinearLabels: comma-separated list of labelslinearTitle: custom issue titlelinearPriority: integer priority from0to4
Selectors can use IDs, and for most resources they can also use the human-readable values exposed by Linear such as team keys, label names, project names, state names, or assignee emails.
The widget automatically adapts to the user's preferred color scheme (light/dark mode). In dark mode, the widget will use lighter colors for text and darker colors for input elements. This behavior is controlled by the color-scheme CSS property.
You can override the color scheme for a specific instance of the widget by setting the color-scheme CSS property on the widget's container:
#feedback-widget {
color-scheme: light; /* Force light mode */
/* or */
color-scheme: dark; /* Force dark mode */
}By default, the widget will respect the user's system preferences and switch automatically between light and dark mode.
Use Node.js 22 and pnpm 10.34.5:
corepack enable
pnpm install --frozen-lockfile
pnpm validate
pnpm buildTo update dependencies, run pnpm outdated --compatible, update the workspace catalogs, then regenerate and verify pnpm-lock.yaml with pnpm install. Major upgrades are handled separately.
Copy backend/.env.example to backend/.env for local Nuxt development. After a build, preview the Worker locally with:
pnpm -C backend worker:devThe preview reads backend/.env. Production variables and secrets are configured in Cloudflare and are not copied into local files.
Production runs on the existing nimiq-feedback Worker at https://nimiq-feedback.je-cf9.workers.dev. The checked-in Wrangler configuration preserves the current storage:
| Binding | Resource |
|---|---|
DB |
Existing D1 database 170f7e79-6cdb-43ac-8071-ad4432f57184 |
CACHE |
Existing KV namespace 0204cdae96eb471da05c251fbf8110bb |
BLOB |
Existing R2 bucket nimiq-feedback |
Connect the existing Worker to nimiq/feedback in Workers Builds. Do not create a new Worker or storage resource. Use these build settings:
- Root directory:
backend - Production branch:
main - Non-production builds: disabled
- Build variable:
NODE_VERSION=22 - Build command:
pnpm -C .. validate && pnpm -C .. build - Deploy command:
pnpm deploy
Keep these runtime variables in the Worker settings:
NUXT_GITHUB_OWNERNUXT_GITHUB_REPONUXT_PRODUCTION_URLNUXT_LINEAR_DEFAULT_WORKSPACE, when used
Keep these secrets in the Worker settings:
NUXT_GITHUB_TOKENNUXT_SLACK_WEBHOOK_URLNUXT_LINEAR_WORKSPACES
keep_vars is enabled so Wrangler deploys retain dashboard-managed variables and secrets.
Before the first Cloudflare build, export the existing D1 database and record the current state:
pnpm -C backend exec wrangler d1 export DB --remote --output <backup-path>
pnpm -C backend exec wrangler d1 execute DB --remote --command "SELECT COUNT(*) AS submissions FROM submissions;"
pnpm -C backend exec wrangler d1 execute DB --remote --command "SELECT * FROM _hub_migrations ORDER BY id;"
pnpm -C backend exec wrangler d1 execute DB --remote --command "SELECT attachments FROM submissions WHERE attachments IS NOT NULL ORDER BY createdAt DESC LIMIT 1;"Record the D1, KV, and R2 identifiers from backend/wrangler.json, plus one attachment URL from the last query. After deployment, verify /, /widget.js, and /widget.css; submit an invalid payload; confirm the submission count is unchanged; load the recorded attachment; and check Worker logs for binding, startup, or migration errors. Migration 0004_ancient_violations.sql is additive and must apply only once.
If smoke tests fail, roll back to the previous Worker version in Cloudflare. The D1 migration remains compatible with the previous version.