Build A Collaborative Roadmap With Rowy (2023)

No-code / low-code enables builders to be more productive at work. It's also a way to develop new income sources for a select few, but you'll need to build something people want. And to do that, you'll need a product roadmap.

A product roadmap represents a plan to develop a product that will satisfy its market: key features and bug fixes, but also non-functional requirements like page speed or test coverage. It's useful to communicate the vision of the product to the team and the end-users, facilitate decision-making, and to provide a reference point in a cross-functional work environment to promote collaboration and alignment.

You only need a spreadsheet to create your own roadmap, but at Rowy we figured out a way to make it collaborative and easy to use. We'll show you how in this article.

If you're interested in rolling out your own fork of this demo, you can find the code directly on Github or skip straight to part 4 of this article.

1. Creating a collaborative spreadsheet with Rowy

You'll need a Rowy account to create a programmable spreadsheet your team can access with just an invite email. The installation process only takes a few minutes and guides you step-by-step, so see you soon: install Rowy.

You can also self-host Rowy using the open-source version on Github.

2. The basic table columns

Once you're logged in, create a new project and pick the Roadmap template to obtain a table that's ready to use.

You'll find several columns that we'll now explain to you. First, we have two columns for a feature title and description. A roadmap should at least contain a list of user stories in the form of feature descriptions:

1.jpg

You'll also find a column for the status of each feature (need feedback, next, in progress, complete) and a column for a target date. We use the dropdown column type for the status column, and the Date column type for the targetRelease column. These columns are useful to decide what needs to be worked on first and communicate this information to all stakeholders:

3.jpg

Your spreadsheet is ready to be populated with features. You can add a few rows to get started, and then send an email invite to your teammates. A few things to keep in mind:

  1. Features drive adoption, so it's important to have a list of features that are planned for the next releases. Without growth, there's no product.
  2. But you also need to be mindful of which features to work on first. You can't do everything at once, and you don't want to overwhelm users with features they don't need: prioritization is key to make the most of the resources you have while decreasing response times.
  3. That's why a roadmap needs to be collaborative by design, to account for the needs of everyone involved. You need a way to display the roadmap to your audience, but also allow them to give their opinion directly via vote or comment or indirectly using key product metrics.

3. The voting feature

We can add features, but we still need a way to prioritize them. A voting feature with upvote / downvote buttons is simple to implement with Rowy―you just need to set up an Action type column. An Action column is a special column type that allows you to run a custom script when a user clicks on its button. For voting, we added 3 types of actions to the template: upvote, downvote, and an 'urgent' vote that's worth two upvotes.

First, the upvote action. We created a new column with the Action type and pasted the following code in the column settings, under the Action section:

onst action: Action = async ({ row, ref, db, storage, auth, actionParams, user }) => {
    // votes collection reference
    const voteCollectionRef = db.collection(`${ref.path}/votes`);
    // get current user's vote if already voted
    const voteQuery = await voteCollectionRef.where("_createdBy.uid", "==", user.uid).get();
    const voteType = "Yes";
    // if it's already voted
    if (voteQuery.docs[0]) {
        const voteRef = voteQuery.docs[0].ref;
        const vote = voteQuery.docs[0].get("vote");
        if (vote === voteType) {
            // do not modify if vote has not changed
            return {
                success: false,
                message: `Your vote has already been registered for ${row.feature} as ${voteType}`,
            };
        } else {
            // update if vote has changed 
            await voteRef.update({ vote: voteType });
            return {
                success: true,
                message: `Successfully updated your vote for ${row.feature} to ${voteType}`,
            };
        }
    } else {
        // add new vote if it's not voted yet
        await voteCollectionRef.add({
            _createdBy: user,
            _createdAt: new Date(),
            vote: voteType,
        })
        return {
            success: true,
            message: `Successfully registered your vote for ${row.feature} as ${voteType}`
        }
    }

}

You'll see something like this in the spreadsheet UI:

5.jpg

As you can see in the code, we need an additional column named votes to store the voting results. This votes column is a sub-collection of documents, each representing a vote:

6.jpg

We did the same for the downvote action, but we changed the voteType variable to "Meh":

const action: Action = async ({ row, ref, db, storage, auth, actionParams, user }) => {
    // votes collection reference
    const voteCollectionRef = db.collection(`${ref.path}/votes`);
    // get current user's vote if already voted
    const voteQuery = await voteCollectionRef.where("_createdBy.uid", "==", user.uid).get();
    const voteType = "Meh";
    if (voteQuery.docs[0]) {
        // if it's already voted
        const voteRef = voteQuery.docs[0].ref;
        const vote = voteQuery.docs[0].get("vote");
        if (vote === voteType) {
            // do not modify if vote has not changed
            return {
                success: false,
                message: `Your voute has already been registered for ${row.feature} as ${voteType}`,
            }
        } else {
            // update if vote has changed 
            await voteRef.update({ vote: voteType });
            return {
                success: true,
                message: `successfully updated your vote to ${voteType} for ${row.feature}`,
            };
        }
    } else {
        // add new vote if it's not voted yet
        await voteCollectionRef.add({
            _createdBy: user,
            vote: voteType,
        });
        return {
            success: true,
            message: `successfully registered your vote as urgent for ${row.feature}`,
        };
    }
}

And finally the urgent vote action:

const action:Action = async ({row,ref,db,storage,auth,actionParams,user}) => {
    const voteCollectionRef = db.collection(`${ref.path}/votes`)
    const voteQuery = await voteCollectionRef.where("_createdBy.uid", "==", user.uid).get()
    const voteType = "Urgent"
    if (voteQuery.docs[0]) {
        const voteRef = voteQuery.docs[0].ref
        const vote = voteQuery.docs[0].get("vote")
        if (vote === voteType) {
            return {
                success: false,
                message: `Your voute has already been registered for ${row.feature} as ${voteType}`
            }
        } else {
            await voteRef.update({vote:voteType,
                comment: actionParams.comment,
                email: actionParams.email,
            })
            return {
                success: true,
                message: `successfully updated your vote to ${voteType} for ${row.feature}`
            }
        }
    } else {
    await voteCollectionRef.add({
        _createdBy: user,
        vote: voteType,
        comment: actionParams.comment,
        email: actionParams.email,
    })
    return {
        success: true,
        message: `successfully registered your vote as urgent for ${row.feature}`
    }
    }
}

At this point, we have the three columns we needed to implement the voting feature. We just needed one last column to calculate a total score that will guide us during the prioritization phase, assuming the higher the score, the better the feature.

We can do this by creating a derivative column called combined. A derivative column derives its value from one or many columns, which is perfect for this use case where you need to aggregate different values in a sum. Just like an Action column, you can paste the following code in the column settings to calculate the total score:

const derivative: Derivative = async ({ row, ref, db, storage, auth }) => {
  return (row.votesSummary.Yes ?? 0) - (row.votesSummary.Meh) + 2 * (row.votesSummary.Urgent);
}

In the end, you obtain the following spreadsheet:

2.jpg

Taking decisions require good data, and a roadmap showcasing clear priorities is a good way to make sure everyone is on the same page. You can use it to meet once a week and define the tasks you need to accomplish.

Of course, you can modify the template to fit your needs―it's all customizable.

4. Making it open to the public

Now that we have a working spreadsheet all teammates can access, we can make it public and share it with all users. To do that, you can use Firebase's API combined with any front-end framework you like, nocode or not. To get you started right away, we built an open-source demo with Remix you can easily fork:

4.jpg

The app includes all the features you'll need to make your roadmap public:

  • Get feedback on your roadmap from public user groups or communities
  • Upvote and downvote
  • Comments
  • Customizable categories: In progress, Next, Needs feedback, Release ..
  • Open-source, flexible and fully free
  • Optionally, the UI comes with an in-app feedback widget for open ended feeature requests or feeback
  • CMS UI with ability to add any automation or workflows with Rowy

You can find the code on Github and adapt it to your tech stack, or you can just deploy it directly to Vercel.

Check Out More Demos

Hope you enjoyed it! If you'd like to see more examples of what you can do with Rowy, you can visit this link: https://demo.rowy.io/tables. There are 24 demos and examples available, including using Replicate API for face restoration, Stable Diffusion for image generation, and GPT-3 for text-to-speech. If you have any questions about building something with Rowy, don't hesitate to reach out to us. We are happy to assist you!

Get started with Rowy in minutes

Continue reading

Browse all