Component from Waghubinger Registry
Requires REGISTRY_TOKEN in .env.local
pnpm dlx shadcn add @waghubinger/supa-tableFor quick testing or one-time installation
pnpm dlx shadcn add http://localhost:3000/r/supa-table.json?token=reO2TJ1CDL8vO-IxZatQTNV1POL6pqKi4e-GxVBKYIcNfhj2nVBl-hXNQkZXPwmAdvanced data table component with Supabase integration, featuring pagination, filtering, sorting, Excel export, and saved views.
pnpm dlx shadcn add https://registry.tools.asscompact.at/r/supa-table.json?token=YOUR_TOKEN
This will install:
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 exportsuser_table_views - Saved filter/sort configurations per userCreate 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: () => {} } }
)
}
⚠️ 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:
components/supa-table/API_ROUTES.md (installed with component)Option B: Manual Setup
See the complete documentation in API_ROUTES.md which includes:
Required routes:
app/api/supa-table/route.ts - Main data fetchingapp/api/supa-table/selection-ids/route.ts - "Select All Filtered" functionalityapp/api/supa-table/export-session/route.ts - Create export sessionsapp/api/supa-table/export/route.ts - Excel export generationWhy 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.
'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}
/>
)
}
| Prop | Type | Required | Description |
|---|---|---|---|
supabaseClient | SupabaseClient | Yes | Supabase client instance |
dbTable | string | Yes | Database table name |
tableFields | SupaTableFieldConfig[] | Yes | Column configuration |
rowIdAccessor | string | Yes | Field name for unique row ID |
dbQueryString | string | No | Custom Supabase select query (default: "*") |
enableExcelExport | boolean | No | Enable Excel export functionality |
enableSavedViews | boolean | No | Enable saved views functionality |
enableColumnVisibility | boolean | No | Enable column show/hide toggle |
enableRowSelection | boolean | No | Enable row selection checkboxes |
adminMode | boolean | No | Use service role client (bypasses RLS) |
globalFilters | GlobalFilter[] | No | Pre-applied filters from parent |
onSelectionChange | (ids: (string|number)[]) => void | No | Callback when selection changes |
customActions | React.ReactNode | No | Custom toolbar actions |
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"
}
}
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}
/>
const statusField: SupaTableFieldConfig = {
field: "status",
label: "Status",
type: "text",
valueMap: {
"active": "Active",
"inactive": "Inactive",
"pending": "Pending",
},
}
const priceField: SupaTableFieldConfig = {
field: "price",
label: "Price",
type: "number",
formattingInfo: { formatNumber: "currency" },
valueManipulation: (val) => val / 100, // Convert cents to dollars
}
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}
/>
SupaTable supports exporting large datasets (1000+ rows) efficiently:
Users can choose:
Users can save their current filter/sort configuration:
// Enable saved views
<SupaTable
enableSavedViews={true}
dbTable="users"
supabaseClient={supabase}
{...otherProps}
/>
Saved views are stored in user_table_views table and include:
<SupaTable
enableRowSelection={true}
onSelectionChange={(selectedIds) => {
console.log("Selected IDs:", selectedIds)
}}
{...otherProps}
/>
"Select All Filtered" functionality:
SupaTable requires 4 API routes for server-side operations:
app/api/supa-table/route.ts - Main data fetching endpointapp/api/supa-table/selection-ids/route.ts - Bulk selection supportapp/api/supa-table/export-session/route.ts - Export session managementapp/api/supa-table/export/route.ts - Excel export generationIMPORTANT: 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:
Quick Setup with Claude Code:
"Claude, read the API_ROUTES.md file and create all SupaTable API routes for me"
Manual Setup:
app/api/supa-table/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.
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
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();
Problem: Export times out or fails for 1000+ rows
Solution: SupaTable automatically uses session-based export for >500 rows. Ensure:
table_export_sessions tableapp/api/supa-table/export-session/route.ts is configuredProblem: Column filters don't affect data
Solution: Check that:
enableFiltering: true is set on the field configapp/api/supa-table/route.ts is properly configuredProblem: No data shows even though database has rows
Solution:
adminMode={true} for testing (bypasses RLS)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
CREATE INDEX idx_users_created_at ON users(created_at);
CREATE INDEX idx_users_status ON users(status);
dbQueryString wisely - Only select needed columns:dbQueryString="id, name, email, created_at" // Don't use * for large tables
// ✅ Good - Single level join
dbQueryString="*, user:users(name)"
// ❌ Slow - Multiple nested joins
dbQueryString="*, user:users(*, profile:profiles(*, company:companies(*)))"
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:
useMemo, React creates a new array reference on every renderIf you're migrating from the old GenericDataTable component:
// Old
import { TableWithDataLayer } from "@/components/Tables/GenericDataTable"
// New
import { SupaTable } from "@/components/supa-table"
// 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
/>
// Old
import { ITableDbFields } from "@/components/Tables/GenericDataTable"
// New
import { SupaTableFieldConfig } from "@/types/supa-table"
Found a bug or want to contribute? This component is part of the Waghubinger Registry.
pnpm registry:buildMIT License - Part of Waghubinger Registry
For issues or questions:
app/api/supa-table/lib/supabase/Version: 1.1.1 Last Updated: 2025-11-04 Registry: https://registry.tools.asscompact.at