Lesson 5 / 25

Querying With supabase-js

Select, filter, join and write.

A chainable query builder

from('table') starts a query. select() takes a column list and can embed related tables through foreign keys, e.g. select('id, name, tasks(id, title)'). Filters chain: eq, neq, gt, lt, in, ilike, is, plus order, limit and range for paging. single() expects exactly one row. Writes use insert, update, upsert and delete; add .select() after a write to get the changed rows back. Always filter update and delete, and remember RLS still decides which rows are visible.

Reads and writes

Typical calls (supabase-js v2).

// nested select through the projects -> tasks foreign key
const { data: projects } = await supabase
  .from('projects')
  .select('id, name, tasks(id, title, done)')
  .order('created_at', { ascending: false })
  .limit(20)

// insert and return the new row
const { data: task, error } = await supabase
  .from('tasks')
  .insert({ project_id: 1, title: 'Write docs' })
  .select()
  .single()

// update with a filter
await supabase.from('tasks').update({ done: true }).eq('id', 42)

// upsert on a unique key, then delete
await supabase.from('tasks').upsert({ id: 42, project_id: 1, title: 'Docs v2' })
await supabase.from('tasks').delete().eq('id', 42)

Empty data can mean RLS, not a bug

If a query returns an empty array with no error, a policy may be hiding the rows. Check policies before suspecting the query.

Quick check: How do you return the inserted row from an insert?

  • Call .returnRow()
  • Chain .select() after .insert()
  • Inserts always return rows by default in v2
  • Run a second query with the service key
Answer

Chain .select() after .insert() — In v2 writes return no rows unless you select.