Supabase
The grid's server fetch contract with Supabase, using Supabase.from().select() plus range as the shortest path.
Recipe
Recipe: not a shadcn registry example. It requires a server.
Supabase's PostgREST API already does filtering, ordering, and pagination, so the handler is mostly translation:
// app/api/grid/route.ts
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!, // server-side: bypass row-level security
);
export async function POST(req: Request) {
const { page, pageSize, sorts, search } = await req.json();
let query = supabase.from("users").select("*", { count: "exact" });
if (search) {
query = query.or(`name.ilike.%${search}%,email.ilike.%${search}%`);
}
for (const s of sorts) {
query = query.order(s.columnId, { ascending: s.direction === "asc" });
}
const { data: rows, count: total, error } = await query.range(
(page - 1) * pageSize,
page * pageSize - 1,
);
if (error) throw new Error(error.message);
return Response.json({ rows, total });
}Notes:
range(from, to)is inclusive on both ends, hence the- 1.- Column filters map to PostgREST operators one-to-one:
eq,neq,gt,gte,lt,lte,like,in.(...)(for example,query = query.eq("score", f.value)forequals). SUPABASE_SERVICE_ROLE_KEYkeeps the grid's requests server-side. With the anon key, you also need a permissive RLS policy onusersfor reads.- The client is the server-side example unchanged. Edits
become
supabase.from("users").update(...)in the write-through handler.
Shareable state
Same as the other recipes: DataGridUrlState moves the view state to the URL, and this route reads the same values from the query string.