Return inserted row ID in Supabase JS
PostgreSQL in Production

Return inserted row ID in Supabase JS

If `data` is null after an insert, Supabase is doing exactly what the current docs say. You need to ask for the row back explicitly.

2026-06-06
5 min read
Return inserted row ID in Supabase JS

Return inserted row ID in Supabase JS#

The symptom is usually this:

ts
const { data, error } = await supabase
  .from('projects')
  .insert({ name: 'Alpha' })

console.log(data) // null

The root cause is not your table and not your primary key. The current Supabase docs say .insert(), .update(), .upsert(), and .delete() do not return modified rows by default.

If you want the inserted ID back, you must request it.

The exact fix#

ts
const { data, error } = await supabase
  .from('projects')
  .insert({ name: 'Alpha' })
  .select('id')
  .single()

if (error) throw error

console.log(data.id)

That is the whole move:

  • .insert(...) writes the row
  • .select('id') asks PostgREST to return the inserted row
  • .single() unwraps the single-row array into one object

If you want more than just the ID#

Ask for the other columns explicitly:

ts
const { data, error } = await supabase
  .from('projects')
  .insert({ name: 'Alpha' })
  .select('id, name, created_at')
  .single()

if (error) throw error

This is usually cleaner than doing a second query immediately after the insert.

If you are inserting multiple rows#

Do not use .single() then.

ts
const { data, error } = await supabase
  .from('projects')
  .insert([
    { name: 'Alpha' },
    { name: 'Beta' },
  ])
  .select('id, name')

if (error) throw error

console.log(data)

Now data is an array, which is what you want.

The common mistake#

People often write an insert, see data === null, and then assume they need a trigger, RPC, or a manual max(id) query. None of that is necessary for the ordinary case. The client just needs .select().

The small but important production rule#

Ask only for the columns you need:

ts
.select('id')

That keeps payloads smaller and makes the call's intent obvious in review.

Where this matters most#

This comes up constantly in:

  • create-and-redirect flows
  • dashboard actions that need the new record ID
  • nested UI state where you want the inserted row immediately
  • server actions that create a record and then navigate to it

If you are building those flows next, these pair well:

References#

Frequently Asked Questions

Have more questions? Contact us

Written by

Mahdi Br
Mahdi Br

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote

One email a month — no fluff

RLS gotchas, Next.js cache debugging, and the one Supabase setting that bit me last month.