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
Create a parent table with a primary key
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.
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.
Add a foreign key column to the child table using SQL
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.
1-- Option 1: Add foreign key when creating a new table2create 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);1011-- Option 2: Add foreign key to an existing table12alter table public.posts13 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.
Choose the correct ON DELETE behavior
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.
1-- CASCADE: delete child rows when parent is deleted2alter table public.posts3 add constraint fk_posts_category4 foreign key (category_id)5 references public.categories(id)6 on delete cascade;78-- SET NULL: set to null when parent is deleted9alter table public.posts10 add constraint fk_posts_category11 foreign key (category_id)12 references public.categories(id)13 on delete set null;1415-- RESTRICT (default): prevent parent deletion if children exist16alter table public.posts17 add constraint fk_posts_category18 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.
Link a table to auth.users
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.
1-- Canonical user profiles pattern2create 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);89-- Enable RLS10alter table public.profiles enable row level security;1112-- RLS policy: users can read and update their own profile13create policy "Users manage own profile"14 on public.profiles for all15 to authenticated16 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.
Add a foreign key using the Table Editor in Dashboard
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.
Query related data across foreign keys
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.
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// Fetch posts with their category name (many-to-one)9const { data: posts } = await supabase10 .from('posts')11 .select(`12 id,13 title,14 categories ( name )15 `)1617// Fetch a category with all its posts (one-to-many)18const { data: category } = await supabase19 .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
1-- Create parent table2create table public.categories (3 id bigint generated always as identity primary key,4 name text not null,5 created_at timestamptz default now()6);78-- Create child table with foreign keys9create 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);1718-- Enable RLS on both tables19alter table public.categories enable row level security;20alter table public.posts enable row level security;2122-- Public read for categories23create policy "Anyone can read categories"24 on public.categories for select25 to anon, authenticated26 using (true);2728-- Users can read all posts29create policy "Authenticated users can read posts"30 on public.posts for select31 to authenticated32 using (true);3334-- Users can only create their own posts35create policy "Users can create own posts"36 on public.posts for insert37 to authenticated38 with check ((select auth.uid()) = user_id);3940-- Users can only update their own posts41create policy "Users can update own posts"42 on public.posts for update43 to authenticated44 using ((select auth.uid()) = user_id)45 with check ((select auth.uid()) = user_id);4647-- Users can only delete their own posts48create policy "Users can delete own posts"49 on public.posts for delete50 to authenticated51 using ((select auth.uid()) = user_id);5253-- Add index on foreign key columns for performance54create 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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation