<- All posts

How to Build a Feature Request Management Tool in 5 Steps

Ronan McQuillan
20 min read Feb 18, 2025

Feature request management tools are some of the most common workflow apps for both internal and customer-facing use cases.

However, the specific information you need to gather and the process for approving requests will vary from one team to the next - or even between individual projects.

So, this is also one of the most requested internal tools.

Today, we鈥檙e checking out how to build a custom solution with 黑料正能量. By the end, you鈥檒l have a fully-functioning app that you can modify with your own data model or approval workflows.

First, though, let鈥檚 get a little bit more context.

What is a feature request management tool?

At its core, a feature request management tool is what鈥檚 known as an approval app. As the name suggests, this enables one set of users - typically the end-users of an existing solution - to ask the developers to add functionality that they need.

In an internal setting, these will generally be colleagues in non-IT departments. The goal is to gather the information our development team needs to make a decision about the requested feature in a consistent format.

A separate type of user then follows a defined process to either approve or reject incoming requests. This is usually a product owner or manager, although it could also be a senior developer, depending on the specific organization and project.

This achieves two things. Firstly, it empowers end-users to shape the tools that they use by giving feedback or suggestions to developers. Second, it creates a clear record of when features are requested and how our team responds to them.

What are we building?

We鈥檙e building a simple feature request management tool on top of an existing PostgreSQL database - although, with 黑料正能量, we could use just about any database. This is made up of two tables - one for incoming requests and one for our planned features.

We鈥檝e created a lightweight, adaptable workflow based on two distinct user roles. Here鈥檚 what each one can do:

  • Requestors can create feature requests, edit their own submissions, and view other open requests. They can also view all existing planned features.
  • Product Owners inherit all permissions from Requestors. They can also accept or reject requests or update planned features.

Each set of users has their own set of screens for interacting with our data.

When a feature request is accepted, it鈥檚 automatically added to the planned features table. We鈥檒l build this logic out using 黑料正能量鈥檚 Automation section.

Here鈥檚 what our app will look like when we鈥檙e finished.

We鈥檙e using 黑料正能量 Cloud, although you could also self-host your feature request tool.

Along the way, we鈥檒l also provide all of the queries you need to build a lookalike database so you can build along. We鈥檙e storing our Postgres database in the free version of NeonDB, so we don鈥檛 have to worry about hosting that either.

Check out our other tutorial on building a SQL GUI .

How to build a feature request management tool in 5 steps

If you haven鈥檛 already, sign up for a free 黑料正能量 account to start building as many internal tools as you need for free.

Join 200,000 teams building workflow apps with 黑料正能量

1. Setting up our data model

The first thing we need to do is create a new 黑料正能量 application. We have the option of starting with a template or importing an existing app dump, but today, we鈥檙e starting from scratch.

When we choose this option, we鈥檒l be prompted to give our new app a name, which is also used to generate a URL slug. We鈥檒l simply call ours Feature Request Management Tool.

Feature Request Management Tool

Connecting to Postgres

Next, we need to select a data source for our app. 黑料正能量 offers dedicated connectors for all kinds of RDBMS, NoSQL tools, APIs, spreadsheet tools, and more - alongside our internal low-code database.

Data

As we said earlier, we鈥檙e going to choose Postgres. When we select this option, we鈥檒l be presented with a modal form where we can input our connection details.

Config

Once we鈥檝e done this, we鈥檙e asked which of our database tables we鈥檇 like to Fetch, making them queryable within 黑料正能量. Our database contains two tables called feature_requests and feature_tracking.

We鈥檙e selecting both.

Tables

If you want to create a lookalike database, you can use the following queries to create and populate these tables. First, feature_requests:

 1-- Table: feature_requests
 2
 3CREATE TABLE feature_requests (
 4
 5  id SERIAL PRIMARY KEY,
 6
 7  title TEXT NOT NULL,
 8
 9  description TEXT NOT NULL,
10
11  status TEXT,
12
13  created_at TIMESTAMP,
14
15  updated_at TIMESTAMP
16
17);
18
19-- Insert sample feature requests
20
21INSERT INTO feature_requests (title, description, status, created_at, updated_at)
22
23VALUES 
24
25  ('Dark Mode UI', 'Users requested a dark theme for better accessibility.', 'New', NOW(), NOW()),
26
27  ('API Rate Limit Increase', 'Developers need higher rate limits for integrations.', 'Under Review', NOW(), NOW()),
28
29  ('Mobile App Enhancements', 'Improve mobile UI and performance.', 'Accepted', NOW(), NOW()),
30
31  ('Custom Webhooks', 'Allow users to create custom event-based webhooks.', 'Accepted', NOW(), NOW()),
32
33  ('AI-Powered Search', 'Enhance search with AI for better results.', 'completed', NOW(), NOW());

Then, feature_tracking:

 1-- Table: feature_tracking
 2
 3CREATE TABLE feature_tracking (
 4
 5  id SERIAL PRIMARY KEY,
 6
 7  feature_request_id INT NOT NULL REFERENCES feature_requests(id) ON DELETE CASCADE,
 8
 9  status TEXT,
10
11  priority TEXT,
12
13  estimated_completion DATE,
14
15  updated_at TIMESTAMP
16
17);
18
19-- Insert sample feature tracking data (for planned/in-progress requests)
20
21INSERT INTO feature_tracking (feature_request_id, status, priority, estimated_completion, updated_at)
22
23VALUES 
24
25  (3, 'Planned', 'High', '2024-04-15', NOW()),
26
27  (4, 'In Progress', 'Critical', '2024-03-20', NOW()),
28
29  (5, 'Released', 'High', NULL, NOW());

Here鈥檚 how our tables look once we鈥檝e fetched them.

Data

Tweaking our database

We could start using this as-is, but we鈥檙e going to make a few minor tweaks to how some of our columns are handled in 黑料正能量 to make our life a little easier when we generate UIs later.

Firstly, our Postgres database has several TEXT attributes, but 黑料正能量 has two different data types for handling text.

When we autogenerate our feature request form, we鈥檙e going to want to give users more space to enter a description, so we鈥檙e updating the Type of the description field in the feature_requests table from Text to Long Form Text.

Options

This won鈥檛 alter the underlying database - only how 黑料正能量 handles the description field.

There are also several TEXT fields in our database where we only want end users to choose from a defined set of options. These are status in the feature_requests table, and status and priority in the feature_tracking table.

First, we鈥檒l update their type from Text to Single Select. When we do this, we can define our options. For the feature_requests status column, these will be New, Under Review, Accepted, and Rejected.

Options

We鈥檒l repeat this process for the two feature_tracking columns we mentioned a second ago. This time, for status, we鈥檒l use Planned, In Progress, and Released. For priority, our options will be Low, Medium, High, and Critical.

Adding default values

Within our data model, there are certain columns that we won鈥檛 always need users to add values for manually. We鈥檙e going to handle these with 黑料正能量鈥檚 default values. This allows us to set a value for columns automatically - although users can still overwrite it.

These come in two clusters. First, for the status attributes in each of our tables, new rows will always start with the same value to reflect the initial point of our feature request workflow.

On the feature_tracking table, we鈥檒l set our default status to Planned.

Planned

We鈥檒l then follow the exact same process to default our feature_requests status to New.

The second group of values we鈥檒l add a default value for are dates. These are the updated_at columns in each of our tables. We want these to automatically be set to the current date and time.

To do this, we鈥檒l hit the lightning bolt icon to open the bindings menu and use {{ Date }} as a handlebars expression for our default value.

Bindings

Remember to repeat this process for the updated_at attribute on both tables.

Setting up relationships

Our tables already have the primary and foreign key attributes we need to set up relationships. However, to utilize this, we need to configure a relationship between them in 黑料正能量.

We鈥檒l start by hitting Define Relationship.

Define Relationships

This will open the following dialog.

Dialog

We鈥檙e going to set this so that many rows in feature_requests can be related to one row in feature_tracking. For our purposes today, we only really need a one-to-one relationship, but this approach will be more scalable if we want to bundle requests together in the future.

Our primary key is id, and our foreign key is feature_request_id.

Keys

Now, in each table, we can see the corresponding row in the other.

Columns

We also want to link each of our tables to 黑料正能量鈥檚 internal Users table. However, this works a little bit differently than linking two tables in an external database.

To start, we鈥檒l add a new column called requestor to our feature_requests table. For the data type, we鈥檒l select Single User. We鈥檙e also selecting the option to default to current user.

img

We鈥檒l also assign ourselves to a couple of rows for testing purposes later.

Assign

We can repeat this process by adding a Single User column to the feature_tracking. We鈥檒l call this owner, remembering to default to the current user again.

Owner

Defining our user roles

That鈥檚 the core of our data model completed. But as we know, we have two separate groups of users who need to interact with this data differently. To reflect this fact, we need to start by setting up our user roles.

Staying within the Data section, head to Manage Roles.

RBAC editor

All 黑料正能量 apps ship with two default roles:

  • App User - which is any authenticated user.
  • App Admin - which has full admin access across our app.

We鈥檙e going to add two custom roles. The first one will be called Product Owner.

Roles

Our second role will be called Requestor.

Feature Request Management Tools

In our desired configuration, Product Owners will inherit all of the permissions we grant to Requestors, but they鈥檒l also have their own additional permissions on top of this.

To achieve this, we鈥檒l place Requestor to the left of Product Owner, and draw a line between them in our visual RBAC editor for inheritance.

Roles

Adding role-specific views

With our roles in place, we can begin granting permissions. In 黑料正能量, database Views allow us to grant read and write permissions at the level of either rows or columns.

Before we create any views, we want to grant Product Owners full CRUD permissions across both of our tables, but we don鈥檛 want to give this level of access to Requestors. So, we鈥檒l set the required access role for the underlying tables to Product Owner.

Again, make sure to repeat this process for both tables.

Permissions

For our feature_requests table, we want to grant Requestors permissions to do the following:

  • Create rows,
  • Read all rows regardless of who created them,
  • Edit certain attributes for rows that they have created.

To do this, we鈥檒l need to create two Views. Hit Create a View. We鈥檒l call our first one Requestor Feature Requests Read.

View

We can hit Columns to define which attributes this view enables users to read or write. We鈥檒l simply set all columns to read-only.

Read Only

Lastly, we鈥檒l set our access role to Requestor.

Role

Now Requestors can read all feature_requests, regardless of who submitted them.

Next, we鈥檒l add a second View and call it Requestor Feature Requests Write, again setting the access role to Requestor. This time, though, we鈥檒l start by hitting Filter once this is created.

Filter

Here, we want to create a filtering rule based on the `requestor attribute. Then, we鈥檒l use the lightning bolt to open the bindings menu for our value.

bindings

Under the Current User section, we鈥檒l bind this to the _id attribute from the Users table.

id

Now, we can only see the rows that we assigned ourselves to earlier.

Under Columns, we鈥檒l then hide the requestor attribute, leaving everything else writable.

Requestor

Now, Requestors can create rows or update ones they鈥檝e submitted previously.

Next, we鈥檒l head to feature_tracking and add a View called Requestor Feature Tracking, with its access role set to Requestor. We鈥檒l then hide the idcolumn and set everything else toread-only`.

Read Only

This time, however, we also want to add some columns from the corresponding feature_requests rows. Specifically, we鈥檒l also enable Requestors to view the title and description from the original request.

View Join

We鈥檒l also want Product Owners to see these attributes when they look at feature_tracking data, so we鈥檒l add one final view called Product Owner Feature Tracking.

This time, we鈥檒l leave everything writable but also add read permissions for those two extra columns.

View Join

2. Building a feature request form

With 黑料正能量, once we鈥檝e set up our data model, we can autogenerate customizable screens based on this. These will inherit the roles and permissions of whichever table or view they鈥檙e connected to.

The first screen we want to build is a simple feature request form. Within the Requestor Feature Request Write View, we鈥檒l hit Generate then Form.

Form

On the dialog that pops up, we鈥檒l select the option for a Create form.

Create

Here鈥檚 how this looks out of the box.

Feature Request Management Tool

We鈥檙e going to hide all fields except title and description, as well as updating our form鈥檚 heading.

Fields

Then, under Styles, we鈥檒l set the button position to top.

Button

Earlier, we created default values for our updated_at attribute so that it will be populated automatically. The feature_requests table also contains an attribute called created_at.

We also want to set a value for this when a row is created, we can configure our Save button to do this.

We鈥檒l start by opening the Actions drawer.

Actions

Under the existing Save Row action, we can manually set a value for our created_at attribute.

Created At

We鈥檒l bind this to the handlebars expression {{ date now "" }}, returning the current date in the default format.

Binding

3. Feature tracking for requestors

Next, we want to build screens to enable Requestors to interact with existing feature requests and planned features.

Open requests

We鈥檒l start by generating a Table UI from our Requestor Feature Requests Read View. This time, we鈥檒l select the option for modal edit forms.

Table

Here鈥檚 how this will look.

Columns

The first change we鈥檒l make is to remove the id and feature_tracking columns from our table to clean it up.

Columns

Next, instead of the default New Row form that comes with our table UI, we want to open our custom form from the last section in a modal screen. So, we can start by deleting this existing modal.

Under the Actions drawer for our button, we鈥檒l replace the existing Open Modal action with a new Navigate To one. We鈥檒l point this at our other screen and select the option to open it in a modal.

Button

Here鈥檚 how this will look.

Form

Lastly, we need to change our default edit row form.

This is a slightly more involved process, as we want to display different forms based on whether or not the clicked row was created by the current user.

Here鈥檚 how this looks, to begin with.

Form

We鈥檒l modify this to be used for editing rows the user has submitted themself.

We鈥檒l start by disabling all fields except title and description.

Fields

We鈥檒l then place these fields at the top and arrange the disabled ones into columns.

Lastly, we鈥檒l update our display text and move our buttons to the top, as before.

Columns

We only want this form to appear if a user clicks on a feature request that they submitted. Otherwise, we want to display a read-only version.

To achieve this, we鈥檒l start by duplicating our existing form. We鈥檒l also rename this duplicated version to keep things clear.

Duplicate

Then, we鈥檒l set the type to View, disable our remaining fields, and update our display text.

Disabled

The last thing we want to do is set up the logic for which version of our form to display. Again, this will be based on the requestor attribute from the row a user clicks on to open the modal.

Back on our table, we鈥檒l open the On Row Click actions drawer. Here, there are two existing actions called Update State and Open Modal.

Update State

We need to add a second Update State action between these. This accepts a key/value pair, which we can then use as a binding elsewhere in the 黑料正能量 builder.

We鈥檒l set our key to clickedRowRequestor.

Update State

Then, we鈥檒l open the bindings drawer and select requestor._id output from our clicked row.

Binding

Back to our modal, we can then use this state variable within conditionality rules on each of our forms.

Conditionality rules can be used to hide, display, or update any native setting of a component, based on any data it鈥檚 exposed to.

On our Edit form, we鈥檒l add a rule to Show Component if {{ State.clickedRowRequestor }} Equals {{ Current User.globalId }}.

Condition

Then, we鈥檒l add a corresponding rule to Hide our other form.

Hide

Now, when we preview our app, the rows we didn鈥檛 assign ourselves to earlier will display a read-only form.

Request Details

Planned features

The last screen we鈥檒l add for Requestors will enable them to search and view planned features. So, we鈥檒l start by autogenerating a table screen from our Requestor Feature Tracking View.

This time, though, we鈥檒l select the option for Side Panel Forms rather than modals.

Side Panel

Here鈥檚 how this looks.

Form

The main thing we need to do here is alter our Edit form to suit our purposes. We鈥檒l start by setting this to be a View form, as well as removing the owner and feature_requests attributes.

We鈥檝e also bound our form Title to the title attribute from the corresponding feature_requests row, using {{ Edit row form block.Requestor Feature Tracking.feature_requests.0.title }}.

View

Our form is also missing the description of the original feature request. So, we鈥檒l add a Long Form Text Field component beneath our existing Form Block and call this description.

Description

We鈥檒l set the Field and Label to Description and the Default Value to {{ Edit row form block.Requestor Feature Tracking.feature_requests.0.description }}.

Our Requestors don鈥檛 have write permissions for this table, so we can delete the New Row side panel and corresponding button.

Requestor

We also want to provide searchability on this screen, so we鈥檒l add a Text Field component above our table, with the Field and Placeholder set to Search.

Search

For this to work, we need to filter our table by the search field鈥檚 output.

We鈥檒l open the Filter menu and create a group with three expressions for if feature_requests equals, is like or starts with {{ Search Text Field.Value }}.

Filter

Now we can search for rows based on their title.

Search

4. Adding an approval flow for product managers

Now that we鈥檝e built all of our screens for Requestors, we can start building the corresponding UIs for Product Owners.

The goal is to enable them to respond to feature_requests and perform full CRUD actions on the feature_tracker table.

When they update a feature_requests 谤辞飞鈥檚 status to Accepted, we want a corresponding row to be created on the feature_tracker table.

We鈥檒l achieve this using 黑料正能量鈥檚 Automation builder.

Creating an automation rule

We鈥檒l start by heading to the Automation section. Since we don鈥檛 have any automations yet, this is what we鈥檒l see.

Automations

We鈥檒l hit create automation. We鈥檒l then be prompted to choose a name and a trigger for our new rule. We鈥檙e going to call ours Accept Request and select an App Action trigger.

App Action

The App Action trigger allows us to initiate our automation elsewhere in the builder when end-users take certain actions.

We鈥檙e going to set the role for our automation to Product Owner. We鈥檙e also adding four arguments called request_Id, estimatedDate, priority, and requestPrimaryKey. These are the fields we鈥檒l need to perform our automation.

Fields

We鈥檙e using the name requestPrimaryKey to keep things readable, as our Postgres database and 黑料正能量鈥檚 automatic data have similarly named id and _id attributes.

Let鈥檚 pause to recall what we want our automation to do. Firstly, we want to take data from a feature_requests row and use it to populate a new row on our feature_tracking table. Then, we want to mark the original feature_request as accepted.

So, we鈥檒l start by adding a Create Row action. We鈥檒l point this at the feature_tracking table.

Create Row

Recall that many of our columns have default values. We need to use our trigger fields to assign values to the ones that don鈥檛. So, we鈥檒l hit Add Fields and select estimated_completion, feature_requests, and priority.

Fields

We can then use the Trigger Fields bindings to assign values to each of these. For instance, {{ trigger.fields.estimatedDate }}.

ID

We鈥檒l copy the _id of one of our feature_requests to use as test data and hit Run Test.

ID

And then, we can verify that this worked.

Test Details

Next, we want to update the appropriate feature_request to mark it as accepted. To do this, we鈥檒l first need to add a Query Rows step, pointed at the feature_requests table.

Query Rows

We鈥檒l add a filter to this so that it only returns rows where id equals {{ trigger.fields.requestPrimaryKey }}.

ID

Lastly, we鈥檒l add an Update Row action beneath this, using {{ steps.Query rows.rows.0._id }} as our Row ID.

We鈥檒l then set status to Accepted and updated_at to the current timestamp.

Update Row

We鈥檒l then run our test again to ensure that this works as expected.

Test

CRUD actions for open requests

Next, we need to build a screen for Product Managers to handle incoming requests. We鈥檒l begin by generating a Table screen with modal forms from our underlying feature_requests table.

We鈥檝e started by tidying up our table columns and removing the components for creating a new row.

CRUD screen

Within our remaining form, we鈥檒l arrange our fields into columns, update our display text, and disable all fields except for status.

Feature Request Management Tool

On the status field, we鈥檙e going to open the Validation Rules drawer and set a rule that this can鈥檛 be equal to Accepted. We鈥檒l add the error message Request can't be manually accepted!

Validation Rule

We鈥檒l also remove our Delete button and add one that says Accept. We won鈥檛 set any actions for this just yet.

Form

Next, we鈥檒l add a Side Panel component beneath our Modal. We鈥檝e called this Accept Side Panel.

Side Panel

Inside this, we鈥檒l nest a Create form for our feature_tracking table, disabling all columns except estimated_completion and priority.

Create Form

We鈥檒l then open the Save button鈥檚 actions drawer, replacing the existing Save Row action with a Trigger Automation action.

We鈥檝e used the following bindings to populate our four trigger fields:

  • request_Id - {{ Edit row form block.feature_requests._id }}
  • estimatedDate - {{ Accept Form Block.Fields.estimated_completion }}
  • priority - {{ Accept Form Block.Fields.priority }}
  • requestPrimaryKey - {{ Edit row form block.feature_requests.id }}

Fields

Lastly, we鈥檒l head back to our feature_requests form, and add an Open Side Panel action to our Accept button.

Open SIde Panel

Since this is a live automation, we鈥檒l need to publish our app to test its functionality.

Live App

CRUD actions for planned features

Lastly, we鈥檒l add a CRUD screen for our feature_tracking table. Again, we鈥檒l start with a Table UI with Modal forms, this time with our Product Owner Feature Tracking View. Again, we鈥檝e removed the components for creating new rows and tidied up our table.

CRUD

We鈥檒l follow the same steps as earlier to add a description field to our remaining form, this time placing it in a container to center it.

Description Field

We鈥檒l also add search functionality to our table, exactly as we did earlier.

Search

5. Design tweaks, navigation & publishing

Functionally speaking, our feature request management tool is ready to go. However, we need to make a few UX improvements before we ship it to users.

Firstly, our navigation bar has gotten quite messy. Each class of users only needs to access two screens - one for feature_requests and one for feature_tracking.

We鈥檙e going to start by changing the URLs for our table screens so that each group of users can access one screen called /feature-requests and one called /feature-tracking.

Where there are two screens with the same URL, users will be directed to the version that matches their role.

We鈥檝e also updated the title on each screen to match.

URLS

Now, under Navigation, we鈥檒l set this to include only these two options.

Nav

We can also go through our app and update the labels and display texts for our form fields and table columns to be more reader-friendly, rather than using the raw column names from our database.

img

Lastly, under Screen and Theme, we鈥檝e chosen Midnight. We鈥檝e also updated our colors to better match the 黑料正能量 brand.

Feature Request Management Tool

When we鈥檙e happy, we can hit Publish to push our app live.

Here鈥檚 a reminder of what our feature request management tool looks like.

Turn data into action with 黑料正能量

黑料正能量 is the open-source, low-code platform that empowers IT teams to turn data into action. With leading external data support, autogenerated UIs, powerful automations, free SSO, and optional self-hosting, it鈥檚 the fast, easy way to ship secure internal tools.

Check out our features overview to learn more.