The Supabase JavaScript client provides chainable filter methods for building precise queries without writing raw SQL. Use .eq() for exact matches, .gt()/.lt() for ranges, .like()/.ilike() for pattern matching, .in() for multiple values, and .contains() for arrays and JSON. Filters map directly to PostgREST query parameters, which translate to PostgreSQL WHERE clauses. Always combine filters with RLS policies to ensure users only access authorized data.
Building Filtered Queries with the Supabase JavaScript Client
Supabase's JavaScript client provides a fluent API for filtering data that maps to PostgREST query parameters under the hood. Each filter method translates to a PostgreSQL WHERE clause, giving you the full power of PostgreSQL without writing raw SQL. This tutorial covers every common filter pattern — from simple equality checks to complex multi-condition queries with OR logic and nested relationships.
Prerequisites
- A Supabase project with tables containing data
- The Supabase JS client installed and initialized
- RLS policies configured for your tables
- Basic understanding of SQL WHERE clauses
Step-by-step guide
Use equality and inequality filters
Use equality and inequality filters
The most common filters are .eq() for exact matches and .neq() for exclusion. These translate to WHERE column = value and WHERE column != value in SQL. You can chain multiple .eq() calls to filter on several columns simultaneously — they combine with AND logic by default.
1import { createClient } from '@supabase/supabase-js';23const supabase = createClient(4 process.env.NEXT_PUBLIC_SUPABASE_URL!,5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!6);78// Exact match9const { data: activeUsers } = await supabase10 .from('users')11 .select('id, name, email')12 .eq('status', 'active');1314// Not equal15const { data: nonAdmins } = await supabase16 .from('users')17 .select('id, name, role')18 .neq('role', 'admin');1920// Multiple equality filters (AND logic)21const { data: activeAdmins } = await supabase22 .from('users')23 .select('id, name')24 .eq('status', 'active')25 .eq('role', 'admin');Expected result: Queries return only rows that match all specified equality conditions.
Use range filters for numbers and dates
Use range filters for numbers and dates
Range filters let you query rows where a column value falls within a range. Use .gt() for greater than, .gte() for greater than or equal, .lt() for less than, and .lte() for less than or equal. These are especially useful for date ranges, price filters, and pagination by timestamp.
1// Numbers: products under $502const { data: affordable } = await supabase3 .from('products')4 .select('name, price')5 .lt('price', 50);67// Numbers: orders between $10 and $1008const { data: midRange } = await supabase9 .from('orders')10 .select('id, total')11 .gte('total', 10)12 .lte('total', 100);1314// Dates: posts from the last 7 days15const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();16const { data: recentPosts } = await supabase17 .from('posts')18 .select('id, title, created_at')19 .gte('created_at', weekAgo)20 .order('created_at', { ascending: false });Expected result: Queries return only rows where the specified columns fall within the given range.
Use pattern matching with like and ilike
Use pattern matching with like and ilike
Use .like() for case-sensitive pattern matching and .ilike() for case-insensitive matching. The % wildcard matches any sequence of characters, and _ matches a single character. These translate to SQL LIKE and ILIKE operators. For more complex text search, consider using .textSearch() with full-text search indexes.
1// Case-insensitive search for names containing 'john'2const { data: johns } = await supabase3 .from('users')4 .select('id, name, email')5 .ilike('name', '%john%');67// Case-sensitive: emails ending with @gmail.com8const { data: gmailUsers } = await supabase9 .from('users')10 .select('id, email')11 .like('email', '%@gmail.com');1213// Names starting with 'A'14const { data: aNames } = await supabase15 .from('users')16 .select('id, name')17 .ilike('name', 'A%');1819// Full-text search (requires tsvector column or function)20const { data: searchResults } = await supabase21 .from('articles')22 .select('id, title, body')23 .textSearch('title', 'supabase authentication');Expected result: Queries return rows where the text column matches the specified pattern, with or without case sensitivity.
Filter by multiple values with in and contains
Filter by multiple values with in and contains
Use .in() to match against a list of values (SQL IN operator). Use .contains() for array columns and JSONB containment queries. Use .containedBy() to check if an array column is a subset of the provided values. These are powerful filters for tagging systems, multi-select filters, and JSON data.
1// Match any of multiple values (SQL IN)2const { data: selectedOrders } = await supabase3 .from('orders')4 .select('*')5 .in('status', ['pending', 'processing', 'shipped']);67// Array column: posts that have the 'supabase' tag8const { data: taggedPosts } = await supabase9 .from('posts')10 .select('id, title, tags')11 .contains('tags', ['supabase']);1213// JSONB containment: products with specific metadata14const { data: electronics } = await supabase15 .from('products')16 .select('id, name, metadata')17 .contains('metadata', { category: 'electronics' });1819// Array is subset of values20const { data: basicPosts } = await supabase21 .from('posts')22 .select('id, title, tags')23 .containedBy('tags', ['supabase', 'postgres', 'react']);Expected result: Queries return rows matching any value in the list, or rows where array/JSONB columns contain the specified values.
Combine filters with OR logic
Combine filters with OR logic
By default, chained filters use AND logic. To use OR logic, pass a comma-separated string of conditions to the .or() method. Each condition uses the format column.operator.value. You can nest OR within AND by combining .or() with other filter methods.
1// OR: status is 'active' OR role is 'admin'2const { data: activeOrAdmin } = await supabase3 .from('users')4 .select('id, name, status, role')5 .or('status.eq.active,role.eq.admin');67// Combine AND and OR: active users who are admin OR moderator8const { data: activeStaff } = await supabase9 .from('users')10 .select('id, name, status, role')11 .eq('status', 'active')12 .or('role.eq.admin,role.eq.moderator');1314// OR with range: price < 10 OR price > 10015const { data: extremePrices } = await supabase16 .from('products')17 .select('id, name, price')18 .or('price.lt.10,price.gt.100');Expected result: Queries return rows matching any of the OR conditions, optionally combined with other AND filters.
Filter on related tables through foreign key joins
Filter on related tables through foreign key joins
When you use the nested select syntax to fetch related data, you can also filter on the related table's columns. Use the foreignTable option in filter methods to specify which related table to filter on. This translates to a filtered join in PostgREST.
1// Fetch orders with their items, only orders with quantity > 52const { data: largeOrders } = await supabase3 .from('orders')4 .select('id, total, order_items(product_name, quantity)')5 .gt('order_items.quantity', 5);67// Fetch users with their posts, filter posts by published status8const { data: usersWithPosts } = await supabase9 .from('users')10 .select('id, name, posts(id, title, status)')11 .eq('posts.status', 'published');1213// Combine filters on both parent and child tables14const { data: activeWithRecent } = await supabase15 .from('users')16 .select('id, name, posts(id, title, created_at)')17 .eq('status', 'active')18 .gte('posts.created_at', weekAgo);Expected result: Related data is filtered within the nested response. Parent rows still appear even if no child rows match the filter.
Complete working example
1// Complete Supabase filter examples2// Demonstrates all common filter patterns34import { createClient } from '@supabase/supabase-js';56const supabase = createClient(7 process.env.NEXT_PUBLIC_SUPABASE_URL!,8 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!9);1011// Type-safe query builder examples12interface Product {13 id: number;14 name: string;15 price: number;16 category: string;17 tags: string[];18 metadata: Record<string, unknown>;19 created_at: string;20}2122// Equality filter23async function getByCategory(category: string) {24 return supabase.from('products').select('*').eq('category', category);25}2627// Range filter28async function getPriceRange(min: number, max: number) {29 return supabase.from('products').select('*').gte('price', min).lte('price', max);30}3132// Pattern matching33async function searchProducts(term: string) {34 return supabase.from('products').select('*').ilike('name', `%${term}%`);35}3637// Multiple values38async function getByCategories(categories: string[]) {39 return supabase.from('products').select('*').in('category', categories);40}4142// Array containment43async function getByTag(tag: string) {44 return supabase.from('products').select('*').contains('tags', [tag]);45}4647// OR conditions48async function getCheapOrExpensive() {49 return supabase.from('products').select('*').or('price.lt.10,price.gt.1000');50}5152// Null checking53async function getWithoutCategory() {54 return supabase.from('products').select('*').is('category', null);55}5657// Not null58async function getWithCategory() {59 return supabase.from('products').select('*').not('category', 'is', null);60}6162// Combined query with ordering and pagination63async function getFilteredProducts(64 category: string,65 maxPrice: number,66 page: number,67 pageSize: number68) {69 const start = page * pageSize;70 const end = start + pageSize - 1;7172 return supabase73 .from('products')74 .select('id, name, price, category', { count: 'exact' })75 .eq('category', category)76 .lte('price', maxPrice)77 .order('price', { ascending: true })78 .range(start, end);79}8081export {82 getByCategory,83 getPriceRange,84 searchProducts,85 getByCategories,86 getByTag,87 getCheapOrExpensive,88 getWithoutCategory,89 getWithCategory,90 getFilteredProducts,91};Common mistakes when querying with Filters in Supabase
Why it's a problem: Using .like() with a leading wildcard ('%term') on a non-indexed column, causing full table scans on large tables
How to avoid: For search functionality, use full-text search with a GIN index instead of .ilike('%term%'). Or use a trigram index: CREATE EXTENSION pg_trgm; CREATE INDEX idx_name_trgm ON users USING gin (name gin_trgm_ops);
Why it's a problem: Assuming .or() takes individual method calls instead of a comma-separated string
How to avoid: The .or() method takes a single string with PostgREST syntax: .or('status.eq.active,role.eq.admin'). It does not support chaining method calls.
Why it's a problem: Filtering on a related table and expecting parent rows to be excluded when no child rows match
How to avoid: Filtering on a related table only filters the nested data, not the parent rows. To exclude parents without matching children, use an RPC function or a database view.
Why it's a problem: Forgetting to add RLS SELECT policies when filters return empty results on a table with data
How to avoid: Check that RLS policies exist for the table and role you are querying with. Empty results with no error usually means RLS is blocking access.
Best practices
- Use specific column selection (.select('id, name')) instead of .select('*') to reduce payload size
- Add database indexes on columns that appear frequently in filters, especially for large tables
- Combine filters with .order() and .range() for efficient paginated queries
- Use .ilike() instead of .like() for user-facing search to handle case insensitivity
- Use .in() instead of multiple .or('column.eq.value') when matching against a list of values
- Always handle the error property from query results to catch RLS or permission issues
- Use { count: 'exact' } in .select() when you need total row counts for pagination UI
- For complex filtering logic, consider creating a database function and calling it with .rpc()
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I need to build a product search page with Supabase that supports filtering by category, price range, text search, and tags. Show me how to use the Supabase JS client filters including eq, gte, lte, ilike, in, contains, and or, with pagination and sorting.
Create a set of reusable query functions for a Supabase products table that support filtering by category (eq), price range (gte/lte), text search (ilike), tags (contains), and multiple values (in). Include pagination with range() and count, plus OR conditions for complex filters.
Frequently asked questions
Can I combine AND and OR filters in the same query?
Yes. Chained filter methods (eq, gt, etc.) use AND logic by default. Use the .or() method for OR conditions. You can combine both: .eq('status', 'active').or('role.eq.admin,role.eq.moderator') means status is active AND (role is admin OR moderator).
How do I filter for NULL values?
Use .is('column', null) to find rows where a column is NULL, and .not('column', 'is', null) to find rows where a column is NOT NULL. Do not use .eq('column', null) — it will not work.
Why does my filter return an empty array instead of an error?
This usually means RLS is blocking access. When RLS is enabled with no matching SELECT policy, queries return empty results silently. Check your RLS policies in the Dashboard.
Can I filter on JSONB nested fields?
Yes. Use the arrow syntax in the column name: .eq('metadata->category', 'electronics') for exact matches on nested JSON fields. For containment queries, use .contains('metadata', { category: 'electronics' }).
Is there a limit to how many filters I can chain?
There is no hard limit on the number of filters, but very complex queries with many filters may hit URL length limits (since filters are passed as query parameters). For extremely complex queries, consider using an RPC function.
How do I filter on a column in a related table?
Use the dot notation: .eq('related_table.column', 'value'). This filters which related rows are included in the nested response, but does not exclude parent rows that have no matching children.
Can RapidDev help build complex filtered queries for my Supabase application?
Yes. RapidDev can design optimized query patterns for your Supabase application including complex filtering, full-text search, pagination, and performance tuning with proper indexes.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation