# How to Delete 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 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.

## Before you start

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

```
import { supabase } from '@/lib/supabase'

// Delete a single task by ID
const { error } = await supabase
  .from('tasks')
  .delete()
  .eq('id', 42)

if (error) {
  console.error('Delete failed:', error.message)
} else {
  console.log('Task deleted successfully')
}

// Delete and return the deleted row
const { data, error: err } = await supabase
  .from('tasks')
  .delete()
  .eq('id', 42)
  .select()

console.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).

```
// Delete multiple tasks by ID
const { error } = await supabase
  .from('tasks')
  .delete()
  .in('id', [10, 11, 12, 13])

// Delete all completed tasks for the current user
const { data: { user } } = await supabase.auth.getUser()
const { error: bulkError } = await supabase
  .from('tasks')
  .delete()
  .eq('user_id', user.id)
  .eq('completed', true)

// Delete old records (older than 30 days)
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString()
const { error: cleanupError } = await supabase
  .from('logs')
  .delete()
  .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.

```
-- Enable RLS (if not already enabled)
ALTER TABLE public.tasks ENABLE ROW LEVEL SECURITY;

-- Allow users to delete their own tasks
CREATE POLICY "Users can delete own tasks"
  ON public.tasks
  FOR DELETE
  TO authenticated
  USING ((SELECT auth.uid()) = user_id);

-- You also need a SELECT policy for delete to find rows
CREATE POLICY "Users can view own tasks"
  ON public.tasks
  FOR SELECT
  TO authenticated
  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.

```
-- Create a table with cascading delete on the foreign key
CREATE TABLE public.comments (
  id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  task_id bigint REFERENCES public.tasks(id) ON DELETE CASCADE,
  user_id uuid REFERENCES auth.users(id) ON DELETE CASCADE,
  body text NOT NULL,
  created_at timestamptz DEFAULT now()
);

-- To change an existing foreign key to cascade:
-- 1. Drop the old constraint
ALTER TABLE public.comments
  DROP CONSTRAINT comments_task_id_fkey;

-- 2. Add it back with CASCADE
ALTER TABLE public.comments
  ADD CONSTRAINT comments_task_id_fkey
  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().

```
-- Add soft-delete column to existing table
ALTER TABLE public.tasks
  ADD COLUMN deleted_at timestamptz DEFAULT null;

-- Update RLS policy to hide soft-deleted rows
DROP POLICY IF EXISTS "Users can view own tasks" ON public.tasks;
CREATE POLICY "Users can view own active tasks"
  ON public.tasks FOR SELECT
  TO authenticated
  USING ((SELECT auth.uid()) = user_id AND deleted_at IS NULL);

-- Soft-delete from the JS client (UPDATE, not DELETE)
-- const { error } = await supabase
--   .from('tasks')
--   .update({ deleted_at: new Date().toISOString() })
--   .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 code example

File: `delete-records.ts`

```typescript
// Complete example: delete operations in Supabase
import { createClient } from '@supabase/supabase-js'

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

// 1. Delete a single record by ID
async function deleteTask(taskId: number) {
  const { error } = await supabase
    .from('tasks')
    .delete()
    .eq('id', taskId)

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

// 2. Bulk delete with filter
async function deleteCompletedTasks(userId: string) {
  const { data, error } = await supabase
    .from('tasks')
    .delete()
    .eq('user_id', userId)
    .eq('completed', true)
    .select('id')

  if (error) throw new Error(`Bulk delete failed: ${error.message}`)
  return data?.length ?? 0
}

// 3. Soft-delete (mark as deleted without removing)
async function softDeleteTask(taskId: number) {
  const { error } = await supabase
    .from('tasks')
    .update({ deleted_at: new Date().toISOString() })
    .eq('id', taskId)

  if (error) throw new Error(`Soft delete failed: ${error.message}`)
  return true
}

// 4. Restore a soft-deleted record
async function restoreTask(taskId: number) {
  const { error } = await supabase
    .from('tasks')
    .update({ deleted_at: null })
    .eq('id', taskId)

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

// 5. Preview before delete (safety check)
async function previewDelete(userId: string) {
  const { data, error } = await supabase
    .from('tasks')
    .select('id, title, completed')
    .eq('user_id', userId)
    .eq('completed', true)

  if (error) throw new Error(`Preview failed: ${error.message}`)
  console.log(`${data.length} tasks will be deleted:`, data)
  return data
}
```

## Common mistakes

- **Calling .delete() without any filter, which attempts to delete all rows in the table** — undefined Fix: 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.
- **Delete operation returns zero rows affected even though matching rows exist** — undefined Fix: 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.
- **Foreign key violation error when deleting a parent record that has child records** — undefined Fix: 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

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

---

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