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

How to Insert Data into a Supabase Table

To insert data into a Supabase table, use supabase.from('table_name').insert() with a single object or an array for bulk inserts. The insert respects Row Level Security, so you need an INSERT policy granting the authenticated role permission. Use upsert() with an onConflict option to handle duplicates. Always check the returned error object and use .select() chained after insert to get the created row back.

What you'll learn

  • How to insert single and multiple rows using the Supabase JS client
  • How to write RLS INSERT policies to allow data creation
  • How to use upsert for conflict resolution on duplicate keys
  • How to return inserted data with the .select() modifier
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 insert data into a Supabase table, use supabase.from('table_name').insert() with a single object or an array for bulk inserts. The insert respects Row Level Security, so you need an INSERT policy granting the authenticated role permission. Use upsert() with an onConflict option to handle duplicates. Always check the returned error object and use .select() chained after insert to get the created row back.

Inserting Data into Supabase Tables

Supabase provides a JavaScript client that wraps the auto-generated PostgREST API to perform CRUD operations on your PostgreSQL database. This tutorial covers inserting single rows, bulk inserts, upserts for handling duplicates, and the RLS policies required to allow inserts from your frontend application.

Prerequisites

  • A Supabase project with at least one table created
  • The Supabase JS client installed (@supabase/supabase-js v2+)
  • RLS enabled on the target table
  • A signed-in user (if your INSERT policy targets the authenticated role)

Step-by-step guide

1

Create a table and enable RLS

Before inserting data, you need a table with RLS enabled. Use the SQL Editor in the Supabase Dashboard to create a table and enable RLS. Once RLS is enabled with no policies, all access is denied by default — inserts will silently fail (return no error but insert nothing) unless you add an INSERT policy.

typescript
1create table public.todos (
2 id bigint generated always as identity primary key,
3 user_id uuid references auth.users not null default auth.uid(),
4 title text not null,
5 completed boolean default false,
6 created_at timestamptz default now()
7);
8
9alter table public.todos enable row level security;

Expected result: A todos table exists with RLS enabled and no access policies yet.

2

Write an INSERT RLS policy

Create an RLS policy that allows authenticated users to insert rows. The WITH CHECK clause validates the new row being inserted. A common pattern is to ensure the user_id column matches the authenticated user's ID, preventing users from creating records on behalf of other users. If your table has a default of auth.uid() on user_id, the policy still needs to verify it.

typescript
1-- Allow authenticated users to insert their own todos
2create policy "Users can insert their own todos"
3on public.todos for insert
4to authenticated
5with check (
6 (select auth.uid()) = user_id
7);
8
9-- Also add a SELECT policy so inserted rows can be returned
10create policy "Users can view their own todos"
11on public.todos for select
12to authenticated
13using (
14 (select auth.uid()) = user_id
15);

Expected result: Authenticated users can insert rows where user_id matches their own auth ID.

3

Insert a single row from the JS client

Use supabase.from('table').insert() to create a new row. Pass an object with the column names as keys. Chain .select() after insert to return the created row. Without .select(), the insert succeeds but returns no data. Always check the error object — a null error means the insert succeeded.

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)
7
8// Insert a single row and return the created record
9const { data, error } = await supabase
10 .from('todos')
11 .insert({ title: 'Buy groceries' })
12 .select()
13
14if (error) {
15 console.error('Insert error:', error.message)
16} else {
17 console.log('Created todo:', data[0])
18}

Expected result: A new row is inserted into the todos table and the created record is returned with all columns including the auto-generated id and timestamps.

4

Insert multiple rows in a single call

Pass an array of objects to insert() to create multiple rows in one database round-trip. This is significantly faster than calling insert() in a loop for each row. All rows in the array are inserted in a single transaction, so either all succeed or all fail.

typescript
1// Bulk insert multiple todos
2const { data, error } = await supabase
3 .from('todos')
4 .insert([
5 { title: 'Buy groceries' },
6 { title: 'Walk the dog' },
7 { title: 'Finish project report' },
8 ])
9 .select()
10
11if (error) {
12 console.error('Bulk insert error:', error.message)
13} else {
14 console.log(`Inserted ${data.length} todos`)
15}

Expected result: All rows in the array are inserted in a single transaction and the created records are returned.

5

Use upsert to handle duplicate conflicts

The upsert() method inserts a row if it does not exist, or updates it if a row with the same unique/primary key already exists. Specify the onConflict option with the column name that has the unique constraint. This is useful for syncing data from external sources or implementing 'save' functionality where the row may or may not already exist.

typescript
1// Upsert: insert or update if title already exists
2// (requires a unique constraint on the title column)
3const { data, error } = await supabase
4 .from('todos')
5 .upsert(
6 { title: 'Buy groceries', completed: true },
7 { onConflict: 'title' }
8 )
9 .select()
10
11if (error) {
12 console.error('Upsert error:', error.message)
13} else {
14 console.log('Upserted:', data[0])
15}

Expected result: If a row with the same title exists, it is updated with completed: true. Otherwise, a new row is created.

Complete working example

insert-data.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// Insert a single row
9async function createTodo(title: string) {
10 const { data, error } = await supabase
11 .from('todos')
12 .insert({ title })
13 .select()
14 .single()
15
16 if (error) throw new Error(`Insert failed: ${error.message}`)
17 return data
18}
19
20// Insert multiple rows
21async function createTodos(titles: string[]) {
22 const rows = titles.map((title) => ({ title }))
23
24 const { data, error } = await supabase
25 .from('todos')
26 .insert(rows)
27 .select()
28
29 if (error) throw new Error(`Bulk insert failed: ${error.message}`)
30 return data
31}
32
33// Upsert (insert or update on conflict)
34async function upsertTodo(title: string, completed: boolean) {
35 const { data, error } = await supabase
36 .from('todos')
37 .upsert({ title, completed }, { onConflict: 'title' })
38 .select()
39 .single()
40
41 if (error) throw new Error(`Upsert failed: ${error.message}`)
42 return data
43}
44
45// Insert with explicit user_id (server-side with service role)
46async function adminCreateTodo(userId: string, title: string) {
47 const adminClient = createClient(
48 process.env.SUPABASE_URL!,
49 process.env.SUPABASE_SERVICE_ROLE_KEY!
50 )
51
52 const { data, error } = await adminClient
53 .from('todos')
54 .insert({ user_id: userId, title })
55 .select()
56 .single()
57
58 if (error) throw new Error(`Admin insert failed: ${error.message}`)
59 return data
60}
61
62// === Usage ===
63const todo = await createTodo('Buy groceries')
64console.log('Created:', todo)
65
66const todos = await createTodos(['Walk the dog', 'Read a book'])
67console.log('Bulk created:', todos.length, 'todos')
68
69const upserted = await upsertTodo('Buy groceries', true)
70console.log('Upserted:', upserted)

Common mistakes when inserting Data into a Supabase Table

Why it's a problem: Insert succeeds with no error but the row does not appear in the table

How to avoid: This is almost always an RLS issue. When RLS is enabled with no INSERT policy, the insert is silently blocked. Add an INSERT policy for the authenticated role with the correct WITH CHECK clause.

Why it's a problem: Forgetting to chain .select() after insert and getting null data back

How to avoid: By default, insert() does not return the created row. Chain .select() to get the inserted data back, or .select().single() if you inserted one row and want a single object instead of an array.

Why it's a problem: Using the service role key in client-side code to bypass RLS for inserts

How to avoid: The service role key bypasses all RLS and must NEVER be used in browser code. Create proper INSERT policies instead. Use the service role key only in server-side code like Edge Functions or API routes.

Why it's a problem: Not handling the unique constraint violation error on upsert without onConflict

How to avoid: If you call upsert without specifying onConflict and there is no unique constraint on the table's primary key for the given data, PostgreSQL does not know which column to check for conflicts. Always specify onConflict with the column name.

Best practices

  • Always enable RLS on tables and write explicit INSERT policies before inserting data from the client
  • Use column defaults (like auth.uid() for user_id) to reduce client-side data that must be sent
  • Chain .select() after insert to get the created row back, including auto-generated columns
  • Use .single() when inserting one row to get a single object instead of an array
  • Batch multiple inserts into a single array call instead of looping insert() calls
  • Use upsert with onConflict for idempotent operations that may be retried
  • Validate data on the client before inserting to provide instant feedback
  • Use the service role key only on the server side for admin operations that bypass RLS

Still stuck?

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

ChatGPT Prompt

Show me how to insert data into a Supabase table using the JavaScript client. Include single insert, bulk insert, and upsert examples. Also show the SQL to create the table with RLS and the INSERT policy needed for authenticated users.

Supabase Prompt

Create a todos table in Supabase with RLS enabled. Write INSERT and SELECT policies for the authenticated role. Then implement TypeScript functions for creating a single todo, bulk creating multiple todos, and upserting a todo with conflict resolution on the title column.

Frequently asked questions

Why does my insert return no error but the row doesn't appear?

This is the most common Supabase issue. When RLS is enabled with no INSERT policy, inserts are silently blocked — no error is thrown but no row is created. Add an INSERT policy for the authenticated role.

How do I get the auto-generated ID after insert?

Chain .select() after insert(): supabase.from('table').insert({ title: 'test' }).select(). The returned data includes all columns, including auto-generated id and timestamp fields.

Can I insert data without a user being signed in?

Only if your INSERT policy targets the anon role (unauthenticated requests). By default, most policies target the authenticated role, requiring a signed-in user. For public data collection (like a contact form), create a policy for the anon role.

What is the difference between insert and upsert?

insert() always 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 matching unique key is found. Use upsert for idempotent operations.

How many rows can I insert in a single bulk call?

There is no hard limit on the number of rows per insert call, but very large batches may hit timeout or memory limits. For best performance, batch inserts in groups of 500-1000 rows.

Can I insert data into related tables in one call?

No, the Supabase JS client does not support multi-table inserts in a single call. Insert into the parent table first, get the ID, then insert into the child table. For atomic multi-table inserts, use a database function called via supabase.rpc().

Can RapidDev help build a data management layer with Supabase?

Yes, RapidDev can design your database schema, write RLS policies, and implement a complete data access layer with inserts, updates, deletes, and real-time subscriptions tailored to your 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.