You are launching a SaaS product—over 500 feature requests come in monthly. The team spends 10 hours per week on internal reports and user responses. A public roadmap automates this: users see statuses themselves, and the team saves resources. One of our clients noted: "The public roadmap reduced support requests by 40% in the first month." — Client, SaaS project Without such a page, users lose patience, increasing churn and support load.
Developing a public roadmap page with Notion or Jira integration—this solution lets users track feature statuses. As developers, we prioritize performance: the page must load in <1 s, update without manual deployment, and integrate with your tools (Notion, Jira, Linear). Below is an architectural solution with ISR caching, Kanban board, and automatic notifications. It fits product teams of any size and pays for itself in less than 3 months.
Why a public roadmap boosts user trust?
Users are tired of "soon" without dates. When they see their feature move from "planned" to "in progress" and finally to "shipped"—it proves their voice was heard. According to Wikipedia, a roadmap in product development is a strategic document. Our page is its public projection, synced with the real backlog.
Data Sources: Notion vs Custom DB
The choice of source affects autonomy and speed. Comparison:
| Criterion | Notion as CMS | Custom Table (Laravel/PostgreSQL) |
|---|---|---|
| Schema flexibility | Limited by properties | Any fields and relations |
| Query speed | ~300–500 ms (API) | ~10–20 ms (indices) |
| Administration | Not needed | Migrations, admin panel |
| Offline access | Depends on Notion | Full control |
| Output customization | Only sort/filter | Any logic |
If the team is small and doesn't want to develop an admin panel—choose Notion. For a high-load project with custom reports—custom tables. The latter is 5 times faster for select queries.
Example integration with Notion:
// lib/roadmap.ts
import { Client } from '@notionhq/client';
const notion = new Client({ auth: process.env.NOTION_TOKEN });
export interface RoadmapItem {
id: string;
title: string;
status: 'planned' | 'in_progress' | 'done';
quarter: string; // 'Q1'
category: string;
votes: number;
description: string;
}
export async function getRoadmap(): Promise<RoadmapItem[]> {
const response = await notion.databases.query({
database_id: process.env.NOTION_ROADMAP_DB_ID!,
filter: {
property: 'Public',
checkbox: { equals: true },
},
sorts: [{ property: 'Quarter', direction: 'ascending' }],
});
return response.results.map(page => ({
id: page.id,
title: page.properties.Name.title[0]?.plain_text ?? '',
status: page.properties.Status.select?.name?.toLowerCase().replace(' ', '_') as any,
quarter: page.properties.Quarter.select?.name ?? '',
category: page.properties.Category.select?.name ?? '',
votes: page.properties.Votes.number ?? 0,
description: page.properties.Description.rich_text[0]?.plain_text ?? '',
}));
}
A custom table in Laravel is created with a migration containing fields: title, status, quarter, category, sort_order, is_public. This gives full control and speed.
How to organize a Kanban board without reloading?
The frontend (React or Vue) renders three columns: "Planned", "In Progress", "Done". Each card contains category, quarter, name, and description. Filtering is client-side, no server requests. For state management we use zustand—simplifying filter synchronization.
Step-by-step implementation:
- Set up a zustand store with fields filterCategory and searchQuery.
- Create a column component that accepts an array of items and displays cards.
- Implement filtering with useMemo—it works instantly without extra re-renders.
- Add virtualization for lists > 500 items (react-window).
// components/Roadmap.tsx
const STATUS_COLUMNS = [
{ key: 'planned', label: 'Planned', color: 'bg-gray-100' },
{ key: 'in_progress', label: 'In Progress', color: 'bg-blue-100' },
{ key: 'done', label: 'Done', color: 'bg-green-100' },
];
export function RoadmapBoard({ items }: { items: RoadmapItem[] }) {
const grouped = STATUS_COLUMNS.map(col => ({
...col,
items: items.filter(i => i.status === col.key),
}));
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{grouped.map(col => (
<div key={col.key}>
<h3 className={`font-semibold px-3 py-2 rounded-t ${col.color}`}>
{col.label} <span className="text-gray-500 font-normal">({col.items.length})</span>
</h3>
<div className="space-y-3 mt-3">
{col.items.map(item => (
<RoadmapCard key={item.id} item={item} />
))}
</div>
</div>
))}
</div>
);
}
function RoadmapCard({ item }: { item: RoadmapItem }) {
return (
<div className="bg-white border rounded-lg p-4 shadow-sm">
{item.category && (
<span className="text-xs bg-indigo-100 text-indigo-700 px-2 py-0.5 rounded-full">{item.category}</span>
)}
<h4 className="font-medium mt-2">{item.title}</h4>
{item.description && <p className="text-sm text-gray-500 mt-1">{item.description}</p>}
{item.quarter && <p className="text-xs text-gray-400 mt-2">{item.quarter}</p>}
</div>
);
}
Search and filter by categories work instantly thanks to React state. We use virtualization for lists exceeding 500 items—guaranteeing 60 fps.
ISR Caching: advantages over SSR
ISR (Incremental Static Regeneration) generates a static page on the first request and then updates it on a schedule. In Next.js this is set via the revalidate parameter. We set 3600 seconds (1 hour)—the page is always fresh but does not load the server on every visit:
// pages/roadmap.tsx
export const getStaticProps: GetStaticProps = async () => {
const items = await getRoadmap();
return {
props: { items },
revalidate: 3600, // Regenerate once per hour
};
};
This gives TTFB < 50 ms and LCP < 1.5 s—excellent for Core Web Vitals. Unlike SSR, where the server generates the page for each request (TTFB ~200 ms), ISR reduces server load by 10 times.
Automatic Notifications
Note: when a status changes to done, we automatically notify subscribers. We use a Laravel event listener: on status change, we check if it became 'done' and send an email to all subscribed users. For this, the RoadmapItem model has a subscribers relation. Emails go to the queue to avoid blocking the response. Typical delivery time is less than 1 minute.
Work Process for the Roadmap Page
| Stage | Duration | Result |
|---|---|---|
| Analytics | 0.5 days | Agreed sources and statuses |
| Design | 0.5 days | Design and component structure |
| Implementation | 1.5 days | Ready page with integration |
| Testing | 0.5 days | Core Web Vitals and functionality check |
| Deployment | 0.5 days | Launch on production |
A typical mistake at this stage is not accounting for mobile filtering, which increases INP. We optimize rendering for any screen, using lazy loading for off-screen cards.
What’s Included in the Result
- Public page with Kanban board and filtering
- Integration with Notion or custom DB (API + migrations)
- ISR caching (Next.js) or equivalent for other frameworks
- Automatic notifications on feature release
- Documentation for adding new items
- Access to repository and hosting
- Team training (1 hour online)
Timeline and Cost
Development time—from 3 to 4 working days. Cost is calculated individually based on integration complexity and design. Usually it’s a fixed price per project—contact us for an estimate. Budget savings on support can reach 50%. According to our estimates, implementing a roadmap reduces operational costs by 20–30%.
Our team’s experience—5+ years in product development, over 50 implemented public roadmaps for SaaS and startups. We guarantee transparent architecture and compliance with Core Web Vitals.
Contact us to get demo access. If you want to implement a public roadmap on your site, get a consultation on integration and timelines. Order roadmap page development today and increase your product transparency.







