Skip to main content
RapidDev - Software Development Agency
supabase-tutorial

How to Update Records in Supabase

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.

What you'll learn

  • How to update single and multiple records using the Supabase JS client
  • How to write RLS policies that permit UPDATE operations
  • How to use filters to target the correct rows for update
  • How to handle update errors and verify results
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner8 min read10-15 minSupabase (all plans), @supabase/supabase-js v2+March 2026RapidDev Engineering Team
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.

Prerequisites

  • 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.

typescript
1import { createClient } from '@supabase/supabase-js'
2
3const supabase = createClient(
4 process.env.NEXT_PUBLIC_SUPABASE_URL!,
5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
6)

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.

typescript
1-- Enable RLS on the table
2alter table public.todos enable row level security;
3
4-- Allow users to read their own rows
5create policy "Users can view own todos"
6 on public.todos for select
7 to authenticated
8 using ((select auth.uid()) = user_id);
9
10-- Allow users to update their own rows
11create policy "Users can update own todos"
12 on public.todos for update
13 to authenticated
14 using ((select auth.uid()) = user_id)
15 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.

typescript
1const { data, error } = await supabase
2 .from('todos')
3 .update({ title: 'Buy groceries', is_complete: true })
4 .eq('id', 1)
5 .select()
6
7if (error) {
8 console.error('Update failed:', error.message)
9} else {
10 console.log('Updated row:', data)
11}

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.

typescript
1const { data, error } = await supabase
2 .from('todos')
3 .update({ is_complete: true })
4 .eq('is_complete', false)
5 .select()
6
7if (error) {
8 console.error('Bulk update failed:', error.message)
9} else {
10 console.log(`Updated ${data.length} rows`)
11}

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.

typescript
1const { data, error } = await supabase
2 .from('todos')
3 .upsert({
4 id: 1,
5 title: 'Buy groceries',
6 is_complete: false,
7 user_id: 'some-user-uuid'
8 })
9 .select()
10
11if (error) {
12 console.error('Upsert failed:', error.message)
13} else {
14 console.log('Upserted row:', data)
15}

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.

typescript
1async function updateTodo(id: number, updates: Record<string, unknown>) {
2 const { data, error } = await supabase
3 .from('todos')
4 .update(updates)
5 .eq('id', id)
6 .select()
7 .single()
8
9 if (error) {
10 throw new Error(`Update failed: ${error.message}`)
11 }
12
13 if (!data) {
14 throw new Error('No row found or RLS policy blocked the update')
15 }
16
17 return data
18}

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 working example

update-records.ts
1import { createClient } from '@supabase/supabase-js'
2
3const supabase = createClient(
4 process.env.NEXT_PUBLIC_SUPABASE_URL!,
5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
6)
7
8// Update a single record by ID
9async function updateTodo(id: number, updates: Record<string, unknown>) {
10 const { data, error } = await supabase
11 .from('todos')
12 .update(updates)
13 .eq('id', id)
14 .select()
15 .single()
16
17 if (error) {
18 throw new Error(`Update failed: ${error.message}`)
19 }
20 if (!data) {
21 throw new Error('No row found or RLS policy blocked the update')
22 }
23 return data
24}
25
26// Update multiple records matching a filter
27async function markAllComplete(userId: string) {
28 const { data, error } = await supabase
29 .from('todos')
30 .update({ is_complete: true })
31 .eq('user_id', userId)
32 .eq('is_complete', false)
33 .select()
34
35 if (error) {
36 throw new Error(`Bulk update failed: ${error.message}`)
37 }
38 return data
39}
40
41// Upsert: insert or update based on primary key
42async function upsertTodo(todo: {
43 id?: number
44 title: string
45 is_complete: boolean
46 user_id: string
47}) {
48 const { data, error } = await supabase
49 .from('todos')
50 .upsert(todo)
51 .select()
52 .single()
53
54 if (error) {
55 throw new Error(`Upsert failed: ${error.message}`)
56 }
57 return data
58}
59
60// Usage examples
61const updated = await updateTodo(1, { title: 'Updated title' })
62console.log('Updated:', updated)
63
64const bulkResult = await markAllComplete('user-uuid-here')
65console.log(`Marked ${bulkResult.length} todos complete`)

Common mistakes when updating Records in Supabase

Why it's a problem: Calling .update() without any filter, which attempts to update every row in the table

How to avoid: Always chain a filter like .eq(), .in(), or .match() after .update() to target specific rows. Supabase will reject unfiltered updates by default.

Why it's a problem: Forgetting to create an UPDATE RLS policy, causing updates to silently return empty results

How to avoid: 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.

Why it's a problem: Using the service role key in client-side code to bypass RLS instead of writing proper policies

How to avoid: 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.

Why it's a problem: Not checking the returned data array length after an update, missing cases where zero rows were affected

How to avoid: 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

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I have a Supabase table called 'todos' with columns id, title, is_complete, and user_id. Write TypeScript code using @supabase/supabase-js to update a single todo by ID and also bulk-update all incomplete todos. Include error handling and RLS policies.

Supabase Prompt

Help me update records in my Supabase todos table. I need to change the title and is_complete status for a specific row by ID. Also show me the RLS policy I need so authenticated users can only update their own rows.

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.