# Querying and filtering data

> Read, filter, sort, paginate, join, insert, update, upsert and delete rows through the SupaHost Data API with supabase-js, with the equivalent REST calls and the 1000-row limit.

Source: https://supahost.app/docs/api/querying · Updated 2026-11-11

supabase-js builds Data API requests for you: `supabase.from('table')` starts a query, and each method you chain adds a filter, a sort or a limit. Nothing is sent until you `await` it. This page shows the patterns you'll use most, using a bookings app with `rooms`, `bookings` and `profiles` tables.

Every query runs as `anon` or as the signed-in user, so RLS policies filter the results before any of your filters apply.

## Selecting columns

```ts
// specific columns (smaller responses, less egress)
const { data, error } = await supabase.from('rooms').select('id, name, capacity')

// rename a column in the result
const { data: rooms } = await supabase.from('rooms').select('id, label:name')
```

Always check `error`. A failed query resolves with `{ data: null, error }`; it doesn't throw.

## Filters

```ts
const { data } = await supabase
  .from('bookings')
  .select('id, room_id, starts_at')
  .eq('room_id', 3)
  .gte('starts_at', '2026-11-01')
  .lt('starts_at', '2026-12-01')
```

| Method | SQL meaning |
|---|---|
| `.eq(col, v)` / `.neq(col, v)` | `=` / `<>` |
| `.gt` `.gte` `.lt` `.lte` | `>` `>=` `<` `<=` |
| `.in(col, [a, b])` | `in (a, b)` |
| `.like(col, '%x%')` / `.ilike(...)` | pattern match, case-sensitive / insensitive |
| `.is(col, null)` | `is null` (also `true` / `false`) |
| `.not(col, 'is', null)` | negate any filter |
| `.contains(col, v)` / `.containedBy(...)` | array or JSONB containment |
| `.textSearch(col, 'query')` | full-text search, see [Full-text search](/docs/database/full-text-search) |
| `.or('a.eq.1,b.eq.2')` | `a = 1 or b = 2` |

Chained filters are combined with AND. For OR, write the conditions in PostgREST syntax inside `.or()`:

```ts
const { data } = await supabase
  .from('rooms')
  .select('id, name')
  .or('capacity.gte.10,has_projector.is.true')
```

Inside `.or()`, values containing commas or brackets need double quotes. See [column does not exist when using or() filters](/kb/or-filter-column-error).

## Sorting and limiting

```ts
const { data } = await supabase
  .from('bookings')
  .select('id, starts_at')
  .order('starts_at', { ascending: false })
  .limit(10)
```

## Pagination and the 1000-row limit

A single request returns at most 1000 rows on SupaHost. Asking for more, or not asking for a limit at all, still gives you at most 1000, without an error. Page with `.range(from, to)`, which is zero-based and inclusive:

```ts
const pageSize = 100
const page = 2 // third page

const { data, count } = await supabase
  .from('bookings')
  .select('id, starts_at', { count: 'exact' })
  .order('id')
  .range(page * pageSize, page * pageSize + pageSize - 1)
```

`count: 'exact'` returns the total number of matching rows alongside the page. On large tables `'estimated'` is much cheaper. To only count, add `head: true` and no rows are returned.

For deep pagination on big tables, filter by the last id you saw instead of using large offsets:

```ts
const { data } = await supabase
  .from('bookings')
  .select('id, starts_at')
  .gt('id', lastSeenId)
  .order('id')
  .limit(100)
```

## Single rows

```ts
// exactly one row, or an error
const { data: room, error } = await supabase.from('rooms').select('*').eq('id', 3).single()

// zero or one row, no error when missing
const { data: maybe } = await supabase.from('profiles').select('*').eq('id', userId).maybeSingle()
```

## Joining related tables

If a foreign key links two tables, name the related table inside `select`:

```ts
// many-to-one: each booking with its room
const { data } = await supabase
  .from('bookings')
  .select('id, starts_at, room:rooms ( name, capacity )')

// one-to-many: each room with its bookings
const { data: busy } = await supabase
  .from('rooms')
  .select('name, bookings ( starts_at, ends_at )')
  .gte('bookings.starts_at', '2026-11-12')
```

A filter on the joined table (`bookings.starts_at`) filters the nested list, not the parent rows. To keep only parents that have matching children, use an inner join:

```ts
const { data } = await supabase
  .from('rooms')
  .select('name, bookings!inner ( starts_at )')
  .eq('bookings.user_id', userId)
```

When two foreign keys point at the same table, say which one: `profiles!bookings_user_id_fkey ( display_name )`. RLS applies to joined tables too, so a related row the user can't see comes back as `null` or is left out.

## Inserting, updating and deleting

```ts
// insert one or many; .select() returns the new rows
const { data: created, error } = await supabase
  .from('bookings')
  .insert({ room_id: 3, starts_at: '2026-11-20T09:00:00Z', ends_at: '2026-11-20T10:00:00Z' })
  .select()
  .single()

// update matching rows
await supabase.from('bookings').update({ room_id: 4 }).eq('id', created!.id)

// upsert on the primary key (or another unique column)
await supabase
  .from('profiles')
  .upsert({ id: userId, display_name: 'Lee' }, { onConflict: 'id' })

// delete
await supabase.from('bookings').delete().eq('id', created!.id)
```

Always give `update` and `delete` a filter. Without one, they target every row your policies allow. RLS still applies: if a policy hides a row, the update or delete simply affects zero rows, with no error. Insert and update also need a select policy if you chain `.select()`.

## The same requests over HTTP

Each supabase-js call maps to a REST request, which is useful from other languages or for debugging:

```bash
curl "https://app-one.acme.supahost.space/rest/v1/bookings?select=id,starts_at,room:rooms(name)&room_id=eq.3&order=starts_at.desc&limit=10" \
  -H "apikey: $SUPABASE_ANON_KEY" \
  -H "Authorization: Bearer $USER_ACCESS_TOKEN"
```

Filters are query parameters (`column=operator.value`), and pagination can also use the `Range: 0-99` header. See the [PostgREST API reference](https://postgrest.org/en/stable/references/api.html) and the [supabase-js reference](https://supabase.com/docs/reference/javascript/select).

## When a query doesn't fit

Aggregates across many rows, multi-step writes that must succeed together, or queries with window functions are easier as a database function you call with `.rpc()`. See [Calling database functions (RPC)](/docs/api/rpc) and [Operations too complex for the client library](/kb/complex-operations-rpc).

## Related

- [select returns an empty array but the table has data](/kb/empty-data-array)
- [JSON and JSONB columns](/docs/database/json)
- [Indexes](/docs/database/indexes)
