Prevent duplicate entries in Supabase by adding UNIQUE constraints to columns that must be unique, using upsert with onConflict to handle conflicts gracefully, and creating partial unique indexes for conditional uniqueness. UNIQUE constraints cause Postgres to reject duplicate inserts with a clear error, while upsert lets you update the existing row instead of failing. Combine these with RLS policies for safe client-side duplicate prevention.
Preventing Duplicate Entries with UNIQUE Constraints and Upsert in Supabase
Duplicate data is one of the most common problems in database applications. Supabase uses PostgreSQL, which provides robust tools for preventing duplicates at the database level: UNIQUE constraints, composite unique constraints, upsert operations, and partial unique indexes. This tutorial shows you how to apply each technique, when to use one over another, and how to handle conflict errors in your client code.
Prerequisites
- A Supabase project (free tier works)
- Access to the SQL Editor in the Supabase Dashboard
- @supabase/supabase-js v2 installed in your project
- A table with RLS enabled and basic policies configured
Step-by-step guide
Add a UNIQUE constraint to a single column
Add a UNIQUE constraint to a single column
The simplest way to prevent duplicates is to add a UNIQUE constraint to the column that must be unique. When you insert a row with a value that already exists in a UNIQUE column, Postgres rejects the insert with a duplicate key violation error. You can add the constraint when creating the table or add it later with ALTER TABLE.
1-- Add UNIQUE when creating the table2create table public.subscribers (3 id bigint generated always as identity primary key,4 email text unique not null,5 name text,6 subscribed_at timestamptz default now()7);89alter table public.subscribers enable row level security;1011-- Or add UNIQUE to an existing column12alter table public.subscribers13 add constraint subscribers_email_unique unique (email);Expected result: Attempting to insert a row with a duplicate email value returns error code 23505 (unique_violation) instead of silently creating a duplicate.
Add a composite UNIQUE constraint for multi-column uniqueness
Add a composite UNIQUE constraint for multi-column uniqueness
Sometimes a single column is not enough to define uniqueness. For example, in a user_roles table, each user should have each role only once, but multiple users can have the same role and each user can have multiple roles. A composite UNIQUE constraint on (user_id, role) enforces this. The combination must be unique, not each column individually.
1create table public.user_roles (2 id bigint generated always as identity primary key,3 user_id uuid not null references auth.users(id) on delete cascade,4 role text not null,5 assigned_at timestamptz default now(),6 unique(user_id, role)7);89alter table public.user_roles enable row level security;1011create policy "Users read own roles" on public.user_roles12 for select to authenticated13 using ((select auth.uid()) = user_id);1415create policy "Auth users insert roles" on public.user_roles16 for insert to authenticated17 with check ((select auth.uid()) = user_id);Expected result: Inserting the same (user_id, role) combination twice fails with a unique violation error. Different users can have the same role.
Use upsert to insert or update on conflict
Use upsert to insert or update on conflict
Instead of failing on duplicate inserts, you can use upsert to update the existing row when a conflict occurs. The Supabase JS client's upsert method performs an INSERT with an ON CONFLICT clause. Specify the onConflict option to tell Postgres which column(s) to check for conflicts. If a match is found, the existing row is updated with the new values instead of creating a duplicate.
1import { createClient } from '@supabase/supabase-js'23const supabase = createClient(4 process.env.NEXT_PUBLIC_SUPABASE_URL!,5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!6)78// Upsert: insert new subscriber or update name if email exists9const { data, error } = await supabase10 .from('subscribers')11 .upsert(12 { email: 'user@example.com', name: 'Updated Name' },13 { onConflict: 'email' }14 )15 .select()1617// Bulk upsert: insert or update multiple rows18const { data: bulk, error: bulkError } = await supabase19 .from('subscribers')20 .upsert([21 { email: 'user1@example.com', name: 'User 1' },22 { email: 'user2@example.com', name: 'User 2' },23 { email: 'user3@example.com', name: 'User 3' }24 ], { onConflict: 'email' })25 .select()Expected result: If the email already exists, the name is updated. If the email is new, a new row is inserted. No duplicate key errors are raised.
Create a partial unique index for conditional uniqueness
Create a partial unique index for conditional uniqueness
A partial unique index enforces uniqueness only for rows that match a condition. This is useful when you want to allow duplicates in some cases but not others. For example, you might want to enforce unique emails only for active subscribers, allowing deleted or unsubscribed users to have the same email reused by new signups.
1-- Only enforce unique emails for active subscribers2create unique index idx_subscribers_active_email3on public.subscribers (email)4where is_active = true;56-- Example: add the is_active column first if it does not exist7alter table public.subscribers8 add column is_active boolean default true;Expected result: Two rows with the same email can exist if one has is_active = false. Inserting a second active row with the same email fails with a unique violation.
Handle duplicate key errors in client code
Handle duplicate key errors in client code
When a UNIQUE constraint is violated and you are not using upsert, the Supabase JS client returns an error with code 23505. Handle this error explicitly in your client code to show a user-friendly message instead of a generic error. This is important for forms where users might submit duplicate values.
1const { data, error } = await supabase2 .from('subscribers')3 .insert({ email: 'user@example.com', name: 'New User' })4 .select()56if (error) {7 if (error.code === '23505') {8 // Duplicate key violation9 console.error('This email is already registered.')10 // Show user-friendly message in your UI11 } else {12 console.error('Insert failed:', error.message)13 }14} else {15 console.log('Subscriber created:', data)16}Expected result: Duplicate inserts are caught with a specific error code, allowing you to display a clear message like 'This email is already registered' instead of a generic error.
Complete working example
1import { createClient } from '@supabase/supabase-js'23const supabase = createClient(4 process.env.NEXT_PUBLIC_SUPABASE_URL!,5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!6)78// Insert with duplicate handling9export async function addSubscriber(email: string, name: string) {10 const { data, error } = await supabase11 .from('subscribers')12 .insert({ email, name })13 .select()14 .single()1516 if (error) {17 if (error.code === '23505') {18 return { success: false, message: 'This email is already registered.' }19 }20 return { success: false, message: error.message }21 }2223 return { success: true, data }24}2526// Upsert: insert or update if exists27export async function upsertSubscriber(email: string, name: string) {28 const { data, error } = await supabase29 .from('subscribers')30 .upsert({ email, name }, { onConflict: 'email' })31 .select()32 .single()3334 if (error) return { success: false, message: error.message }35 return { success: true, data }36}3738// Bulk upsert with conflict resolution39export async function bulkUpsertSubscribers(40 subscribers: { email: string; name: string }[]41) {42 const { data, error } = await supabase43 .from('subscribers')44 .upsert(subscribers, { onConflict: 'email' })45 .select()4647 if (error) return { success: false, message: error.message }48 return { success: true, count: data.length, data }49}5051// Check if a value already exists before inserting52export async function emailExists(email: string): Promise<boolean> {53 const { count } = await supabase54 .from('subscribers')55 .select('id', { count: 'exact', head: true })56 .eq('email', email)5758 return (count ?? 0) > 059}Common mistakes when avoiding Duplicated Entries in Supabase
Why it's a problem: Relying on client-side checks (query then insert) instead of UNIQUE constraints, creating race conditions
How to avoid: Always enforce uniqueness at the database level with UNIQUE constraints. Client-side checks can miss concurrent inserts from other users.
Why it's a problem: Using upsert without an UPDATE RLS policy, causing the operation to fail when a conflict is found
How to avoid: Add both INSERT and UPDATE RLS policies when using upsert. The upsert performs an UPDATE when a conflict is detected.
Why it's a problem: Adding a UNIQUE constraint to a column that already contains duplicate values, causing the ALTER TABLE to fail
How to avoid: Clean up existing duplicates before adding the constraint. Query for duplicates first: SELECT email, count(*) FROM subscribers GROUP BY email HAVING count(*) > 1.
Best practices
- Always enforce uniqueness at the database level with UNIQUE constraints, not just in application code
- Use upsert with onConflict when you want insert-or-update behavior instead of failing on duplicates
- Add composite UNIQUE constraints when uniqueness depends on multiple columns
- Handle error code 23505 explicitly in client code to show user-friendly duplicate messages
- Use partial unique indexes for soft-delete patterns where uniqueness should only apply to active records
- Clean up existing duplicates before adding UNIQUE constraints to avoid ALTER TABLE failures
- Add both INSERT and UPDATE RLS policies when using upsert operations
- Use select('id', { count: 'exact', head: true }) for lightweight existence checks
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I have a Supabase table that keeps getting duplicate entries. Show me how to add UNIQUE constraints, use upsert with onConflict to handle duplicates gracefully, and create partial unique indexes for conditional uniqueness. Include TypeScript client code with error handling.
Create a Supabase subscribers table with a UNIQUE constraint on email. Show TypeScript functions for inserting with duplicate error handling (code 23505), upsert with onConflict, bulk upsert, and a partial unique index that only enforces uniqueness for active records.
Frequently asked questions
What error code does Supabase return for duplicate key violations?
Supabase returns PostgreSQL error code 23505 (unique_violation) when an INSERT or UPDATE violates a UNIQUE constraint. Check error.code in the JS client response.
What is the difference between insert and upsert in Supabase?
Insert creates a new row and fails if a UNIQUE constraint is violated. Upsert creates a new row if no conflict exists, or updates the existing row if a conflict is found on the specified onConflict column.
Can I have multiple UNIQUE constraints on the same table?
Yes. You can add UNIQUE constraints to multiple columns independently. Each constraint is checked separately. For example, both email and username can be unique on a users table.
Does upsert work with composite UNIQUE constraints?
Yes. Pass the composite columns as a comma-separated string: onConflict: 'user_id,role'. The upsert checks the combination of columns for conflicts.
Why does my upsert fail even though the UNIQUE constraint exists?
Upsert requires both INSERT and UPDATE RLS policies. When a conflict is found, the operation switches to an UPDATE, which needs an UPDATE policy. Add one if missing.
How do I find and clean up existing duplicates in a table?
Run: SELECT column_name, count(*) FROM table_name GROUP BY column_name HAVING count(*) > 1. Then delete the extra rows before adding a UNIQUE constraint.
Can RapidDev help design a duplicate-free database schema for my Supabase project?
Yes. RapidDev can audit your existing schema for missing uniqueness constraints, clean up duplicate data, and implement proper upsert patterns to prevent future duplicates.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation