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

How to Delete Records in Supabase

To delete records in Supabase, use the JavaScript client method supabase.from('table').delete().eq('column', value). Always include a filter like .eq() or .in() to target specific rows — calling .delete() without a filter will attempt to delete all rows. You must have RLS delete policies enabled for the operation to succeed through the API, and cascading deletes require ON DELETE CASCADE on foreign keys.

What you'll learn

  • How to delete single and multiple records using the Supabase JS client
  • How to write RLS policies that allow authenticated users to delete their own data
  • How to configure cascading deletes with foreign key constraints
  • How to soft-delete records instead of permanently removing them
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner9 min read10-15 minSupabase (all plans), @supabase/supabase-js v2+March 2026RapidDev Engineering Team
TL;DR

To delete records in Supabase, use the JavaScript client method supabase.from('table').delete().eq('column', value). Always include a filter like .eq() or .in() to target specific rows — calling .delete() without a filter will attempt to delete all rows. You must have RLS delete policies enabled for the operation to succeed through the API, and cascading deletes require ON DELETE CASCADE on foreign keys.

Deleting Records from Supabase Tables

Deleting data is a fundamental database operation, but it requires extra care in Supabase because Row Level Security policies control who can delete what. This tutorial covers deleting single rows, bulk deleting with filters, cascading deletes through foreign keys, and implementing soft-delete patterns. You will learn both the JavaScript client approach and the raw SQL approach so you can choose the right tool for each situation.

Prerequisites

  • A Supabase project with at least one table containing data
  • The Supabase JS client installed (@supabase/supabase-js v2)
  • RLS enabled on your tables with appropriate policies
  • Basic understanding of SQL WHERE clauses and filters

Step-by-step guide

1

Delete a single record by ID

The most common delete operation targets a single row by its primary key. Use supabase.from('table').delete().eq('id', value) to delete exactly one row. The .eq() filter ensures only the matching row is removed. The delete method returns the deleted rows by default if you chain .select() after it. Without .select(), it returns only a count. If no rows match the filter, no error is thrown — the operation succeeds with zero affected rows.

typescript
1import { supabase } from '@/lib/supabase'
2
3// Delete a single task by ID
4const { error } = await supabase
5 .from('tasks')
6 .delete()
7 .eq('id', 42)
8
9if (error) {
10 console.error('Delete failed:', error.message)
11} else {
12 console.log('Task deleted successfully')
13}
14
15// Delete and return the deleted row
16const { data, error: err } = await supabase
17 .from('tasks')
18 .delete()
19 .eq('id', 42)
20 .select()
21
22console.log('Deleted row:', data)

Expected result: The row with the matching ID is removed from the table. If you chain .select(), the deleted row data is returned.

2

Delete multiple records with filters

You can delete multiple rows at once by using filters that match more than one row. Use .in() for a list of IDs, .lt() / .gt() for ranges, or .eq() on a non-unique column. Be very careful with bulk deletes — always run a SELECT with the same filter first to preview which rows will be affected. There is no undo for deletes unless you have point-in-time recovery enabled (Pro plan).

typescript
1// Delete multiple tasks by ID
2const { error } = await supabase
3 .from('tasks')
4 .delete()
5 .in('id', [10, 11, 12, 13])
6
7// Delete all completed tasks for the current user
8const { data: { user } } = await supabase.auth.getUser()
9const { error: bulkError } = await supabase
10 .from('tasks')
11 .delete()
12 .eq('user_id', user.id)
13 .eq('completed', true)
14
15// Delete old records (older than 30 days)
16const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString()
17const { error: cleanupError } = await supabase
18 .from('logs')
19 .delete()
20 .lt('created_at', thirtyDaysAgo)

Expected result: All rows matching the combined filter conditions are deleted from the table.

3

Create RLS policies that allow deletion

Row Level Security must explicitly allow DELETE operations. Without a delete policy, the API returns zero affected rows (not an error) when attempting to delete. The policy uses a USING clause to define which existing rows the user is allowed to delete. A common pattern is restricting users to deleting only rows they own by checking auth.uid() against a user_id column. You need a corresponding SELECT policy too, because Supabase must first find the rows before deleting them.

typescript
1-- Enable RLS (if not already enabled)
2ALTER TABLE public.tasks ENABLE ROW LEVEL SECURITY;
3
4-- Allow users to delete their own tasks
5CREATE POLICY "Users can delete own tasks"
6 ON public.tasks
7 FOR DELETE
8 TO authenticated
9 USING ((SELECT auth.uid()) = user_id);
10
11-- You also need a SELECT policy for delete to find rows
12CREATE POLICY "Users can view own tasks"
13 ON public.tasks
14 FOR SELECT
15 TO authenticated
16 USING ((SELECT auth.uid()) = user_id);

Expected result: Authenticated users can delete only the rows they own. Attempts to delete other users' rows silently return zero affected rows.

4

Set up cascading deletes with foreign keys

When a parent record is deleted, you usually want related child records to be deleted automatically. Configure this with ON DELETE CASCADE on the foreign key constraint. Without it, trying to delete a parent row that has children will fail with a foreign key violation error. You can set this when creating the table or alter an existing foreign key. The cascade happens at the database level, so RLS policies on the child table are not checked during cascading deletes.

typescript
1-- Create a table with cascading delete on the foreign key
2CREATE TABLE public.comments (
3 id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
4 task_id bigint REFERENCES public.tasks(id) ON DELETE CASCADE,
5 user_id uuid REFERENCES auth.users(id) ON DELETE CASCADE,
6 body text NOT NULL,
7 created_at timestamptz DEFAULT now()
8);
9
10-- To change an existing foreign key to cascade:
11-- 1. Drop the old constraint
12ALTER TABLE public.comments
13 DROP CONSTRAINT comments_task_id_fkey;
14
15-- 2. Add it back with CASCADE
16ALTER TABLE public.comments
17 ADD CONSTRAINT comments_task_id_fkey
18 FOREIGN KEY (task_id) REFERENCES public.tasks(id) ON DELETE CASCADE;

Expected result: Deleting a task automatically deletes all its related comments. No orphan records remain.

5

Implement soft-delete instead of permanent deletion

In many applications, you want to mark records as deleted without actually removing them from the database. This allows recovery and audit trails. Add a deleted_at timestamp column that is null for active records. Update your queries to filter out soft-deleted records, and modify your RLS policies to only show non-deleted rows. When a user deletes a record, set deleted_at to the current timestamp instead of calling .delete().

typescript
1-- Add soft-delete column to existing table
2ALTER TABLE public.tasks
3 ADD COLUMN deleted_at timestamptz DEFAULT null;
4
5-- Update RLS policy to hide soft-deleted rows
6DROP POLICY IF EXISTS "Users can view own tasks" ON public.tasks;
7CREATE POLICY "Users can view own active tasks"
8 ON public.tasks FOR SELECT
9 TO authenticated
10 USING ((SELECT auth.uid()) = user_id AND deleted_at IS NULL);
11
12-- Soft-delete from the JS client (UPDATE, not DELETE)
13-- const { error } = await supabase
14-- .from('tasks')
15-- .update({ deleted_at: new Date().toISOString() })
16-- .eq('id', 42)

Expected result: Deleted records are hidden from normal queries but remain in the database for recovery. The deleted_at column records when the deletion occurred.

Complete working example

delete-records.ts
1// Complete example: delete operations in Supabase
2import { createClient } from '@supabase/supabase-js'
3
4const supabase = createClient(
5 process.env.NEXT_PUBLIC_SUPABASE_URL!,
6 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
7)
8
9// 1. Delete a single record by ID
10async function deleteTask(taskId: number) {
11 const { error } = await supabase
12 .from('tasks')
13 .delete()
14 .eq('id', taskId)
15
16 if (error) throw new Error(`Delete failed: ${error.message}`)
17 return true
18}
19
20// 2. Bulk delete with filter
21async function deleteCompletedTasks(userId: string) {
22 const { data, error } = await supabase
23 .from('tasks')
24 .delete()
25 .eq('user_id', userId)
26 .eq('completed', true)
27 .select('id')
28
29 if (error) throw new Error(`Bulk delete failed: ${error.message}`)
30 return data?.length ?? 0
31}
32
33// 3. Soft-delete (mark as deleted without removing)
34async function softDeleteTask(taskId: number) {
35 const { error } = await supabase
36 .from('tasks')
37 .update({ deleted_at: new Date().toISOString() })
38 .eq('id', taskId)
39
40 if (error) throw new Error(`Soft delete failed: ${error.message}`)
41 return true
42}
43
44// 4. Restore a soft-deleted record
45async function restoreTask(taskId: number) {
46 const { error } = await supabase
47 .from('tasks')
48 .update({ deleted_at: null })
49 .eq('id', taskId)
50
51 if (error) throw new Error(`Restore failed: ${error.message}`)
52 return true
53}
54
55// 5. Preview before delete (safety check)
56async function previewDelete(userId: string) {
57 const { data, error } = await supabase
58 .from('tasks')
59 .select('id, title, completed')
60 .eq('user_id', userId)
61 .eq('completed', true)
62
63 if (error) throw new Error(`Preview failed: ${error.message}`)
64 console.log(`${data.length} tasks will be deleted:`, data)
65 return data
66}

Common mistakes when deleting Records in Supabase

Why it's a problem: Calling .delete() without any filter, which attempts to delete all rows in the table

How to avoid: Always chain a filter like .eq(), .in(), or .lt() after .delete(). The Supabase client does allow unfiltered deletes, so this is your responsibility to prevent.

Why it's a problem: Delete operation returns zero rows affected even though matching rows exist

How to avoid: This is almost always a missing RLS DELETE policy. Create a policy with a USING clause that matches the rows the user should be able to delete. You also need a SELECT policy.

Why it's a problem: Foreign key violation error when deleting a parent record that has child records

How to avoid: Add ON DELETE CASCADE to the foreign key constraint, or delete child records first. Use ALTER TABLE to modify the existing constraint.

Best practices

  • Always chain a filter (.eq, .in, .lt) after .delete() to prevent accidental bulk deletion of all rows
  • Run a SELECT query with the same filters before executing a DELETE to preview which rows will be affected
  • Implement soft-delete for user-facing data so records can be recovered if deleted by mistake
  • Use ON DELETE CASCADE on foreign keys to prevent orphan records when parent rows are deleted
  • Create explicit RLS DELETE policies — do not rely on ALL policies which may be too permissive
  • Add an index on the user_id column used in RLS policies to keep delete operations fast
  • Log delete operations to an audit table if you need to track who deleted what and when
  • Enable point-in-time recovery (Pro plan) as a safety net for accidental data deletion

Still stuck?

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

ChatGPT Prompt

I need to delete records from a Supabase table using the JavaScript client. Show me how to delete a single row by ID, bulk delete with filters, set up RLS delete policies, and implement soft-delete with a deleted_at column. Include the SQL for the table schema and RLS policies.

Supabase Prompt

Create a tasks table with RLS enabled and a delete policy that lets users delete only their own tasks. Show the JS client code for single delete, bulk delete of completed tasks, and a soft-delete implementation with a deleted_at column.

Frequently asked questions

What happens if I call .delete() without any filter?

The Supabase client will attempt to delete all rows in the table. RLS policies may prevent this, but if the policy allows it (e.g., a permissive policy for authenticated users), all rows matching the policy will be deleted. Always chain a filter after .delete().

Why does my delete return success but no rows are actually deleted?

This is almost always caused by a missing or incorrect RLS DELETE policy. When RLS blocks the operation, Supabase returns success with zero affected rows instead of an error. Check your policies in the Dashboard under Authentication > Policies.

Can I undo a delete in Supabase?

Permanent deletes cannot be undone unless you have point-in-time recovery enabled (Pro plan) or a recent backup. This is why soft-delete patterns (using a deleted_at column) are recommended for user-facing data.

How do cascading deletes interact with RLS?

Cascading deletes triggered by a foreign key constraint bypass RLS policies on the child table. The cascade happens at the database level, not through the API. Only the initial delete on the parent table is subject to RLS.

Can I delete records from the Supabase Dashboard without writing code?

Yes. Open the Table Editor in the Dashboard, find the row you want to delete, and click the trash icon. You can also run DELETE SQL statements in the SQL Editor. Both methods bypass RLS because they use the service role.

Is there a rate limit on delete operations?

Supabase does not have a specific rate limit on DELETE operations, but the general API rate limits apply (based on your plan). For bulk deletes of thousands of rows, consider using the SQL Editor or a server-side function with the service role key.

Can RapidDev help me set up safe delete patterns for my Supabase project?

Yes. RapidDev can help you implement soft-delete workflows, cascading delete configuration, audit logging, and RLS policies for secure data deletion in your Supabase application.

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.