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
Delete a single record by ID
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.
1import { supabase } from '@/lib/supabase'23// Delete a single task by ID4const { error } = await supabase5 .from('tasks')6 .delete()7 .eq('id', 42)89if (error) {10 console.error('Delete failed:', error.message)11} else {12 console.log('Task deleted successfully')13}1415// Delete and return the deleted row16const { data, error: err } = await supabase17 .from('tasks')18 .delete()19 .eq('id', 42)20 .select()2122console.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.
Delete multiple records with filters
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).
1// Delete multiple tasks by ID2const { error } = await supabase3 .from('tasks')4 .delete()5 .in('id', [10, 11, 12, 13])67// Delete all completed tasks for the current user8const { data: { user } } = await supabase.auth.getUser()9const { error: bulkError } = await supabase10 .from('tasks')11 .delete()12 .eq('user_id', user.id)13 .eq('completed', true)1415// Delete old records (older than 30 days)16const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString()17const { error: cleanupError } = await supabase18 .from('logs')19 .delete()20 .lt('created_at', thirtyDaysAgo)Expected result: All rows matching the combined filter conditions are deleted from the table.
Create RLS policies that allow deletion
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.
1-- Enable RLS (if not already enabled)2ALTER TABLE public.tasks ENABLE ROW LEVEL SECURITY;34-- Allow users to delete their own tasks5CREATE POLICY "Users can delete own tasks"6 ON public.tasks7 FOR DELETE8 TO authenticated9 USING ((SELECT auth.uid()) = user_id);1011-- You also need a SELECT policy for delete to find rows12CREATE POLICY "Users can view own tasks"13 ON public.tasks14 FOR SELECT15 TO authenticated16 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.
Set up cascading deletes with foreign keys
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.
1-- Create a table with cascading delete on the foreign key2CREATE 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);910-- To change an existing foreign key to cascade:11-- 1. Drop the old constraint12ALTER TABLE public.comments13 DROP CONSTRAINT comments_task_id_fkey;1415-- 2. Add it back with CASCADE16ALTER TABLE public.comments17 ADD CONSTRAINT comments_task_id_fkey18 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.
Implement soft-delete instead of permanent deletion
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().
1-- Add soft-delete column to existing table2ALTER TABLE public.tasks3 ADD COLUMN deleted_at timestamptz DEFAULT null;45-- Update RLS policy to hide soft-deleted rows6DROP POLICY IF EXISTS "Users can view own tasks" ON public.tasks;7CREATE POLICY "Users can view own active tasks"8 ON public.tasks FOR SELECT9 TO authenticated10 USING ((SELECT auth.uid()) = user_id AND deleted_at IS NULL);1112-- Soft-delete from the JS client (UPDATE, not DELETE)13-- const { error } = await supabase14-- .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
1// Complete example: delete operations in Supabase2import { createClient } from '@supabase/supabase-js'34const supabase = createClient(5 process.env.NEXT_PUBLIC_SUPABASE_URL!,6 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!7)89// 1. Delete a single record by ID10async function deleteTask(taskId: number) {11 const { error } = await supabase12 .from('tasks')13 .delete()14 .eq('id', taskId)1516 if (error) throw new Error(`Delete failed: ${error.message}`)17 return true18}1920// 2. Bulk delete with filter21async function deleteCompletedTasks(userId: string) {22 const { data, error } = await supabase23 .from('tasks')24 .delete()25 .eq('user_id', userId)26 .eq('completed', true)27 .select('id')2829 if (error) throw new Error(`Bulk delete failed: ${error.message}`)30 return data?.length ?? 031}3233// 3. Soft-delete (mark as deleted without removing)34async function softDeleteTask(taskId: number) {35 const { error } = await supabase36 .from('tasks')37 .update({ deleted_at: new Date().toISOString() })38 .eq('id', taskId)3940 if (error) throw new Error(`Soft delete failed: ${error.message}`)41 return true42}4344// 4. Restore a soft-deleted record45async function restoreTask(taskId: number) {46 const { error } = await supabase47 .from('tasks')48 .update({ deleted_at: null })49 .eq('id', taskId)5051 if (error) throw new Error(`Restore failed: ${error.message}`)52 return true53}5455// 5. Preview before delete (safety check)56async function previewDelete(userId: string) {57 const { data, error } = await supabase58 .from('tasks')59 .select('id, title, completed')60 .eq('user_id', userId)61 .eq('completed', true)6263 if (error) throw new Error(`Preview failed: ${error.message}`)64 console.log(`${data.length} tasks will be deleted:`, data)65 return data66}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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation