@floo-one/nuxt-feedback
@floo-one/nuxt-feedback
A drop-in in-app feedback dialog for Nuxt 4. Your users press a shortcut, pick Bug or Feedback, and hit send:
- 🐞 Bug → a GitHub issue (labelled
bug) with the full context — reporter, page URL, app build, recent console errors — plus a type (crash / visual / data / performance) and a severity (critical → cosmetic). - 💬 Feedback → a GitHub issue on the repo you choose (ideas, praise, or anything else).
It's auth-agnostic (you give it a tiny function to identify the user — it never imports your auth), and when the user is already logged in it doesn't bother asking for their email.
Requirements
- Nuxt ≥ 4
@nuxt/ui≥ 4 (peer dependency — the dialog is built from Nuxt UI components your app already has)
Setup (3 steps)
1. Install
pnpm add @floo-one/nuxt-feedback
2. Register and configure it
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/ui', '@floo-one/nuxt-feedback'],
feedback: {
github: { repo: 'your-org/your-repo' }, // where issues are filed
shortcut: 'g-f', // press g then f
resolveUserPath: './server/feedback-user.ts',
},
})
3. Tell it who the user is
Create the file you referenced in resolveUserPath. It runs on the server, gets the request event, and returns the logged-in user (or null). Use whatever auth your app already has — the module never touches it.
// server/feedback-user.ts
import type { H3Event } from 'h3'
export default async function resolveUser(event: H3Event) {
const session = await getServerSession(event) // ← however your app reads the session
return session?.user
? { id: session.user.id, email: session.user.email, name: session.user.name }
: null
}
Must never throw — return
nullwhen logged out. When it returns a user, the dialog hides the email field and attaches their identity to the report automatically. (OmitresolveUserPathentirely to always-anonymous + ask for an email.)
That's it. The dialog auto-mounts everywhere — press g then f, or open it from code:
const { open } = useFeedback()
open() // last-used type
open('bug') // or 'feature' (the Feedback tab)
Environment variables
Only one secret is needed — the GitHub token:
| Variable | Required | What it's for |
|---|---|---|
NUXT_GITHUB_TOKEN |
Yes | A GitHub token the server uses to create issues (bugs and ideas). Read from runtimeConfig — never exposed to the browser. |
Getting the GitHub token — create a fine-grained PAT scoped to only your target repo with Issues: Read and write, then:
# .env (local — git-ignored, never commit a real token)
NUXT_GITHUB_TOKEN=github_pat_xxxxxxxx
In production, set NUXT_GITHUB_TOKEN in your host's secret store (Coolify / Vercel / etc.) and redeploy. (No org-restricted PATs? A classic token with the repo scope works too.)
Options
| Option | Type | Default | Description |
|---|---|---|---|
github.repo |
string |
— | Required. Target repo for issues, "owner/name". |
shortcut |
string |
'g-f' |
Keybind in Nuxt UI defineShortcuts syntax. Pick one that doesn't clash with your app's chords. |
github.labels.feature |
string |
'enhancement' |
Label for feedback issues. (Bugs use bug.) |
resolveUserPath |
string |
— | Path to your identity hook (see step 3). Omit for always-anonymous. |
resolveAppPath |
string |
— | Path to a hook mapping a submission to an "app bucket" (used for the app: label). Omit to fall back to context.app. |
onSubmitPath |
string |
— | Path to a hook run server-side after a report is filed — notifications, logging, tracking. Receives (report, result); a throw is caught and ignored. See below. |
enabled |
boolean |
true |
Turn the whole thing off (e.g. per-environment). |
The GitHub token is never an option — it lives only in
runtimeConfig(NUXT_GITHUB_TOKEN) and never reaches the client bundle, responses, or logs.
How it routes
bug → GitHub issue (label: bug)
carries type (crash | visual | data | performance)
and severity (critical | blocking | annoying | cosmetic)
feedback → GitHub issue (label: enhancement)
Prefixed label scheme (opt in by setting any
github.labels.*Prefixorbase): issues get<base>+type:<bug|idea>+app:<bucket>, and bugs also getseverity:<level>+category:<kind>.
A report is never silently dropped, and submit failures return a clean error toast (no provider internals leak to the client).
React to submissions (onSubmitPath)
Point onSubmitPath at a server file to run your own code after each report is
filed — ping Slack, write to a DB, track it for a "my reports" view. It runs
after the GitHub issue is created and never blocks the user's submission (a
throw is caught and logged).
// server/feedback-submit.ts
export default async function onSubmit(report, result) {
// report: { type, message, severity?, category?, user, email?, app, labels, context? }
// result: { channel: 'github', issue: { number, url } }
await fetch(process.env.SLACK_WEBHOOK!, {
method: 'POST',
body: JSON.stringify({ text: `New ${report.type}: ${result.issue.url}` }),
})
}
Drop it into a repo with one prompt
Paste this into an AI coding agent (Claude Code, Cursor, …) inside the target Nuxt 4 repo:
Integrate the npm package @floo-one/nuxt-feedback into this Nuxt 4 app.
1. Install it: `pnpm add @floo-one/nuxt-feedback`. It needs @nuxt/ui v4 as a peer —
confirm this app already uses @nuxt/ui (it should).
2. In nuxt.config.ts: add '@floo-one/nuxt-feedback' to `modules`, and add a `feedback`
block with:
- github.repo: the "owner/name" of THIS app's GitHub repo (find it from the git remote)
- shortcut: 'g-f' — but first grep the codebase for existing `defineShortcuts` chords
and pick a non-colliding one if g-f is taken
- resolveUserPath: './server/feedback-user.ts'
3. Create server/feedback-user.ts that default-exports
`async (event: H3Event) => ({ id, email, name }) | null`. Wire it to THIS app's existing
auth — find how the app reads its session/user on the server and use that. Return null
when logged out; it must never throw.
4. Add `NUXT_GITHUB_TOKEN` to .env.example with a comment: server-only, a GitHub token with
"Issues: write" on the repo. Do NOT put a real token anywhere in the code or config —
I'll set the real value in the host's secret store.
5. Verify: `nuxt typecheck` (or build) passes, and the dialog opens with the shortcut.
Report what you changed and what I still need to do (set NUXT_GITHUB_TOKEN).
Hard rule: the GitHub token is a secret. Read it only via runtimeConfig (NUXT_GITHUB_TOKEN);
never a public option, never committed, never in the client bundle.
Host the dashboard (live demo)
The repo ships a Dockerfile that builds the dashboard app — today a self-contained demo site with the module installed on a real page (dialog and API routes fully working), and long-term the dashboard for triaging the issues the module files.
On Coolify: create an application from this repo, pick the Dockerfile build pack (port 3000), and set one env var:
| Variable | Value |
|---|---|
NUXT_GITHUB_TOKEN |
A GitHub token with Issues: write on the repo demo submissions should land in (the dashboard targets floo-one/nuxt-feedback). |
Without the token the site still runs and the dialog opens — submits just fail with an error toast.
Or locally:
docker build -t nuxt-feedback-demo .
docker run -p 3000:3000 -e NUXT_GITHUB_TOKEN=github_pat_xxx nuxt-feedback-demo
Develop this module
pnpm install
pnpm dev # run the dashboard app locally
pnpm lint && pnpm test && pnpm test:types
pnpm prepack # build the distributable
License
MIT
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.2.4 | ^3.0 | MIT | |
| 0.2.3 | ^3.0 | MIT | |
| 0.2.2 | ^3.0 | MIT | |
| 0.2.0 | ^3.0 | MIT | |
| 0.1.0 | ^3.0 | MIT |