Building a Comprehensive Product Roadmap Management System with Next.js and TanStack Table

Discover how EliteSaaS built a powerful product roadmap management system using Next.js, TanStack Table, and PostgreSQL. Explore advanced admin tools, timezone-safe date handling, and multiple public views. Perfect for SaaS teams seeking scalable solutions.

July 1, 2025
Updated July 1, 2025
Building a Comprehensive Product Roadmap Management System with Next.js and TanStack Table

Product roadmaps are the backbone of transparent product development, but building a feature-rich roadmap system that handles both admin management and public viewing can be surprisingly complex. Today, I'll walk you through how we built a comprehensive roadmap management system for EliteSaaS that tackles everything from timezone handling to advanced table functionality.

The Challenge: Beyond Basic CRUD

Most roadmap implementations are simple lists, but we needed something more sophisticated:

  • Dual Interface Design: Powerful admin tools + beautiful public views
  • Advanced Table Management: Sorting, filtering, pagination with professional UX
  • Timezone-Safe Date Handling: Because "May 17th" should be May 17th everywhere
  • Multiple View Types: Kanban boards, timeline views, and card layouts
  • Comprehensive Tracking: Both estimated and actual completion dates
  • Real-time Updates: Instant UI feedback for all operations

Technical Architecture Overview

Our roadmap system is built on a modern stack:

// Core Stack
- Next.js 15 (App Router)
- Supabase (PostgreSQL + RLS)
- TanStack Table v8 (Advanced table functionality)
- TypeScript (End-to-end type safety)
- Tailwind CSS + shadcn/ui (Design system)
- Zustand (State management)

Database Schema Design

The foundation starts with a robust PostgreSQL schema:

CREATE TABLE roadmap_items (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  title VARCHAR(255) NOT NULL,
  description TEXT NOT NULL,
  status VARCHAR(20) DEFAULT 'planned', -- completed, in-progress, planned, cancelled
  priority VARCHAR(20) DEFAULT 'medium', -- low, medium, high, critical
  category VARCHAR(100) NOT NULL,
  quarter VARCHAR(20), -- "Q1 2025" or NULL for TBD
  estimated_completion DATE, -- DATE type (not timestamp!)
  completed_at DATE, -- DATE type (not timestamp!)
  sort_order INTEGER DEFAULT 0,
  is_public BOOLEAN DEFAULT true,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

The Date Type Decision

One of our biggest architectural decisions was using PostgreSQL's DATE type instead of TIMESTAMP for completion dates. Here's why:

// ❌ TIMESTAMP approach (timezone nightmare)
completed_at: "2025-05-17T04:00:00.000Z";
// At 2:31 AM EST, this becomes May 16th 11:00 PM

// ✅ DATE approach (timezone-safe)
completed_at: "2025-05-17";
// Always displays as May 17th, regardless of timezone

This seemingly simple change eliminated an entire class of timezone-related bugs that plague date-heavy applications.

Advanced Table Management with TanStack Table

For the admin interface, we needed professional-grade table functionality. TanStack Table v8 provided the perfect foundation:

Column Configuration

const columns: ColumnDef<RoadmapItem>[] = [
  {
    accessorKey: "title",
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Title" />
    ),
    cell: ({ row }) => {
      const item = row.original;
      return (
        <div className="max-w-[300px]">
          <p className="font-medium truncate">{item.title}</p>
          <p className="text-sm text-muted-foreground line-clamp-2">
            {item.description}
          </p>
        </div>
      );
    },
  },
  {
    accessorKey: "estimated_completion",
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Est. Completion" />
    ),
    cell: ({ row }) => {
      const date = row.getValue("estimated_completion") as string | null;
      if (!date) return <div className="text-muted-foreground">—</div>;
      return <div className="text-sm">{formatDateString(date)}</div>;
    },
  },
  // ... more columns
];

Smart Filtering and Search

The table includes intelligent filtering capabilities:

// Status-based filtering
filterFn: (row, id, value) => {
  return value.includes(row.getValue(id));
},
  (
    // Global search across title and description
    <DataTableToolbar
      table={table}
      searchKey="title"
      searchPlaceholder="Search roadmap items..."
    />
  );

Responsive Design Considerations

One challenge with data tables is horizontal scrolling on mobile. We solved this with:

// Sticky headers with proper scrolling
<ScrollArea className="h-[400px] w-full rounded-md border">
  <Table className="relative">
    <TableHeader className="sticky top-0 bg-background z-10">
      {/* Headers */}
    </TableHeader>
    <TableBody>{/* Rows */}</TableBody>
  </Table>
  <ScrollBar orientation="horizontal" />
</ScrollArea>

Dual Date Tracking System

A sophisticated roadmap needs both planning and reality tracking:

Estimated vs Actual Completion

interface RoadmapFormData {
  estimated_completion: string | null; // Planning date
  completed_at: string | null; // Reality date
}

// Auto-populate actual completion when status changes
const handleStatusChange = (value: string) => {
  updateFormData("status", value);

  if (value === "completed" && !formData.completed_at) {
    const today = new Date().toISOString().split("T")[0];
    updateFormData("completed_at", today);
  }
};

Smart Form Behavior

The completion date field only appears when relevant:

{
  formData.status === "completed" && (
    <div className="space-y-2">
      <Label htmlFor="completed_at">Actual Completion Date</Label>
      <Input
        id="completed_at"
        type="date"
        value={formatDateForInput(formData.completed_at)}
        onChange={(e) => updateFormData("completed_at", e.target.value)}
      />
    </div>
  );
}

Solving the Timezone Problem

The most insidious issue we faced was timezone conversion during date display. Even with DATE-only database storage, JavaScript's new Date() constructor was causing problems:

The Problem

// ❌ This creates timezone issues
const date = "2025-05-17"; // DATE from database
new Date(date).toLocaleDateString();
// Returns "5/16/2025" at 2:31 AM EST!

The Solution

Instead of converting to Date objects, we implemented direct string formatting:

// ✅ Timezone-safe date formatting
export const formatDateString = (dateString: string): string => {
  const [year, month, day] = dateString.split("-");
  const monthNames = [
    "Jan",
    "Feb",
    "Mar",
    "Apr",
    "May",
    "Jun",
    "Jul",
    "Aug",
    "Sep",
    "Oct",
    "Nov",
    "Dec",
  ];
  return `${monthNames[parseInt(month) - 1]} ${parseInt(day)}, ${year}`;
};

// Usage across all components
{
  formatDateString(item.completed_at);
} // Always correct!

This approach is:

  • Bulletproof: No timezone math involved
  • Consistent: Same format everywhere
  • Performant: Simple string manipulation vs Date object creation
  • Predictable: "2025-05-17" always becomes "May 17, 2025"

Multiple View Types for Public Display

The public roadmap offers three distinct viewing experiences:

1. Kanban Board View

Perfect for status-based organization:

const groupedItems = roadmapItems.reduce((acc, item) => {
  if (!acc[item.status]) acc[item.status] = [];
  acc[item.status].push(item);
  return acc;
}, {} as Record<string, RoadmapItem[]>);

const columnOrder = ["planned", "in-progress", "completed", "cancelled"];

2. Timeline View

Chronological organization by quarters:

const sortedItems = [...roadmapItems].sort((a, b) => {
  // Handle "TBD" and empty quarters
  if (a.quarter === "TBD") return 1;
  if (b.quarter === "TBD") return -1;

  // Parse "Q1 2025" format for proper sorting
  const aMatch = a.quarter?.match(/Q(\d+) (\d+)/);
  const bMatch = b.quarter?.match(/Q(\d+) (\d+)/);

  if (aMatch && bMatch) {
    const aYear = parseInt(aMatch[2]);
    const bYear = parseInt(bMatch[2]);
    if (aYear !== bYear) return aYear - bYear;
    return parseInt(aMatch[1]) - parseInt(bMatch[1]);
  }

  return a.sort_order - b.sort_order;
});

3. Card View

Information-dense layout with visual priority indicators:

const priorityConfig = {
  critical: { icon: Zap, color: "bg-red-500/10 text-red-700" },
  high: { icon: Target, color: "bg-orange-500/10 text-orange-700" },
  medium: { icon: Calendar, color: "bg-yellow-500/10 text-yellow-700" },
  low: { icon: Circle, color: "bg-gray-500/10 text-gray-700" },
};

Code Quality and Maintainability

DRY Principle Application

Initially, we had the same date formatting function in four different files. We refactored to a shared utility:

// Before: Copy-pasted 4 times ❌
const formatDateString = (dateString: string) => {
  /* ... */
};

// After: Single source of truth ✅
// apps/web/src/lib/utils/date.ts
export const formatDateString = (dateString: string): string => {
  // Implementation once, used everywhere
};

TypeScript Integration

Strong typing throughout the system prevents runtime errors:

export interface RoadmapItem {
  id: string;
  title: string;
  status: "completed" | "in-progress" | "planned" | "cancelled";
  priority: "low" | "medium" | "high" | "critical";
  estimated_completion: string | null; // YYYY-MM-DD
  completed_at: string | null; // YYYY-MM-DD
  // ... other fields
}

Component Composition

The dialog system uses a single component for both create and edit modes:

interface RoadmapItemDialogProps {
  mode: "create" | "edit";
  item?: RoadmapItem; // Required for edit mode
  children: React.ReactNode;
  onItemChanged?: () => void;
}

// Dynamic behavior based on mode
const Icon = mode === "create" ? Plus : Edit;
const title = mode === "create" ? "Create Roadmap Item" : "Edit Roadmap Item";

Performance Optimizations

Efficient State Management

Loading states prevent multiple API calls:

const [updatingVisibility, setUpdatingVisibility] = useState<Set<string>>(
  new Set()
);

const togglePublic = async (id: string, currentPublic: boolean) => {
  if (updatingVisibility.has(id)) return; // Prevent duplicate calls

  setUpdatingVisibility((prev) => new Set([...prev, id]));
  try {
    await updateItem(id, { is_public: !currentPublic });
  } finally {
    setUpdatingVisibility((prev) => {
      const newSet = new Set(prev);
      newSet.delete(id);
      return newSet;
    });
  }
};

Optimistic UI Updates

The system provides immediate feedback before server confirmation:

// Show loading state immediately
<Button disabled={isUpdating}>
  {isUpdating ? (
    <Loader2 className="w-4 h-4 animate-spin" />
  ) : (
    <Eye className="w-4 h-4" />
  )}
</Button>

Lessons Learned

  1. Date handling is deceptively complex - Always consider timezone implications early
  2. User experience details matter - Loading states and responsive design make the difference
  3. Code duplication creeps in fast - Regular refactoring to shared utilities pays off
  4. TypeScript catches edge cases - Strong typing prevents many runtime errors
  5. Progressive enhancement works - Start with basic functionality, add advanced features incrementally

Next Steps

Future enhancements we're considering:

  • Drag-and-drop reordering in Kanban view
  • Bulk operations for status changes
  • Integration with GitHub issues for automatic updates
  • Email notifications for roadmap changes
  • Public API for third-party integrations
  • Advanced filtering by date ranges and custom fields

Technical Specs Summary

Architecture:
  - Next.js 15 App Router
  - PostgreSQL with Row Level Security
  - TanStack Table v8
  - TypeScript throughout
  - Tailwind CSS + shadcn/ui

Key Features:
  - Dual date tracking (estimated vs actual)
  - Three public view types (kanban, timeline, cards)
  - Advanced table with sorting/filtering/pagination
  - Timezone-safe date handling
  - Real-time UI feedback
  - Responsive design
  - Type-safe API layer

Performance:
  - Optimistic UI updates
  - Efficient state management
  - Minimal re-renders
  - Code splitting by view type

Try It Yourself

We'd love to get your feedback on this roadmap system! You can see the live demo roadmap here and learn more about the roadmap system to see all the features in action.

Some questions for the community:

  • What roadmap features are most important to your users?
  • How do you handle timezone issues in date-heavy applications?
  • Have you implemented similar table functionality? What challenges did you face?
  • What would you add to make this roadmap system even better?

Drop us a line at hello@elitesaas.dev or connect with us on Twitter. We're always looking to improve and would love to hear about your experiences building similar systems!


Want to build something similar? The techniques in this post apply to any data-heavy application with complex table requirements. The key is starting with solid architectural decisions and progressively enhancing the user experience.

Found this helpful? Join our waitlist to get early access to more tools and resources like this

500+ developers waiting

Join Our Waitlist

Building something similar? Get early access to our comprehensive SaaS template and ship faster

Get early access • No spam, ever