# How to Update Records in Supabase

- Tool: Supabase
- Difficulty: Beginner
- Time required: 10-15 min
- Compatibility: Supabase (all plans), @supabase/supabase-js v2+
- Last updated: March 2026

## TL;DR

To update records in Supabase, use the JavaScript client method supabase.from('table').update({ column: value }).eq('id', recordId). You must include a filter like .eq() to target specific rows. Make sure your table has RLS policies that allow UPDATE operations for the authenticated user, or the update will silently return no rows.

## Updating Records in Supabase with the JavaScript Client

This tutorial walks you through updating existing records in a Supabase table using the JavaScript client library. You will learn how to perform partial updates on single rows, bulk updates on filtered sets, and how to configure Row Level Security policies so that authenticated users can modify their own data. By the end, you will have a working update flow with proper error handling.

## Before you start

- A Supabase project with at least one table containing data
- The @supabase/supabase-js library installed in your project
- Your Supabase URL and anon key available as environment variables
- Basic understanding of JavaScript or TypeScript

## Step-by-step guide

### 1. Initialize the Supabase client

Before performing any database operations, you need to create a Supabase client instance. Import createClient from the supabase-js library and pass your project URL and anon key. The anon key is safe to use in client-side code because it respects Row Level Security policies. Never use the service role key in the browser.

```
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
```

**Expected result:** A configured Supabase client instance ready to make authenticated requests.

### 2. Create an RLS policy that allows updates

Row Level Security must be enabled on your table, and you need an explicit UPDATE policy. Without one, update calls will silently return zero rows — no error is thrown. The policy below allows authenticated users to update only their own rows by comparing auth.uid() to the user_id column. You also need a matching SELECT policy because Supabase must read the row before updating it.

```
-- Enable RLS on the table
alter table public.todos enable row level security;

-- Allow users to read their own rows
create policy "Users can view own todos"
  on public.todos for select
  to authenticated
  using ((select auth.uid()) = user_id);

-- Allow users to update their own rows
create policy "Users can update own todos"
  on public.todos for update
  to authenticated
  using ((select auth.uid()) = user_id)
  with check ((select auth.uid()) = user_id);
```

**Expected result:** RLS is enabled and authenticated users can read and update their own rows in the todos table.

### 3. Update a single record by ID

Use the .update() method followed by a filter to target one specific row. The update method accepts an object containing only the columns you want to change — unchanged columns are left alone (partial update). Always chain a filter like .eq() to avoid updating every row in the table. Add .select() at the end if you want the updated row returned in the response.

```
const { data, error } = await supabase
  .from('todos')
  .update({ title: 'Buy groceries', is_complete: true })
  .eq('id', 1)
  .select()

if (error) {
  console.error('Update failed:', error.message)
} else {
  console.log('Updated row:', data)
}
```

**Expected result:** The row with id 1 is updated and the response contains the modified record with the new title and is_complete values.

### 4. Update multiple records with a filter

You can update multiple rows at once by using a broader filter. For example, to mark all incomplete todos as complete for the current user, filter by is_complete equals false. The update applies to every row that matches the filter and passes the RLS policy. This is useful for bulk status changes, batch processing, or soft-delete operations.

```
const { data, error } = await supabase
  .from('todos')
  .update({ is_complete: true })
  .eq('is_complete', false)
  .select()

if (error) {
  console.error('Bulk update failed:', error.message)
} else {
  console.log(`Updated ${data.length} rows`)
}
```

**Expected result:** All rows where is_complete was false are now set to true. The response includes all updated rows.

### 5. Use upsert for insert-or-update logic

When you want to insert a new row if it does not exist, or update it if it does, use .upsert() instead of .update(). Upsert checks for conflicts on the primary key or a unique constraint. If a matching row exists, it updates; if not, it inserts. This is particularly useful for syncing external data or handling forms where the user might be creating or editing a record.

```
const { data, error } = await supabase
  .from('todos')
  .upsert({
    id: 1,
    title: 'Buy groceries',
    is_complete: false,
    user_id: 'some-user-uuid'
  })
  .select()

if (error) {
  console.error('Upsert failed:', error.message)
} else {
  console.log('Upserted row:', data)
}
```

**Expected result:** If a row with id 1 exists, it is updated. If not, a new row is inserted. The response includes the resulting row.

### 6. Handle errors and verify the update

Always check both the error object and the returned data. A null error with an empty data array means the row was not found or RLS blocked the operation. Implement proper error handling to surface issues to the user. You can also verify updates by re-fetching the row or using the .select() chain to get the updated record in the same call.

```
async function updateTodo(id: number, updates: Record<string, unknown>) {
  const { data, error } = await supabase
    .from('todos')
    .update(updates)
    .eq('id', id)
    .select()
    .single()

  if (error) {
    throw new Error(`Update failed: ${error.message}`)
  }

  if (!data) {
    throw new Error('No row found or RLS policy blocked the update')
  }

  return data
}
```

**Expected result:** The function returns the updated row object, or throws a descriptive error if the update fails or no matching row is found.

## Complete code example

File: `update-records.ts`

```typescript
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

// Update a single record by ID
async function updateTodo(id: number, updates: Record<string, unknown>) {
  const { data, error } = await supabase
    .from('todos')
    .update(updates)
    .eq('id', id)
    .select()
    .single()

  if (error) {
    throw new Error(`Update failed: ${error.message}`)
  }
  if (!data) {
    throw new Error('No row found or RLS policy blocked the update')
  }
  return data
}

// Update multiple records matching a filter
async function markAllComplete(userId: string) {
  const { data, error } = await supabase
    .from('todos')
    .update({ is_complete: true })
    .eq('user_id', userId)
    .eq('is_complete', false)
    .select()

  if (error) {
    throw new Error(`Bulk update failed: ${error.message}`)
  }
  return data
}

// Upsert: insert or update based on primary key
async function upsertTodo(todo: {
  id?: number
  title: string
  is_complete: boolean
  user_id: string
}) {
  const { data, error } = await supabase
    .from('todos')
    .upsert(todo)
    .select()
    .single()

  if (error) {
    throw new Error(`Upsert failed: ${error.message}`)
  }
  return data
}

// Usage examples
const updated = await updateTodo(1, { title: 'Updated title' })
console.log('Updated:', updated)

const bulkResult = await markAllComplete('user-uuid-here')
console.log(`Marked ${bulkResult.length} todos complete`)
```

## Common mistakes

- **Calling .update() without any filter, which attempts to update every row in the table** — undefined Fix: Always chain a filter like .eq(), .in(), or .match() after .update() to target specific rows. Supabase will reject unfiltered updates by default.
- **Forgetting to create an UPDATE RLS policy, causing updates to silently return empty results** — undefined Fix: Create an explicit UPDATE policy on the table. Remember that UPDATE also requires a matching SELECT policy so Supabase can read the row before modifying it.
- **Using the service role key in client-side code to bypass RLS instead of writing proper policies** — undefined Fix: Always use the anon key in browsers and write proper RLS policies. The service role key bypasses all security and must only be used in server-side code.
- **Not checking the returned data array length after an update, missing cases where zero rows were affected** — undefined Fix: Always verify that data is not empty after an update. An empty array with no error means the filter matched no rows or RLS blocked the operation.

## Best practices

- Always include a filter with .update() to avoid accidentally modifying unintended rows
- Chain .select() after .update() to get the modified row back in the same request
- Write both SELECT and UPDATE RLS policies — UPDATE operations need to read rows first
- Wrap auth.uid() in a select subquery in RLS policies for better per-statement caching
- Use .single() when updating one specific row to get an object instead of an array
- Use .upsert() with onConflict when you need insert-or-update behavior based on a unique constraint
- Add an index on columns used in your update filters (like user_id) for faster query performance
- Log update errors server-side for debugging — client errors may not show the full picture

## Frequently asked questions

### Why does my Supabase update return an empty array with no error?

This almost always means the row was not found by your filter, or an RLS policy is blocking the update. Check that the authenticated user has an UPDATE policy on the table and that the filter conditions match an existing row.

### Can I update multiple columns in a single call?

Yes. Pass an object with all the columns you want to change to the .update() method. Only the columns you include will be modified — all other columns remain unchanged.

### What is the difference between update and upsert in Supabase?

The .update() method only modifies existing rows and returns nothing if no match is found. The .upsert() method inserts a new row if no matching primary key or unique constraint exists, or updates the existing row if it does.

### Do I need both a SELECT and UPDATE RLS policy?

Yes. Supabase needs to read the row before updating it, so a SELECT policy is required alongside the UPDATE policy. Without both, the update will silently return no results.

### How do I update a record without knowing the user ID?

If your RLS policy uses auth.uid() to match the user_id column, you only need to filter by the row's primary key. The RLS policy automatically ensures the user can only update their own rows.

### Can I use .update() to set a column to null?

Yes. Pass null as the value for the column: .update({ description: null }). This sets the column to NULL in the database, as long as the column allows null values.

### Can RapidDev help me build a Supabase backend with complex update logic?

Yes. RapidDev specializes in building production-ready backends with Supabase, including complex update flows, RLS policies, and server-side validation using Edge Functions.

---

Source: https://www.rapidevelopers.com/supabase-tutorial/how-to-update-records-in-supabase
© RapidDev — https://www.rapidevelopers.com/supabase-tutorial/how-to-update-records-in-supabase
