DEV Community

LunarDrift
LunarDrift

Posted on

Build a Feedback Channel That Does Not Become Another Abandoned Inbox

A feedback channel is easy to launch: add an email address, form, or chat link.

The difficult part is operating it six months later.

Messages become abandoned when their destination is disconnected from the maintainers’ normal work. The solution is not necessarily a better inbox. It is a small routing system with explicit ownership, visible states, and a predictable triage schedule.

This tutorial builds that system for an open-source repository using:

  • a contact page that routes each request to the right channel;
  • GitHub Issue Forms for actionable bug reports;
  • a support policy with response expectations;
  • labels that represent workflow state;
  • a scheduled GitHub Action that detects overdue triage.

The same design works for internal tools, small SaaS products, libraries, and community projects.

Start with routing, not a generic form

A single “Send feedback” box mixes requests with very different handling requirements:

  • reproducible bugs;
  • usage questions;
  • feature ideas;
  • security reports;
  • account or billing details;
  • partnership and private messages.

Once these enter the same queue, maintainers must classify every message before doing useful work. Sensitive information can also land in public places by mistake.

Instead, make the first page a router:

Request Destination Why
Bug Issue Form Structured, searchable, and tied to development
Usage question Discussion forum Other users can answer and find it later
Feature idea Discussion or issue template Supports clarification before implementation
Vulnerability Private security channel Avoids public disclosure
Private matter Maintainer contact page Keeps personal details out of public issues

A feedback channel is healthy when every route has an owner and a review schedule. The specific tools matter less than that operating contract.

1. Publish the support contract

Add SUPPORT.md at the repository root:

# Support and feedback

Choose the route that best matches your request:

- **Bug:** Open a bug report.
- **Usage question:** Start a Q&A discussion.
- **Feature idea:** Start an Ideas discussion.
- **Security vulnerability:** Follow SECURITY.md. Do not open a public issue.
- **Private matter:** Use the maintainer contact page.

## What to expect

New bug reports are normally triaged within three working days.
Triage means that a maintainer has classified the report; it does not
promise that the bug will be fixed within that period.

Community support is best-effort. Questions without a reproducible
example may be closed with a request for more information.

## Scope

This project can help with defects and documented usage. It cannot
provide private application debugging or guaranteed implementation dates.
Enter fullscreen mode Exit fullscreen mode

Use expectations that your maintainers can realistically honor. If there is no guaranteed response time, say “best-effort” rather than implying an SLA.

The distinction between triaged and resolved is important. A maintainer can acknowledge and classify a report without promising an immediate fix.

2. Put the routes near the point of need

Do not hide support information in the repository footer. Add a short section to README.md, near installation or usage instructions:

## Help and feedback

- Found a reproducible bug? [Open a bug report](../../issues/new?template=bug.yml)
- Need help using the project? [Start a Q&A discussion](../../discussions/categories/q-a)
- Have a feature idea? [Start an Ideas discussion](../../discussions/categories/ideas)
- Need to report a vulnerability? Read [SECURITY.md](SECURITY.md)
- Need to contact the maintainers privately? [Open the contact page](CONTACT_PAGE_URL)

Please read [SUPPORT.md](SUPPORT.md) for scope and response expectations.
Enter fullscreen mode Exit fullscreen mode

Replace CONTACT_PAGE_URL with the private route you operate. If you do not have one, omit that line rather than publishing an inbox nobody checks.

Repository-relative GitHub links should be tested from your default branch. Depending on where the README is rendered, you may prefer complete repository URLs.

3. Collect enough information to act

Create .github/ISSUE_TEMPLATE/bug.yml:

name: Bug report
description: Report a reproducible defect
title: "[Bug]: "
labels:
  - bug
  - needs-triage
body:
  - type: markdown
    attributes:
      value: |
        Thanks for reporting a bug. Do not include passwords, tokens,
        personal data, or unannounced security vulnerabilities.

  - type: textarea
    id: summary
    attributes:
      label: What happened?
      description: Describe the observed behavior and what you expected instead.
      placeholder: I expected ... but instead ...
    validations:
      required: true

  - type: textarea
    id: reproduction
    attributes:
      label: Minimal reproduction
      description: Provide the smallest example or repository that demonstrates the problem.
      render: shell
    validations:
      required: true

  - type: input
    id: version
    attributes:
      label: Project version
      placeholder: 2.4.1
    validations:
      required: true

  - type: input
    id: environment
    attributes:
      label: Environment
      placeholder: Node.js 22, Ubuntu 24.04, Firefox 128
    validations:
      required: true

  - type: textarea
    id: logs
    attributes:
      label: Relevant logs
      description: Remove secrets and personal information before submitting.
      render: text

  - type: checkboxes
    id: checks
    attributes:
      label: Submission checks
      options:
        - label: I searched existing issues for duplicates.
          required: true
        - label: I removed secrets and personal information.
          required: true
        - label: This is not a security vulnerability.
          required: true
Enter fullscreen mode Exit fullscreen mode

Then configure the issue chooser in .github/ISSUE_TEMPLATE/config.yml:

blank_issues_enabled: false
contact_links:
  - name: Usage questions
    url: https://github.com/OWNER/REPOSITORY/discussions/categories/q-a
    about: Ask the community for help using the project.

  - name: Security reports
    url: https://github.com/OWNER/REPOSITORY/security/advisories/new
    about: Privately report a suspected vulnerability.
Enter fullscreen mode Exit fullscreen mode

Replace OWNER and REPOSITORY. Also confirm that Discussions and private vulnerability reporting are enabled for the repository before publishing those routes.

Forms should request information that changes the next action. A field such as “How did this make you feel?” may be useful for product research, but it probably does not help reproduce a parser crash. Every extra required field increases the effort needed to report a defect.

4. Define a small state machine

Labels should describe what happens next, not merely restate the issue title.

A minimal workflow is:

needs-triage
    ├──> needs-info
    ├──> accepted
    ├──> duplicate
    ├──> not-planned
    └──> invalid
Enter fullscreen mode Exit fullscreen mode

Recommended meanings:

  • needs-triage: no maintainer has classified the issue;
  • needs-info: the reporter must provide something specific;
  • accepted: the problem or request is understood, but not necessarily scheduled;
  • duplicate: discussion continues on another issue;
  • not-planned: the project does not intend to act on it;
  • invalid: the report is outside scope or cannot be interpreted safely.

During triage, remove needs-triage and add exactly one outcome label. If requesting more information, state what is missing and when the issue may be closed.

Example response:

Thanks for the report. I can reproduce the behavior on version 3.1.0,
so I have marked this as `accepted`. This classification does not assign
a release date; implementation will be tracked in this issue.
Enter fullscreen mode Exit fullscreen mode

Clear rejection is usually more useful than indefinite silence. A polite not-planned decision lets contributors stop waiting or propose a different approach.

5. Detect overdue triage automatically

Automation cannot perform thoughtful triage, but it can detect when the process has stopped.

Create .github/workflows/check-feedback-triage.yml:

name: Check feedback triage

on:
  schedule:
    - cron: "17 8 * * 1-5"
  workflow_dispatch:

permissions:
  issues: read

jobs:
  overdue:
    runs-on: ubuntu-latest
    steps:
      - name: Find issues awaiting triage for more than three days
        uses: actions/github-script@v7
        with:
          script: |
            const maxAgeMs = 3 * 24 * 60 * 60 * 1000;
            const cutoff = Date.now() - maxAgeMs;

            const issues = await github.paginate(
              github.rest.issues.listForRepo,
              {
                owner: context.repo.owner,
                repo: context.repo.repo,
                state: "open",
                labels: "needs-triage",
                per_page: 100
              }
            );

            const overdue = issues.filter(issue => {
              // The issues endpoint may include pull requests.
              if (issue.pull_request) return false;
              return new Date(issue.created_at).getTime() < cutoff;
            });

            if (overdue.length === 0) {
              core.info("No overdue feedback found.");
              return;
            }

            const summary = overdue
              .map(issue => `- #${issue.number}: ${issue.title} (${issue.html_url})`)
              .join("\n");

            await core.summary
              .addHeading("Feedback awaiting triage")
              .addRaw(summary)
              .write();

            core.setFailed(
              `${overdue.length} issue(s) have awaited triage for more than three days.`
            );
Enter fullscreen mode Exit fullscreen mode

The unusual minute in the schedule avoids placing the job exactly on the hour, when scheduled CI systems may be busy.

A failing workflow only helps if somebody receives and acts on its notifications. Assign one maintainer as the weekly triage owner, or rotate ownership in the same calendar used for releases and on-call work.

Also remember that the script measures calendar days. If your policy promises working days, either increase the threshold or implement business-day calculation. The automation should reflect the published contract rather than quietly enforcing a different one.

6. Create a lightweight triage routine

A practical review session can be short:

  1. Open all items labeled needs-triage.
  2. Check for secrets or sensitive data first.
  3. Merge duplicates into one canonical issue.
  4. Attempt reproduction when appropriate.
  5. Apply an outcome label and remove needs-triage.
  6. Record one explicit next action.
  7. Review needs-info items whose waiting period has expired.

For multiple maintainers, document the current owner in a pinned issue or team calendar. “Everyone owns the inbox” often means nobody knows who should open it today.

Optional implementation: a hosted contact page

One implementation for the private route is Knocket, a contact layer that provides a shareable contact page and does not require visitors to create an account before starting a chat.

After creating a Knocket contact page, place its generated URL into the same routing structure rather than presenting it as the destination for every request:

## Help and feedback

- Bugs: [open a structured issue](../../issues/new?template=bug.yml)
- Questions: [start a discussion](../../discussions/categories/q-a)
- Security: read [SECURITY.md](SECURITY.md)
- Private or maintainer-specific matters: [contact the maintainers](YOUR_GENERATED_CONTACT_PAGE_URL)
Enter fullscreen mode Exit fullscreen mode

The operational rule remains unchanged: name an owner, decide when the private channel is reviewed, and redirect technical reports into public issues when doing so is safe and the sender agrees.

Failure modes to plan for

The contact page becomes the default for bugs

Private bug reports cannot be searched or deduplicated by the community. Label the route as “private matters,” while making the issue form the prominent bug-reporting option.

Every message is treated as urgent

Without severity definitions, notification fatigue eventually turns into ignored notifications. Reserve urgent handling for cases such as active security incidents or major service unavailability, and document who can declare that state.

Automation closes reports too aggressively

A stale bot can remove valid reports simply because maintainers were unavailable. Start with reminders and visibility. Add automatic closure only after publishing the waiting period and providing a clear way to reopen the discussion.

Required fields encourage fabricated answers

Reporters may enter placeholders when forced to provide details they do not have. Make a field required only when the report cannot be evaluated without it.

The private route has no continuity

A contact address tied to one person can disappear when responsibilities change. Use a project-controlled destination where possible, and document how ownership is transferred.

The policy promises more than the team can sustain

A three-day promise is harmful if the repository is reviewed monthly. Publish the actual cadence, even if it is “reviewed on the first Monday of each month.” Predictability is more valuable than an ambitious promise that is routinely broken.

A channel is a process, not a URL

Before adding any new contact method, answer five questions:

  1. What requests belong here?
  2. Who reviews them?
  3. When are they reviewed?
  4. What states can a request enter?
  5. How will the team notice when triage stops?

If those answers are missing, another form creates another place to lose messages. If they are explicit, even a simple README link and issue tracker can become a dependable feedback system.


Disclosure: I work on Knocket, so treat it as one implementation example rather than a neutral recommendation.

Top comments (0)