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

How to Add a Foreign Key in Supabase

To add a foreign key in Supabase, use ALTER TABLE with a REFERENCES clause in the SQL Editor or use the Table Editor's column settings to link one table to another. Foreign keys enforce referential integrity, ensuring that a value in one column always matches a valid row in the referenced table. Choose CASCADE or RESTRICT for delete behavior depending on whether child rows should be removed or preserved when the parent is deleted.

What you'll learn

  • How to add foreign key constraints using SQL and the Table Editor
  • How to choose between CASCADE, RESTRICT, and SET NULL delete behavior
  • How to link tables to auth.users for user-owned data
  • How to query related data across foreign key relationships
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner8 min read10-15 minSupabase (all plans), PostgreSQL 15+March 2026RapidDev Engineering Team
TL;DR

To add a foreign key in Supabase, use ALTER TABLE with a REFERENCES clause in the SQL Editor or use the Table Editor's column settings to link one table to another. Foreign keys enforce referential integrity, ensuring that a value in one column always matches a valid row in the referenced table. Choose CASCADE or RESTRICT for delete behavior depending on whether child rows should be removed or preserved when the parent is deleted.

Adding Foreign Key Constraints in Supabase

Foreign keys are the foundation of relational data modeling in Supabase. They create links between tables, ensure data integrity, and enable the Supabase JS client to automatically join related data in queries. This tutorial covers creating foreign keys via SQL and the Dashboard Table Editor, linking to auth.users, and choosing the right delete behavior for your use case.

Prerequisites

  • A Supabase project with at least one existing table
  • Access to the SQL Editor or Table Editor in the Dashboard
  • Basic understanding of SQL and relational databases
  • Familiarity with the Supabase JavaScript client

Step-by-step guide

1

Create a parent table with a primary key

A foreign key references the primary key (or unique column) of another table. Start by creating the parent table that will be referenced. In this example, we create a categories table with an auto-generated bigint primary key. Every category has a unique ID that other tables can reference.

typescript
1create table public.categories (
2 id bigint generated always as identity primary key,
3 name text not null,
4 created_at timestamptz default now()
5);

Expected result: The categories table is created with an auto-incrementing id column as the primary key.

2

Add a foreign key column to the child table using SQL

Add a column to the child table that references the parent table's primary key. The REFERENCES keyword creates the foreign key constraint. You can add it when creating the table or alter an existing table. The column type must match the referenced column type exactly. In this example, we add a category_id column to a posts table that references categories.

typescript
1-- Option 1: Add foreign key when creating a new table
2create table public.posts (
3 id bigint generated always as identity primary key,
4 title text not null,
5 body text,
6 category_id bigint references public.categories(id),
7 user_id uuid references auth.users(id) on delete cascade,
8 created_at timestamptz default now()
9);
10
11-- Option 2: Add foreign key to an existing table
12alter table public.posts
13 add column category_id bigint references public.categories(id);

Expected result: The posts table has a category_id column that is constrained to only accept values that exist in the categories table.

3

Choose the correct ON DELETE behavior

When a referenced parent row is deleted, PostgreSQL needs to know what to do with the child rows. CASCADE deletes all child rows automatically. RESTRICT (the default) prevents the parent from being deleted if children exist. SET NULL sets the foreign key column to null. For user-owned data linked to auth.users, CASCADE is the standard choice — when a user is deleted, their data should be removed too.

typescript
1-- CASCADE: delete child rows when parent is deleted
2alter table public.posts
3 add constraint fk_posts_category
4 foreign key (category_id)
5 references public.categories(id)
6 on delete cascade;
7
8-- SET NULL: set to null when parent is deleted
9alter table public.posts
10 add constraint fk_posts_category
11 foreign key (category_id)
12 references public.categories(id)
13 on delete set null;
14
15-- RESTRICT (default): prevent parent deletion if children exist
16alter table public.posts
17 add constraint fk_posts_category
18 foreign key (category_id)
19 references public.categories(id)
20 on delete restrict;

Expected result: The foreign key constraint is created with the specified delete behavior.

4

Link a table to auth.users

The most common foreign key in Supabase connects a table to auth.users so each row belongs to a specific user. Reference auth.users(id) with ON DELETE CASCADE. This is the canonical pattern for profiles tables, user-generated content, and any data that should be deleted when the user account is removed. The id column in auth.users is a UUID, so your foreign key column must also be UUID type.

typescript
1-- Canonical user profiles pattern
2create table public.profiles (
3 id uuid not null references auth.users(id) on delete cascade,
4 display_name text,
5 avatar_url text,
6 primary key (id)
7);
8
9-- Enable RLS
10alter table public.profiles enable row level security;
11
12-- RLS policy: users can read and update their own profile
13create policy "Users manage own profile"
14 on public.profiles for all
15 to authenticated
16 using ((select auth.uid()) = id);

Expected result: The profiles table is linked to auth.users with cascade delete, RLS is enabled, and users can manage their own profile.

5

Add a foreign key using the Table Editor in Dashboard

If you prefer a visual approach, open your Supabase Dashboard and navigate to the Table Editor. Click on the table you want to modify, then click Edit Column on the column that should be a foreign key (or add a new column). In the column settings, enable the Foreign Key toggle and select the target table and column. Choose the ON DELETE action from the dropdown. Click Save to apply the constraint.

Expected result: The foreign key constraint is visible in the column settings and enforced on all new and existing data.

6

Query related data across foreign keys

Supabase PostgREST automatically detects foreign key relationships and lets you query nested data using the select syntax. Pass the related table name as a nested select to get parent or child data in a single request. This avoids multiple round trips and is the recommended way to fetch related data in the Supabase JS client.

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// Fetch posts with their category name (many-to-one)
9const { data: posts } = await supabase
10 .from('posts')
11 .select(`
12 id,
13 title,
14 categories ( name )
15 `)
16
17// Fetch a category with all its posts (one-to-many)
18const { data: category } = await supabase
19 .from('categories')
20 .select(`
21 id,
22 name,
23 posts ( id, title )
24 `)
25 .eq('id', 1)
26 .single()

Expected result: The response includes nested related data — posts contain their category object, and categories contain their array of posts.

Complete working example

foreign-key-setup.sql
1-- Create parent table
2create table public.categories (
3 id bigint generated always as identity primary key,
4 name text not null,
5 created_at timestamptz default now()
6);
7
8-- Create child table with foreign keys
9create table public.posts (
10 id bigint generated always as identity primary key,
11 title text not null,
12 body text,
13 category_id bigint references public.categories(id) on delete set null,
14 user_id uuid not null references auth.users(id) on delete cascade,
15 created_at timestamptz default now()
16);
17
18-- Enable RLS on both tables
19alter table public.categories enable row level security;
20alter table public.posts enable row level security;
21
22-- Public read for categories
23create policy "Anyone can read categories"
24 on public.categories for select
25 to anon, authenticated
26 using (true);
27
28-- Users can read all posts
29create policy "Authenticated users can read posts"
30 on public.posts for select
31 to authenticated
32 using (true);
33
34-- Users can only create their own posts
35create policy "Users can create own posts"
36 on public.posts for insert
37 to authenticated
38 with check ((select auth.uid()) = user_id);
39
40-- Users can only update their own posts
41create policy "Users can update own posts"
42 on public.posts for update
43 to authenticated
44 using ((select auth.uid()) = user_id)
45 with check ((select auth.uid()) = user_id);
46
47-- Users can only delete their own posts
48create policy "Users can delete own posts"
49 on public.posts for delete
50 to authenticated
51 using ((select auth.uid()) = user_id);
52
53-- Add index on foreign key columns for performance
54create index idx_posts_category_id on public.posts(category_id);
55create index idx_posts_user_id on public.posts(user_id);

Common mistakes when adding a Foreign Key in Supabase

Why it's a problem: Using a different data type for the foreign key column than the referenced primary key (e.g., int4 referencing a bigint)

How to avoid: Ensure the foreign key column type exactly matches the referenced column type. Use bigint for bigint references and uuid for uuid references.

Why it's a problem: Forgetting to add ON DELETE CASCADE when linking to auth.users, causing user deletion to fail if they have related data

How to avoid: Always use ON DELETE CASCADE for foreign keys referencing auth.users(id). This ensures user data is cleaned up automatically when an account is deleted.

Why it's a problem: Not adding an index on foreign key columns, causing slow joins on large tables

How to avoid: PostgreSQL does not automatically index foreign key columns. Add an explicit index: CREATE INDEX idx_table_column ON table(column).

Why it's a problem: Trying to insert a child row with a foreign key value that does not exist in the parent table

How to avoid: Insert the parent row first, then the child row. Or use a transaction to insert both atomically. The foreign key constraint rejects invalid references.

Best practices

  • Always add ON DELETE CASCADE for foreign keys referencing auth.users to ensure clean user data removal
  • Add an index on every foreign key column for faster joins and lookups
  • Use SET NULL for optional relationships where child records should survive parent deletion
  • Use the nested select syntax in the Supabase JS client to fetch related data in a single query
  • Create foreign keys in SQL migrations for reproducible schema changes across environments
  • Name your constraints descriptively: fk_posts_category instead of relying on auto-generated names
  • Enable RLS on all tables with foreign key relationships and write policies for each operation
  • Use UUID primary keys when you need globally unique IDs across distributed systems

Still stuck?

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

ChatGPT Prompt

I have a Supabase project with a categories table and a posts table. Show me how to add a foreign key from posts.category_id to categories.id, link posts to auth.users, set up RLS policies, and query nested data with the JavaScript client.

Supabase Prompt

Help me set up foreign key relationships in my Supabase database. I need a posts table linked to categories and auth.users. Show me the SQL for the tables, constraints, RLS policies, and how to query related data.

Frequently asked questions

Does PostgreSQL automatically create an index on foreign key columns?

No. Unlike primary keys, PostgreSQL does not automatically index foreign key columns. You should create an index manually on each foreign key column to improve join and lookup performance.

Can I add a foreign key to an existing column that already has data?

Yes, but all existing values in the column must already exist in the referenced table. If any orphaned values exist, the ALTER TABLE command will fail. Clean up invalid data first.

What is the difference between CASCADE and RESTRICT?

CASCADE automatically deletes or updates child rows when the parent row is deleted or updated. RESTRICT prevents the parent from being deleted if any child rows reference it. RESTRICT is the default behavior.

Can I reference a column other than the primary key?

Yes, you can reference any column with a UNIQUE constraint. However, referencing the primary key is the most common and recommended approach.

How do I remove a foreign key constraint?

Use ALTER TABLE table_name DROP CONSTRAINT constraint_name. Find the constraint name by querying information_schema.table_constraints or checking the Table Editor in the Dashboard.

Can I have multiple foreign keys on the same table?

Yes. A table can have as many foreign keys as needed. For example, a posts table can reference both categories(id) and auth.users(id) simultaneously.

Can RapidDev help design my Supabase database schema?

Yes. RapidDev can help design your database schema with proper foreign key relationships, RLS policies, and indexing strategies optimized for your application's access patterns.

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.