Whole table downloaded, filtered in the browser
What it looks like
const { data: all } = await supabase.from("invoices").select("*");
const mine = all.filter(i => i.user_id === user.id && i.status === "unpaid");
setInvoices(mine);What happens
Every user's device downloads far more than it shows, so pages slow down as the table grows, and if the database does not also restrict rows, other people's records land in the browser.
Where it shows up
Descriptive, not a verdict: builders and stacks whose defaults make this reachable, or that a public incident names. Each hub states the tool's own security features.
Check it yourself
Uses your builder's dashboard and your browser. No tools to install.
- Sign in to your app as a normal user, open a page that lists your own items, press F12, choose the Network tab, filter by "Fetch/XHR" and reload.
- Click the request that loads the list and open its Response tab.
- Compare what came back with what the screen shows: look for records with other people's names, emails or ids, and for
select=*with no filter such asuser_id=eq.in the request URL.
Bad result: the response contains rows that never appear on screen, especially rows belonging to other users, or far more rows than the page displays.
What we check
We diff the rows each authenticated role receives against the rows the screen renders, across every list view, and flag any field or row that is filtered only in the browser. Where other users' rows arrive, the finding is raised as a data-access issue as well.
Fix direction
Move the filter into the query (.eq('user_id', user.id), .eq('status', 'unpaid')) so the database does the work and sends only matching rows, and select only the needed columns. Keep row-level security on as well; Supabase enables it by default on tables created in the dashboard, so the query filter is for speed and the policy is for access.
Mechanism documented in: Supabase JavaScript reference: Using Filters (verified 2026-09-21); Supabase: Query Optimization (verified 2026-09-21).
Related patterns
- List endpoints that return everythingPerformanceMedium
As the table grows, the response gets slower and heavier until it hits a platform body-size or time limit and the page simply stops loading, usually for your biggest customer first.
- Row Level Security disabled on a tableData accessCritical
Anyone with the project URL and the public anon key, both of which sit in the app's JavaScript, can read, edit or delete every row in the table.
- Record ids accepted without an ownership checkData accessCritical
Anyone who changes the id in a URL or request can read, edit or delete another customer's record.
- One database query per list itemPerformanceMedium
Pages that were fast with test data slow to a crawl as real data grows, the database handles hundreds of tiny queries per page view, and timeouts and higher hosting bills follow.
Questions
is it ok to filter data on the client side
Filtering a short list the user already owns, such as sorting their own tasks, is fine. Downloading a whole table and then picking the current user's rows is not: it is slow, it grows without limit, and it relies on the browser to hide data it has already received. Put filters that decide what a user should see into the query and into a database policy.
why can i see other users data in the network tab
The query asks for every row and the code filters afterwards, so the full table travels to the browser and only the display is narrowed. If row-level security (RLS, the database rule that limits which rows each user can read) is off or too broad, those rows really are other users' data. Add the filter to the query and check the RLS policies on that table.
how do i filter rows in supabase instead of in javascript
Chain filters onto the query before it runs: .eq('user_id', user.id), .eq('status', 'unpaid'), .gte('created_at', since) and so on. Supabase's filter reference lists the operators, and the database applies them before sending anything. Selecting only the columns you need, instead of *, shrinks the response further.
Back to the catalogue, or run the self-check for your stack.