# Collect feature requests you can count, rank and answer

> A feature request form that asks for the problem instead of the solution, stores requests by theme, counts people not rows, and emails them when it ships.

Published 2026-10-25 · https://ikiform.com/guides/feedback-form/feature-request

Ask what the person is trying to do and how they do it today, not which feature they want, and require an email so you can tell them when it ships. Store each request in a table by webhook, tag requests into themes, and rank themes by how many different people are blocked. When a theme ships, email everyone who asked.

## What's different about feature requests

A bug report is done when it's fixed. A feature request is a data point: one of many people describing the same gap in different words. Three things follow from that:

- **People describe solutions.** "Add a CSV button" might mean "my accountant needs last month's invoices". Ask for the job, and you can pick a better fix than the one requested.
- **The same need arrives many times.** Ikiform stores every post and doesn't merge repeats, and two people rarely use the same words anyway. You group requests yourself, by theme, and count distinct people.
- **The value is in the follow-up.** Telling someone "you asked, we built it" is the best reason to ask at all. That needs an email on every request, so this form requires one.

Diagram: How a feature request becomes a counted theme and a launch email

1. Someone describes what they're trying to do, picks an area and an impact, and leaves an email.
2. A webhook relay inserts the request into your database, once per submission id.
3. Each week you tag new requests with a theme, by hand or with a pattern.
4. If it fits an existing theme, that theme now has one more person behind it.
5. If not, it starts a new theme.
6. When a theme ships, you select every email in it and send one message.

The ranking is people per theme, not raw requests. One customer asking five times is still one customer.

## Ask for the problem

Form: Feature request

- **What are you trying to do?** (`problem`, textarea, required)
- **How do you do it today?** (`workaround`, textarea)
- **Which part of the product?** (`area`, select, required)
- **How much does it matter?** (`impact`, radio, required)
- **Email** (`email`, email, required)

`problem` has a 30-character minimum, which turns "dark mode pls" into a sentence with a reason in it.

| Field | Why it's there |
| --- | --- |
| `problem` | The job to be done, in their words. The most useful field by far |
| `workaround` | Tells you how painful it is. "I copy it into a spreadsheet by hand" is a strong signal |
| `area` | A coarse first sort, so you tag within billing, not across everything |
| `impact` | Separates blocked customers from curious ones, phrased as outcomes people can judge |
| `email` | Required. Without it you can't close the loop or ask a question |

Leave out a priority or vote field. Everyone marks their own request as high priority, and the count of people per theme is a better vote than any button.

## The form

The form needs no script: it posts and redirects. Set **Redirect after submit** to a page that says what happens next. For a version that stays on the page in your framework, take the pillar's [form code](https://ikiform.com/guides/feedback-form.md#form) and swap in these fields.

`feature-request.html`:

```html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <label for="problem">What are you trying to do?</label>
  <textarea id="problem" name="problem" rows="5" required minlength="30"
    placeholder="Send last month's invoices to our accountant in one go"></textarea>
  <label for="workaround">How do you do it today?</label>
  <textarea id="workaround" name="workaround" rows="3"></textarea>
  <label for="area">Which part of the product?</label>
  <select id="area" name="area" required>
    <option value="billing">Billing and invoices</option>
    <option value="reports">Reports</option>
    <option value="team">Team and permissions</option>
    <option value="integrations">Integrations and API</option>
    <option value="other">Something else</option>
  </select>
  <fieldset>
    <legend>How much does it matter?</legend>
    <label><input type="radio" name="impact" value="blocking" required /> It stops us using the product for something</label>
    <label><input type="radio" name="impact" value="slow" /> It costs us time every week</label>
    <label><input type="radio" name="impact" value="nice" /> It would be nice</label>
  </fieldset>
  <label for="email">Email</label>
  <input id="email" name="email" type="email" required />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Send request</button>
</form>
```

## Store requests by webhook

The dashboard and CSV export work for the first few dozen requests. Once you're tagging themes and counting people every week, a table is easier. Create one in Postgres:

`schema.sql`:

```text
create table feature_requests (
  submission_id text primary key,
  area text not null,
  impact text not null,
  problem text not null,
  workaround text,
  email text not null,
  theme text,
  created_at timestamptz not null
);
```

Then add a [webhook](https://ikiform.com/docs/webhooks.md) endpoint in Ikiform that points at this route. It runs on any Node host with the `pg` package, and [your own server](https://ikiform.com/blog/form-submissions-to-your-own-server.md) covers the same pattern in more depth:

`app/api/feature-requests/route.ts`:

```ts
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";
import { Pool } from "pg";

const pool = new Pool({ connectionString: process.env.DATABASE_URL });

const INSERT = `insert into feature_requests (submission_id, area, impact, problem, workaround, email, created_at)
values ($1, $2, $3, $4, $5, lower($6), $7)
on conflict (submission_id) do nothing`;

export async function POST(request: Request) {
  const payload = await request.text();
  let event;
  try {
    event = await verifyWebhook({
      payload,
      headers: request.headers,
      secret: process.env.IKIFORM_WEBHOOK_SECRET!,
    });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
    throw error;
  }
  if (event.type !== "submission.created") return new Response(null, { status: 204 });

  const { problem, workaround, area, impact, email } = event.data.fields;
  await pool.query(INSERT, [
    event.data.submission_id,
    area,
    impact,
    problem,
    workaround ?? null,
    email,
    event.data.created_at,
  ]);
  return new Response(null, { status: 204 });
}
```

- `verifyWebhook` rejects anything not signed with your endpoint's `whsec_` secret.
- `on conflict (submission_id) do nothing` makes retries safe. Ikiform resends a failed delivery with the same submission, and the second insert is a no-op.
- A database error throws, the route answers `500`, and Ikiform retries later. You don't lose requests during a database restart.
- The email is lowercased on the way in, so counting distinct people works.

> Ikiform doesn't merge or reject repeat submissions by email. An identical repeat within 24 hours only adds to the spam score. Deduplicate where the list lives, or when you export.

## Tag and rank

### Tag new requests

Once a week, read the untagged rows and give each a theme. Patterns handle the obvious ones; read the rest yourself, because the interesting requests don't match a pattern.

`tag.sql`:

```text
update feature_requests
set theme = 'invoice-export'
where theme is null
  and area = 'billing'
  and problem ilike any (array['%export%', '%accountant%', '%download%']);
```

### Rank themes

This ranks themes by how many requests say they're blocked, then by how many different people asked:

`ranking.sql`:

```text
select coalesce(theme, 'untagged') as theme,
       count(*) as requests,
       count(distinct email) as people,
       count(*) filter (where impact = 'blocking') as blocking
from feature_requests
group by 1
order by blocking desc, people desc;
```

1. Read the top three themes' `problem` texts in full before you design anything.
2. Look at the workarounds. Often one small change removes the worst of it.
3. Weigh the ranking against your plan; it shows demand, not what's right to build.

## Close the loop

When a theme ships, pull everyone who asked for it:

`notify.sql`:

```text
select distinct email
from feature_requests
where theme = 'invoice-export';
```

Send one short email from your email tool or your own inbox, saying what shipped and linking to it. Ikiform doesn't email the person who fills in the form. Notification emails go to you and your team only, so there is no autoresponder, confirmation email or double opt-in. Send those from your email tool, triggered by a [webhook](https://ikiform.com/docs/webhooks.md). The same applies here: the launch email comes from you, not from Ikiform.

## Check it works

1. Submit a request with a 10-character problem. Ikiform answers `422` naming `problem`.
2. Submit a real request. It appears under **Submissions** and as a row in `feature_requests`.
3. Under **Webhooks**, resend the delivery. The table still has one row for that submission.
4. Submit twice with the same email in different case. The ranking query counts one person.

## Gotchas

- A public feedback board, such as Canny, adds voting and a public status page. If you want customers to see each other's requests, that's the better tool; this setup keeps requests private.
- Requests that include several links can score as spam. Check the **Spam** filter weekly while volume is low.
- If you add an `area` option later, add it to the definition first. A value that isn't listed is refused.
- Store what you need and no more. A requests table with emails is personal data; say so in your privacy notice and delete rows you no longer need.

## Troubleshooting

| Problem | What to do |
| --- | --- |
| Rows missing from the table | Open the endpoint under **Webhooks**. A `400` means the secret is wrong; a `500` with a database error means the insert failed and Ikiform is retrying. |
| `422 validation_failed` | The post doesn't match the form's field definition. `error.fields` names each field and the reason. Match the input `name` to the field id. |
| The submission is in the dashboard but no email or webhook arrived | It was stored as spam. Spam sends nothing. Open it under the **Spam** filter to see the reasons, and mark it as not spam to send its email and webhooks. |

## Questions

### What should a feature request form ask?

What the person is trying to do, how they do it today, which part of the product it touches, how much it matters, and an email. Asking for the problem rather than the feature gives you room to find a better fix.

### How do I handle duplicate feature requests?

Expect them and count people, not rows. Tag each request with a theme and rank themes by distinct emails. Ikiform stores every submission and doesn't merge repeats.

### Can users vote on feature requests?

Not with a form alone. A form keeps requests private; a public board such as Canny adds voting. Counting distinct people per theme gives you a private equivalent.

### How do I tell people when their request ships?

Keep the email with each request, select every email in the shipped theme, and send one message from your email tool. Ikiform doesn't email submitters itself.

## More guides

- [Feedback form for a website or app](https://ikiform.com/guides/feedback-form.md): Put a feedback form on any page or inside your app: a rating, a comment, the page it came from, spam filtering, and alerts when a score is low.
- [Bug report form](https://ikiform.com/guides/feedback-form/bug-report.md): A bug report form that asks people for steps and severity, and lets the page add the URL, browser, viewport and recent errors on its own.
- [Beta tester feedback](https://ikiform.com/guides/feedback-form/beta-tester-feedback.md): A beta feedback form with a personal link per tester and build, two rating scales and a weekly summary that shows whether each build got better.
- [NPS survey](https://ikiform.com/guides/feedback-form/nps-survey.md): Run an NPS survey with a 0 to 10 score in the email, a follow-up question on your own page that changes with the score, and a script that calculates NPS.
- [React feedback widget](https://ikiform.com/guides/feedback-form/react-feedback-widget.md): A floating feedback button for a React app that opens a small panel, sends the screen, app version and account with each answer, and never loses a draft.
- [Cancellation survey](https://ikiform.com/guides/feedback-form/cancellation-survey.md): Ask why customers cancel inside your SaaS cancel flow, keep Skip one click away, save the reason to Stripe and Ikiform, and never delay the cancel.
