supa table

Component from Waghubinger Registry

Back to Registry

Option A: Via components.json

Requires REGISTRY_TOKEN in .env.local

pnpm dlx shadcn add @waghubinger/supa-table

Option B: Direct URL with Token

For quick testing or one-time installation

pnpm dlx shadcn add http://localhost:3000/r/supa-table.json?token=YOUR_TOKEN

SupaTable

Advanced data table component with Supabase integration, featuring pagination, filtering, sorting, Excel export, and saved views.

Features

  • Server-side pagination - Handle large datasets efficiently
  • Advanced filtering - Column-specific filters, date ranges, global search
  • Sorting - Multi-column sorting with Supabase integration
  • Excel export - Export filtered data with formatting
  • Saved views - Save and share filter/sort configurations
  • Column visibility - Show/hide columns dynamically
  • Row selection - Single or multi-row selection with "Select All Filtered"
  • Responsive design - Mobile-friendly with horizontal scroll
  • TypeScript - Full type safety with comprehensive interfaces

Installation

pnpm dlx shadcn add https://registry.tools.asscompact.at/r/supa-table.json?token=YOUR_TOKEN

This will install:

  • Components (10 files)
  • Hooks (2 files)
  • Helper functions (3 files)
  • Types (1 file)
  • API routes (4 template files)
  • Database migration (1 SQL file)

Quick Start

1. Database Setup

Run the migration to create required tables:

# Migration is installed to: supabase/migrations/XXX_supa_table_setup.sql
supabase db push

This creates:

  • table_export_sessions - Temporary storage for large exports
  • user_table_views - Saved filter/sort configurations per user

2. Supabase Client Setup

Create Supabase server clients in your project:

// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export async function createClient() {
  const cookieStore = await cookies()
  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    { cookies: () => cookieStore }
  )
}

export async function createServiceClient() {
  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.SUPABASE_SERVICE_ROLE_KEY!,
    { cookies: { getAll: () => [], setAll: () => {} } }
  )
}

3. Setup API Routes (Manual Step Required)

⚠️ API routes are NOT automatically installed to prevent accidental overwrites during updates.

Option A: Use Claude Code (Recommended)

If you're using Claude Code, simply ask:

"Claude, read the API_ROUTES.md file and create all SupaTable API routes for me"

Claude Code will:

  • ✅ Read the complete API route code from components/supa-table/API_ROUTES.md (installed with component)
  • ✅ Create all 4 API routes with correct import paths
  • ✅ Replace Supabase client placeholders automatically
  • ✅ Verify syntax and fix errors

Option B: Manual Setup

See the complete documentation in API_ROUTES.md which includes:

  • Full source code for all 4 routes
  • Detailed setup instructions
  • Import path adjustments
  • Test commands
  • Troubleshooting guide

Required routes:

  • app/api/supa-table/route.ts - Main data fetching
  • app/api/supa-table/selection-ids/route.ts - "Select All Filtered" functionality
  • app/api/supa-table/export-session/route.ts - Create export sessions
  • app/api/supa-table/export/route.ts - Excel export generation

Why not auto-installed? API routes require project-specific configuration (Supabase clients, auth). Auto-installing would overwrite your customizations on every update. The API_ROUTES.md file is installed with the component so you (or Claude) always have the correct version available.

4. Basic Usage

'use client'

import { SupaTable } from "@/components/supa-table"
import { SupaTableFieldConfig } from "@/types/supa-table"
import { createClient } from "@/lib/supabase/client"

const tableFields: SupaTableFieldConfig[] = [
  {
    field: "id",
    label: "ID",
    type: "number",
    contentWidth: 80,
    visible: true,
  },
  {
    field: "name",
    label: "Name",
    type: "text",
    contentWidth: 200,
    visible: true,
    enableFiltering: true,
    enableSorting: true,
  },
  {
    field: "email",
    label: "Email",
    type: "text",
    contentWidth: 250,
    visible: true,
    enableFiltering: true,
  },
  {
    field: "created_at",
    label: "Created",
    type: "date",
    contentWidth: 180,
    visible: true,
    enableSorting: true,
  },
]

export default function UsersTable() {
  const supabase = createClient()

  return (
    <SupaTable
      supabaseClient={supabase}
      dbTable="users"
      tableFields={tableFields}
      rowIdAccessor="id"
      enableExcelExport={true}
      enableSavedViews={true}
      enableColumnVisibility={true}
    />
  )
}

Component API

SupaTable Props

PropTypeRequiredDescription
supabaseClientSupabaseClientYesSupabase client instance
dbTablestringYesDatabase table name
tableFieldsSupaTableFieldConfig[]YesColumn configuration
rowIdAccessorstringYesField name for unique row ID
tableIdstringNoUnique ID for view storage (default: dbTable)
dbQueryStringstringNoCustom Supabase select query (default: "*")
enableExcelExportbooleanNoEnable Excel export functionality
allowSavedFiltersbooleanNoEnable saved views functionality
enableColumnVisibilitybooleanNoEnable column show/hide toggle
enableRowSelectionbooleanNoEnable row selection checkboxes
adminModebooleanNoUse service role client (bypasses RLS)
globalFiltersGlobalFilter[]NoPre-applied filters from parent
staticViewsStaticViewConfig[]NoPredefined views (read-only, from code)
remoteViewsRemoteViewsConfigNoServer-side view storage config
onSelectionChange(ids: (string|number)[]) => voidNoCallback when selection changes
onViewChange(view: ViewPreset | null) => voidNoCallback when active view changes
customActionsReact.ReactNodeNoCustom toolbar actions

SupaTableFieldConfig

interface SupaTableFieldConfig {
  field: string                    // Database column name (supports dot notation for joins)
  label: string                    // Display label
  type: FieldType                  // "text" | "number" | "date" | "bool" | "json"
  contentWidth?: number            // Column width in pixels
  visible?: boolean                // Show/hide column
  enableFiltering?: boolean        // Enable filter for this column
  enableSorting?: boolean          // Enable sorting for this column
  valueMap?: Record<string, any>   // Map database values to display values
  valueManipulation?: (val: any) => any  // Transform value before display
  formattingInfo?: {
    formatNumber?: "currency" | "percentage"
    formatDate?: "short" | "long"
  }
}

Advanced Features

Custom Queries with Joins

const tableFields: SupaTableFieldConfig[] = [
  { field: "id", label: "ID", type: "number" },
  { field: "user.name", label: "User", type: "text" }, // Dot notation for joins
  { field: "user.email", label: "Email", type: "text" },
]

<SupaTable
  dbTable="orders"
  dbQueryString="*, user:users(name, email)" // Supabase join syntax
  tableFields={tableFields}
  rowIdAccessor="id"
  supabaseClient={supabase}
/>

Value Mapping

const statusField: SupaTableFieldConfig = {
  field: "status",
  label: "Status",
  type: "text",
  valueMap: {
    "active": "Active",
    "inactive": "Inactive",
    "pending": "Pending",
  },
}

Value Manipulation

const priceField: SupaTableFieldConfig = {
  field: "price",
  label: "Price",
  type: "number",
  formattingInfo: { formatNumber: "currency" },
  valueManipulation: (val) => val / 100, // Convert cents to dollars
}

Global Filters

Pre-filter data from parent component:

const globalFilters: GlobalFilter[] = [
  {
    field: "status",
    value: "active",
    operator: "eq",
  },
  {
    field: "created_at",
    value: new Date(2024, 0, 1).toISOString(),
    operator: "gte",
  },
]

<SupaTable
  dbTable="users"
  globalFilters={globalFilters}
  {...otherProps}
/>

Excel Export

SupaTable supports exporting large datasets (1000+ rows) efficiently:

  1. Small datasets (<500 rows): Direct export
  2. Large datasets (>500 rows): Session-based export via database

Users can choose:

  • Formatted export: Apply value maps, date formatting, number formatting
  • Raw export: Export database values as-is

Views System

SupaTable v2.0+ includes a comprehensive view management system with three types of views:

  1. Local Views - Stored in localStorage (default)
  2. Static Views - Predefined in code (read-only)
  3. Remote Views - Stored on server with optional global sharing

Basic Local Views

Enable saved views with localStorage storage:

<SupaTable
  tableId="customers"       // Unique ID for view storage
  allowSavedFilters={true}  // Enable view functionality
  dbTable="customers"
  supabaseClient={supabase}
  {...otherProps}
/>

Users can:

  • Save current filter/sort/column configuration as a view
  • Set icons for views (30+ Lucide icons available)
  • Set a view as default (auto-applied on load)
  • Rename, reorder, and delete views

Static Views (Predefined)

Define read-only views in code that users cannot modify or delete:

import { StaticViewConfig } from "@/lib/supa-table-types"

const staticViews: StaticViewConfig[] = [
  {
    name: "Aktive Kunden",
    icon: "users",
    searchValues: { status: "active" },
    sorting: { sortBy: "created_at", sortAscending: false },
    isDefault: true,  // Applied on first load if no user default
  },
  {
    name: "VIP Kunden",
    icon: "star",
    searchValues: { tier: "vip" },
    description: "Kunden mit VIP-Status",  // Shown in tooltip
  },
  {
    name: "Offene Rechnungen",
    icon: "alert-triangle",
    searchValues: { invoice_status: "pending" },
    columnVisibility: {
      internal_notes: false,  // Hide specific columns
    },
  },
]

<SupaTable
  tableId="customers"
  staticViews={staticViews}
  allowSavedFilters={true}
  {...otherProps}
/>

StaticViewConfig Properties:

PropertyTypeDescription
namestringDisplay name (required)
iconViewIconNameIcon from Lucide (optional)
searchValuesRecord<string, string>Filter values
sorting{ sortBy, sortAscending }Sort configuration
columnVisibilityRecord<string, boolean>Column show/hide
isDefaultbooleanUse as default if no user default
descriptionstringTooltip text

Available Icons:

Status: star, heart, bookmark, flag, bell, check, x, circle-check, circle-x, alert-triangle, alert-circle, ban, sparkles, thumbs-up, thumbs-down

Time: calendar, calendar-days, clock

Data: filter, search, list, table, layers, grid

Organization: folder, archive, inbox, tag, users, user, building

Business: briefcase, shopping-cart, credit-card

Remote Views (Server Storage)

Enable server-side view storage with optional global sharing:

Step 1: Run the migration

# Apply migrations/002_supa_table_user_views.sql
supabase db push

Step 2: Create the API route

See API_ROUTES.md Section 5 for the complete route code, or ask Claude Code:

"Claude, read API_ROUTES.md and create the views API route"

Step 3: Enable remote views

<SupaTable
  tableId="customers"
  allowSavedFilters={true}
  remoteViews={{
    enabled: true,
    apiEndpoint: "/api/supa-table/views",  // default
    allowGlobalViews: true,                 // show global views from others
    canCreateGlobal: userIsAdmin,           // permission to create global views
  }}
  {...otherProps}
/>

RemoteViewsConfig Properties:

PropertyTypeDefaultDescription
enabledboolean-Enable remote storage (required)
apiEndpointstring/api/supa-table/viewsAPI endpoint URL
allowGlobalViewsbooleantrueShow global views in dropdown
canCreateGlobalbooleanfalseAllow creating global views

Remote Views Features:

  • Views synced to database (user_table_views table)
  • Personal views only visible to creator
  • Global views visible to all users (requires canCreateGlobal permission)
  • "Upload to server" button for local views
  • "Make global" option when saving (if permitted)

View Priority Chain

When loading SupaTable, views are applied in this priority order:

  1. User's Remote Default - If remote views enabled and user has set one
  2. User's Local Default - If user has set a local default
  3. Static Default - If a static view has isDefault: true
  4. Initial Values - Component's initial filter/sort state

Combining View Types

You can use all three view types together:

<SupaTable
  tableId="orders"
  allowSavedFilters={true}
  staticViews={[
    { name: "Pending", icon: "clock", searchValues: { status: "pending" } },
    { name: "Shipped", icon: "check", searchValues: { status: "shipped" } },
  ]}
  remoteViews={{
    enabled: true,
    canCreateGlobal: user?.role === "admin",
  }}
  {...otherProps}
/>

This creates a dropdown with:

  • Vordefinierte Ansichten (Static) - Pending, Shipped (read-only)
  • Globale Ansichten (Remote Global) - Created by admins, visible to all
  • Meine Ansichten (Local + Remote Personal) - User's own views

Row Selection

<SupaTable
  enableRowSelection={true}
  onSelectionChange={(selectedIds) => {
    console.log("Selected IDs:", selectedIds)
  }}
  {...otherProps}
/>

"Select All Filtered" functionality:

  • Automatically handles large datasets (1000+ rows)
  • Uses pagination to fetch all filtered IDs
  • Shows progress during fetch

API Routes

SupaTable requires 4 API routes for server-side operations:

  • app/api/supa-table/route.ts - Main data fetching endpoint
  • app/api/supa-table/selection-ids/route.ts - Bulk selection support
  • app/api/supa-table/export-session/route.ts - Export session management
  • app/api/supa-table/export/route.ts - Excel export generation

⚠️ Manual Setup Required

IMPORTANT: API routes are NOT automatically installed. This prevents accidental overwrites when updating the supa-table component.

Complete setup instructions are available in API_ROUTES.md, which includes:

  • ✅ Full source code for all 4 routes
  • ✅ Step-by-step setup guide
  • ✅ Import path adjustments
  • ✅ Supabase client configuration
  • ✅ Test commands
  • ✅ Troubleshooting guide
  • ✅ Special instructions for Claude Code (auto-setup)

Quick Setup with Claude Code:

"Claude, read the API_ROUTES.md file and create all SupaTable API routes for me"

Manual Setup:

  1. Read API_ROUTES.md for complete code
  2. Create 4 API route files in app/api/supa-table/
  3. Adjust import paths (registry → project paths)
  4. Configure Supabase clients
  5. Test endpoints

Why manual? API routes require project-specific configuration and updates would overwrite your customizations. The API_ROUTES.md file is installed with the component so you always have the correct version available.

Environment Variables

Required in your project's .env.local:

# Supabase
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key

# For admin mode / service client
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key

Troubleshooting

API Routes Return 500 Error

Problem: API routes fail with "Cannot read property 'from' of null"

Solution: Make sure you've replaced the placeholder Supabase clients:

// ❌ Wrong - Placeholder still active
const supabase = null as any; // PLACEHOLDER

// ✅ Correct - Import and use your client
import { createClient } from "@/lib/supabase/server";
const supabase = await createClient();

Export Fails for Large Datasets

Problem: Export times out or fails for 1000+ rows

Solution: SupaTable automatically uses session-based export for >500 rows. Ensure:

  1. Migration created table_export_sessions table
  2. API route app/api/supa-table/export-session/route.ts is configured
  3. Session cleanup runs (automatic after 1 hour)

Filtering Not Working

Problem: Column filters don't affect data

Solution: Check that:

  1. enableFiltering: true is set on the field config
  2. API route app/api/supa-table/route.ts is properly configured
  3. Database column exists and has correct type

RLS Policies Block Data

Problem: No data shows even though database has rows

Solution:

  • Check RLS policies on your table
  • Use adminMode={true} for testing (bypasses RLS)
  • Verify user is authenticated when using user client

TypeScript Errors

Problem: Import errors or type mismatches

Solution:

# Regenerate types from your Supabase schema
npx supabase gen types typescript --project-id YOUR_PROJECT_ID > types/supabase.ts

Performance Tips

Optimize Large Tables

  1. Add database indexes on frequently filtered/sorted columns:
CREATE INDEX idx_users_created_at ON users(created_at);
CREATE INDEX idx_users_status ON users(status);
  1. Use dbQueryString wisely - Only select needed columns:
dbQueryString="id, name, email, created_at" // Don't use * for large tables
  1. Limit joins - Each join increases query complexity:
// ✅ Good - Single level join
dbQueryString="*, user:users(name)"

// ❌ Slow - Multiple nested joins
dbQueryString="*, user:users(*, profile:profiles(*, company:companies(*)))"

Reduce Re-renders

IMPORTANT: Always wrap your tableFields configuration with useMemo to prevent unnecessary re-renders.

Large field configurations (20+ fields) can cause performance issues if recreated on every render:

export default function MyTable() {
  const supabase = createClient()

  // ⚠️ REQUIRED: Use useMemo for tableFields
  const tableFields = useMemo<SupaTableFieldConfig[]>(() => [
    {
      field: "id",
      label: "ID",
      type: "number",
      searchable: true,
      searchType: "number",
      defaultEnabled: true,
    },
    // ... rest of your fields (can be 50+ fields)
  ], []) // Empty dependency array - config never changes

  return (
    <SupaTable
      supabaseClient={supabase}
      tableFields={tableFields}  // Now stable across renders
      // ... other props
    />
  )
}

Why this matters:

  • Large configs (50+ fields with custom formatters/icons) can be expensive to recreate
  • Without useMemo, React creates a new array reference on every render
  • SupaTable internally uses React Table which triggers full recalculation when fields change
  • For complex tables with 1000+ rows, this can cause noticeable lag

Migration from GenericDataTable

If you're migrating from the old GenericDataTable component:

Component Rename

// Old
import { TableWithDataLayer } from "@/components/Tables/GenericDataTable"

// New
import { SupaTable } from "@/components/supa-table"

Props Changes

// Old
<TableWithDataLayer
  supabaseAdmin={supabase}
  dbTable="users"
  tableDbFields={fields}
  useServerActionForFiltering={true}
/>

// New
<SupaTable
  supabaseClient={supabase}     // Renamed from supabaseAdmin
  dbTable="users"
  tableFields={fields}           // Renamed from tableDbFields
  adminMode={false}              // Replaces useServerActionForFiltering
/>

Type Changes

// Old
import { ITableDbFields } from "@/components/Tables/GenericDataTable"

// New
import { SupaTableFieldConfig } from "@/types/supa-table"

Contributing

Found a bug or want to contribute? This component is part of the Waghubinger Registry.

  1. Test your changes locally
  2. Update this README if adding features
  3. Update CHANGELOG.md with your changes
  4. Rebuild registry: pnpm registry:build

License

MIT License - Part of Waghubinger Registry

Support

For issues or questions:

  • Check this README first
  • Review API route setup in app/api/supa-table/
  • Check Supabase client configuration in lib/supabase/
  • Verify database migration ran successfully

Version: 2.0.1 Last Updated: 2025-12-17 Registry: https://registry.tools.asscompact.at