Skip to main content
Glama
hasna
by hasna

Open Feedback

Reusable feedback collection for Hasna-coded apps.

Open Feedback provides a small HTTP API, TypeScript SDK, CLI, MCP server, and local SQLite storage so apps can collect product feedback without standing up a database server first. Production deployments can inject a cloud-backed FeedbackStore adapter while keeping local operation unchanged. The local project slug is open-feedback; the GitHub repository is hasna/feedback.

Install

bun add @hasna/feedback

For local CLI usage:

bunx @hasna/feedback init
feedback serve --port 8787

Deprecated: the separate feedback-serve bin is deprecated as of 0.3.0 and will be removed in 0.4.0. Use feedback serve — it is the same server with the same --host/--port options. The bin still works and now prints a migration notice to stderr.

Related MCP server: feedback-mcp-server

HTTP API

The HTTP API is a local development server. It serves the append-only JSONL store at ~/.hasna/feedback/feedback.jsonl and has no PostgreSQL support: createFeedbackStore() throws in cloud mode unless the host injects a FeedbackStore adapter. To run feedback as a real service, mount createFeedbackHandler() from @hasna/feedback/api inside your own app and pass it a store you control — which is what the Hasna platform apps do.

Start the local API:

feedback serve --host 127.0.0.1 --port 8787

Set FEEDBACK_API_TOKEN to require bearer-token auth for every API request. Shared deployments should use scoped tokens instead of one broad token:

  • submit: accepts browser or app-server submissions.

  • read: lists feedback, reads one item, and reads stats.

  • triage: updates status.

  • export: streams JSONL exports.

For public collection, enable public submit only at the app backend or feedback service boundary and keep read, triage, and export scoped. In shared deployment mode, non-local read, triage, and export routes fail closed when their scoped token is missing. Submit requests are still checked for spam-like payloads, duplicate recent submissions, and per-client rate limits before storage writes.

Submit feedback:

curl -X POST http://127.0.0.1:8787/v1/feedback \
  -H 'content-type: application/json' \
  -d '{
    "appId": "my-app",
    "message": "The billing screen should show the invoice PDF sooner.",
    "kind": "idea",
    "tags": ["billing"]
  }'

Useful endpoints:

  • GET /health

  • POST /v1/feedback

  • GET /v1/feedback?appId=my-app&limit=50

  • GET /v1/feedback/:id

  • PATCH /v1/feedback/:id with { "status": "triaged" }

  • GET /v1/stats

  • GET /v1/export.jsonl

SDK

import { createFeedbackClient } from "@hasna/feedback";

const feedback = createFeedbackClient({
  baseUrl: "http://127.0.0.1:8787",
  token: process.env.FEEDBACK_API_TOKEN,
});

await feedback.submit({
  appId: "my-app",
  message: "Export fails after selecting a date range.",
  kind: "bug",
  severity: "high",
  context: {
    route: "/reports",
    version: "2026.07.01",
  },
});

Browser apps can collect standard route/device context without a UI dependency:

import { collectBrowserFeedbackContext } from "@hasna/feedback/browser";

const context = collectBrowserFeedbackContext({
  version: import.meta.env.VITE_APP_VERSION,
  environment: import.meta.env.MODE,
});

For in-process server apps, use local storage directly:

import { LocalFeedbackStore } from "@hasna/feedback/storage";

const store = new LocalFeedbackStore();
await store.createFeedback({
  appId: "my-app",
  message: "Add CSV export.",
});

CLI

feedback init
feedback doctor
feedback submit "Add export history" --app my-app --kind idea --tag reports --route /reports --app-version 1.2.3 --env production
feedback list --app my-app --search export --since 2026-01-01 --limit 20
feedback show <id>
feedback status <id> triaged
feedback shipped <id> --changelog-ref open-todos@1.2.3
feedback sync-tasks
feedback stats
feedback export --format jsonl --until 2026-12-31

Use --api-url and --token to target a remote Open Feedback API instead of local JSONL storage, or set FEEDBACK_API_URL / FEEDBACK_API_TOKEN once so every command uses the shared deployment without retyping the flags. An explicit flag always beats the environment. The CLI does not open database connections or create cloud resources itself.

feedback shipped <id> --changelog-ref <ref> marks feedback as shipped, records the changelog-entry linkage (changelogRef, shippedAt), and emits the feedback.triaged notification event with disposition shipped. It works against both the local store and a remote API (--api-url/--token, or FEEDBACK_API_URL). feedback status <id> shipped also moves the status but records no changelogRef — prefer shipped so the link between a report and the thing that resolved it survives.

feedback doctor exits non-zero when it reports ok: false, so it can gate a health check or a loop.

Closing the loop: feedback → task → PR

Feedback is only useful if something picks it up. On the create path, Open Feedback files a task in a task tracker and records the link on the feedback item as taskRef:

feedback submit "Export button 500s for orgs over 10k members" --app my-app --kind bug --severity high
# -> stores the feedback AND creates a task titled
#    "[feedback:my-app] Export button 500s for orgs over 10k members"

The task body carries the feedback id, the reporter context, and the commands to read the original report and close it out, so an executor picking the task up has everything it needs.

This runs in-process rather than through an out-of-process event subscriber on purpose: channel configuration is machine-local state a fresh install does not inherit, so a wire that lives there is invisible when it is missing and silent when it fails.

variable

default

meaning

FEEDBACK_TASK_SINK

auto

auto (use todos when its CLI is on PATH, otherwise do nothing), todos, command, or none

FEEDBACK_TASK_PROJECT

project every task is filed under

FEEDBACK_TASK_PROJECT_MAP

JSON {"<appId>": "<project>"}, per-app routing; beats FEEDBACK_TASK_PROJECT

FEEDBACK_TASK_PRIORITY_MAP

severity→same name

JSON overriding the severity→priority mapping

FEEDBACK_TASK_TAGS

comma-separated extra tags

FEEDBACK_TASK_BIN

todos

task CLI name or path

FEEDBACK_TASK_TIMEOUT_MS

15000

how long task creation may block capture before it is killed and recorded as a failure

FEEDBACK_TASK_COMMAND

with FEEDBACK_TASK_SINK=command, the command to run; it receives {"feedback":…,"task":…} on stdin and must print JSON containing an id

auto is deliberately quiet: an install without a task CLI writes feedback and creates nothing, rather than failing every submit.

Capture is never held hostage by the tracker. The report is written to storage first, and task creation runs after it with a timeout — a tracker that is down, slow, or hung costs you a task, never a report. If filing fails, the error is recorded on the item as taskError (truncated to the schema bound), submit warns and exits non-zero, and feedback sync-tasks retries:

feedback sync-tasks   # -> {"sinkConfigured":true,"created":2,"failed":0,"skipped":11,"uncertain":0,"remaining":0,"errors":[]}

sync-tasks distinguishes two kinds of unlinked feedback, because they are not equally safe to retry:

  • a recorded taskError means the attempt is known to have failed, so it is retried automatically;

  • an attempt with no recorded outcome (a crash or timeout between "task created" and "link written") is reported as uncertain and skipped, because a task may already exist and re-filing would duplicate it. Use --retry-uncertain to force it after checking.

--limit reports what it did not get to as remaining, so a partial run never reads as a complete one.

Storage shape

The SQLite store keeps one row per feedback item, holding the full item as JSON alongside projected id, created_at, app_id, status, kind and severity columns. The JSON is the source of truth, which is what keeps exportJsonl byte-identical to the JSONL store's output and lets the item shape grow a field without a schema migration. Updates replace a row in a transaction, so there is no compaction step and reads scale with items rather than records.

The JSONL file is an append-only log with two kinds of record:

  • a full item — the whole feedback object, written once when it is submitted (and again when the log is compacted);

  • a linkage patch{"patch":"task","id":…,"taskRef":…}, carrying only the task fields, where null clears a field.

Reading folds the log by id: a full record replaces, a patch merges field by field. Task linkage is written as a patch rather than as a fresh snapshot of the whole item, and that distinction is load-bearing: the snapshot would be taken before task creation, so replaying it would resurrect the pre-task status and silently erase a shipped (and its changelogRef) that landed while the task was being created.

Linkage is never written by rewriting the file. Rewriting on the create path is O(n) under the data lock and, under concurrency, drops writes outright. feedback status and feedback shipped compact the log back to one full record per item.

A patch is small, but an untriaged store still carries roughly one extra record per item until something compacts it, and reads scale with records rather than items. That is fine at the scale this is built for; it is worth knowing before pointing it at a very large backlog.

Distribution events

Feedback stores emit feedback.created and feedback.triaged event envelopes (distribution event catalog, contract hasna.feedback.v1) through @hasna/events on the create/triage paths. Pass eventSink: null to LocalFeedbackStore to disable emission, or provide your own FeedbackEventSink. The default sink respects HASNA_EVENTS_DIR.

feedback doctor checks the package version, selected storage runtime, local data file path and permissions when local mode is active, the resolved task sink, the configured remote URL, token configuration, cloud configuration presence, and whether the expected binaries are on PATH. Diagnostics only report whether sensitive settings are configured; they do not print token, DSN, ARN, or secret values. It exits non-zero when ok is false.

Terminal Slash Commands

For terminal or agent slash-command style workflows, wire the command body to feedback submit and pass the current app slug:

# /feedback Add an activity filter to the inbox view
feedback submit "Add an activity filter to the inbox view" --app my-app --kind idea --tag slash-command

# /bug Export fails after picking a date range
feedback submit "Export fails after picking a date range" --app my-app --kind bug --severity high

The slash-command wrapper should provide --api-url and --token when feedback belongs in a shared deployment.

MCP

Run the MCP server:

feedback-mcp

Available tools:

  • submit_feedback

  • list_feedback

  • get_feedback

  • update_feedback_status

  • feedback_stats

  • export_feedback

  • feedback_diagnostics

Feedback submitted through the MCP server goes through the same store, so it creates a task and records taskRef exactly as the CLI does.

Storage

By default, Open Feedback stores feedback in a local SQLite database:

~/.hasna/feedback/feedback.db

Override the directory with HASNA_FEEDBACK_DATA_DIR, or name the database file outright with HASNA_FEEDBACK_SQLITE_PATH. Configuration is read from HASNA_FEEDBACK_* first and falls back to the historical unprefixed FEEDBACK_* names, so existing setups keep working.

Select the engine with HASNA_FEEDBACK_STORE:

value

backend

unset, sqlite, db

SQLite at ~/.hasna/feedback/feedback.db (default)

jsonl, file, local

the append-only JSONL log at ~/.hasna/feedback/feedback.jsonl

postgres, postgresql, cloud, rds

a host-injected FeedbackStore adapter

Migrating from feedback.jsonl

This happens automatically and needs no action. The first time a SQLite store opens, it imports any feedback.jsonl from the data directory (HASNA_FEEDBACK_DATA_DIR, default ~/.hasna/feedback) and records that it has done so, so the import runs once and cannot duplicate rows. Relocating only the database with HASNA_FEEDBACK_SQLITE_PATH still imports that log; a log sitting beside the database is picked up too, if the data directory has none.

The open that performs the import prints a one-line notice to stderr naming the source, the destination and the row count. Rolling back is safe but not lossless, and the moment that becomes true is the moment worth saying so — rather than only in this paragraph, which nobody is reading at the time.

The import is non-destructive: feedback.jsonl is never written, renamed or deleted. To roll back, set HASNA_FEEDBACK_STORE=jsonl — the original log is still there, unchanged, and still authoritative for that engine. Note that feedback captured under SQLite after the switch does not flow back into the JSONL log, so a rollback leaves behind anything recorded in between. There is deliberately no two-way sync: writing to both engines would restore the dual-write hazard this migration exists to end.

JSONL remains a first-class export format regardless of engine — feedback export --format jsonl and GET /v1/export.jsonl produce byte-identical output on either backend.

Set HASNA_FEEDBACK_STORE=postgres only in a host runtime that injects a FeedbackStore adapter:

import { createFeedbackHandler, type FeedbackStore } from "@hasna/feedback";

const cloudStore: FeedbackStore = createYourFeedbackStoreAdapter();
const handler = createFeedbackHandler({
  store: cloudStore,
  apiToken: process.env.FEEDBACK_API_TOKEN,
});

@hasna/feedback does not create databases, run migrations, provision AWS/RDS resources, create secrets, or send notifications. Without an injected adapter, cloud mode fails closed with a clear diagnostic blocker. Optional readiness settings such as FEEDBACK_CLOUD_PROVIDER, FEEDBACK_CLOUD_DATABASE_URL, FEEDBACK_CLOUD_RESOURCE_ARN, FEEDBACK_CLOUD_SECRET_ARN, and FEEDBACK_CLOUD_TABLE are reported as configured/not configured only.

App Integration

See docs/app-integration.md for browser, server, CLI, and MCP integration examples.

Development

bun install
bun run typecheck
bun test
bun run build

Security

Open Feedback redacts common credential patterns and sensitive metadata keys before storing feedback. Treat feedback exports as potentially sensitive product data. Do not commit feedback JSONL files or API tokens.

A
license - permissive license
-
quality - not tested
A
maintenance

Maintenance

Maintainers
Response time
0dRelease cycle
2Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • A
    license
    B
    quality
    D
    maintenance
    A modern Model Context Protocol (MCP) server that enables AI assistants to collect interactive user feedback, supporting text and image-based responses.
    3
    MIT
  • A
    license
    A
    quality
    C
    maintenance
    A lightweight MCP server that enables AI assistants to collect interactive user feedback via a browser window with full Markdown rendering and syntax highlighting.
    1
    15
    3
    MIT
  • A
    license
    A
    quality
    C
    maintenance
    A Model Context Protocol server for FeaturePulse feedback management, enabling AI assistants to query feature requests, analyze MRR impact, and manage product roadmaps through natural language.
    6
    7
    MIT

View all related MCP servers

Related MCP Connectors

  • A comprehensive Model Context Protocol (MCP) server that enables AI assistants to interact with yo…

  • MCP server for AI agents to plan, verify, and deploy Cloudflare-native apps.

  • MCP server exposing the Backtest360 engine API as tools for AI agents.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/hasna/feedback'

If you have feedback or need assistance with the MCP directory API, please join our Discord server