Dashboard

How to Add a Public Roadmap Page to an AI-Built App

A practical walkthrough for adding a public roadmap page to an AI-built app: the public-vs-internal call, a minimal status-and-votes data model, and the moderation queue that keeps raw submissions off the live page.

Steve Jefferson
Steve Jefferson
Developer Advocate
5 September 20261 min read

A public roadmap page shows visitors what you are planning, building, and have already shipped, usually laid out as three columns: Planned, In Progress, and Shipped. Adding a public roadmap page to an AI-built app is mostly a data modeling and moderation problem, not a design problem. This guide covers the public-versus-internal decision, a minimal schema for statuses and votes, and why raw user submissions should never post straight to a live page.

Decide whether the roadmap should actually be public

This post assumes you already have a working app and are adding a feature to it; if you are still at the stage of picking a stack and a build process, start there first. Before you build anything on top of that, settle the public-versus-internal question, because it changes the schema.

A public roadmap is a commitment device: customers see it, screenshot it, and hold you to it. That is useful for reducing "when is X coming?" support tickets and for showing prospects the product is alive. It is a bad fit for anything competitively sensitive, anything tied to a pricing change you have not finalized, or work you might quietly drop.

Most teams land on a hybrid: near-term, committed work goes on the public page with a status, not a date. Long-range strategic bets stay in an internal doc or project tracker. If you publish a date and miss it, you get a wave of comments; if you publish a status and slip, nobody notices until the status changes.

Pick a status model that matches your audience

Three statuses cover almost every case: Planned, In Progress, and Shipped. If you also accept public feature requests, add a fourth, internal-only status such as Under review, so new submissions do not appear until someone has looked at them. Whether you use technical language ("in development") or plain language ("in progress") depends on your audience; a public roadmap feature aimed at a general consumer app should skip engineering jargon entirely.

Shipped items pair naturally with a changelog page, where the roadmap card can link straight to the release notes once it moves to Shipped, so visitors get the announcement and the details in one click instead of two separate, disconnected pages.

A minimal roadmap page data model

The roadmap page data model needs two tables: the items themselves, and the votes on them. Keep the status as a constrained value, not free text, or you will eventually get a card stuck in "in-progress" with a lowercase i and hyphen that your frontend filter does not match.

create type roadmap_status as enum ('under_review', 'planned', 'in_progress', 'shipped', 'declined');

create table roadmap_items (
  id uuid primary key default gen_random_uuid(),
  title text not null,
  description text not null,
  status roadmap_status not null default 'under_review',
  is_public boolean not null default false,
  admin_notes text,
  created_by uuid references auth.users(id),
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create table roadmap_votes (
  id uuid primary key default gen_random_uuid(),
  roadmap_item_id uuid not null references roadmap_items(id) on delete cascade,
  user_id uuid not null references auth.users(id),
  created_at timestamptz not null default now(),
  unique (roadmap_item_id, user_id)
);

Three details do the heavy lifting here. The status column is an enum, not a string, so a typo cannot silently create a fifth status your UI never renders. The is_public flag is separate from status, which is what makes moderation possible: an item can sit fully filled out and triaged as planned while still being invisible until someone flips is_public to true. And the unique (roadmap_item_id, user_id) constraint on votes is what enforces one vote per user at the database level, not just in your API code.

For the vote count itself, do not maintain a separate counter column unless the table is genuinely huge. A count(*) grouped by roadmap_item_id, with an index on that column, is fast enough for a roadmap page with a few hundred items and reads far more reliably than a counter that can drift out of sync with the underlying rows.

Let users vote on features without letting them vote twice

The vote button should toggle, not just add. On click, try to insert a row into roadmap_votes; if the unique constraint rejects it because that user already voted, delete the existing row instead. That single insert-or-delete pattern is the entire voting feature; you do not need a separate "unvote" endpoint.

Requiring a signed-in user_id for every vote is a deliberate trade-off. Anonymous voting keyed on a cookie or IP address is trivial to inflate by clearing cookies or opening a private window, so most teams that let users vote on features require at least a lightweight account or a magic-link email before the vote counts. If you use Supabase, row level security policies can enforce this directly: allow insert only when user_id = auth.uid(), allow delete only on rows the user owns, and allow select on every row so the public count stays visible to everyone, including signed-out visitors.

The moderation problem nobody plans for

If visitors can submit their own feature requests, you have built a small user-generated-content surface attached to a public page, and it needs the same caution as a comments section. Unmoderated submissions end up carrying spam links, profanity, complaints about specific coworkers or customers by name, or requests that quote another company's private roadmap or pricing back at you. None of that should reach a public URL automatically.

The fix is the is_public flag from the schema above, used as a real moderation gate rather than decoration. A submission lands as under_review with is_public set to false. It stays invisible to everyone but the submitter and admins until a person on your side reads it, edits the title and description if needed, and flips it to planned and public together. For a small team this queue is nothing more than an admin-only filtered view of the same table sorted by created_at.

  • Require a signed-in account to submit, so every request is traceable to a real user, not an anonymous form post.

  • Rate-limit submissions per user per day, since a single frustrated visitor filing thirty requests is more common than you would expect.

  • Run a basic keyword or profanity filter as a first pass, but treat it as a triage aid, not a replacement for a human reading the item before its first publish.

  • Keep an admin_notes field separate from the public description, so internal context ("duplicate of #42", "customer threatened to churn over this") never leaks onto the public page.

Where AI coding tools default to the wrong version

Ask an AI coding tool to "add a roadmap page with voting" and the fastest path it reaches for is usually a single public table, a status text column, and a vote counter with no unique constraint, meaning anyone can click the button repeatedly and inflate the number, and anything typed into the request form appears on the live page immediately. That is true whether you are prompting Swarmz, Bolt, Lovable, or any other AI app builder: describing the constraint explicitly gets you further than accepting the first draft. Paste the two-table schema above into the prompt, ask for the enum and the unique constraint by name, and ask for a private, unapproved default state for new submissions rather than a public one.

It is worth pairing the roadmap page with a feedback widget so requests have one clear entry point instead of scattering across support email and social replies, and with a status page for the separate job of reporting incidents and uptime, which a roadmap should never try to double as.

Frequently asked questions

Should I let users vote on features anonymously or require an account?

Require an account, even a lightweight one created with just an email address. Anonymous voting tied to a cookie or IP address is easy to inflate by clearing cookies or switching browsers, and it gives you no way to notify a voter when the item they cared about ships.

What is a good roadmap page data model if I am starting from scratch?

Two tables: one for roadmap items with a status enum, an is_public flag, and admin notes, and one for votes with a unique constraint on the item and user pair. That combination gives you moderation, status tracking, and one-vote-per-user enforcement without any extra machinery.

How do I stop a public roadmap feature from filling up with spam?

Put every new submission behind an is_public flag set to false by default, review it before flipping it on, require sign-in to submit, and rate-limit submissions per user. A keyword filter can flag obvious junk automatically, but a person should still glance at each item before its first publish.

What is the difference between a roadmap page and a changelog page?

A roadmap page is forward-looking: what is planned, what is being built, what shipped recently. A changelog is backward-looking: a dated log of what actually went out, usually with more detail than a roadmap card. Many teams link the two, so a card that moves to Shipped points at its changelog entry.

Should roadmap items show exact ship dates?

Generally no. A status (Planned, In Progress, Shipped) sets expectations without creating a deadline you might miss in public. Reserve a specific date for items you are highly confident about, and even then a quarter or month is safer than a day.

How did this land?

About the author

Steve Jefferson
Steve Jefferson

Developer Advocate

Steve builds something with Swarmz every week and writes up what worked, what broke, and what he'd do differently. Tutorials and hands-on guides are his lane.

Share

Get the next post in your inbox

One email a month. Product updates, engineering posts, and the best of Built with Swarmz.

I agree to receive emails about AI building tips and Swarmz product news. Unsubscribe any time.