To add a phone number to a Supabase user profile, create a profiles table with a phone column linked to auth.users via a foreign key. Update the phone number from the client using supabase.from('profiles').update(). You can also enable phone OTP verification in the Supabase Dashboard under Authentication Providers to verify the number before saving it. Always write RLS policies so users can only update their own profile.
Adding a Phone Number Field to Supabase User Profiles
This tutorial walks you through adding a phone number field to your Supabase user profiles. You will extend the profiles table with a phone column, write RLS policies that let users update only their own profile, and build the client-side code to save the phone number. Optionally, you can enable phone OTP verification to confirm the number is real before storing it.
Prerequisites
- A Supabase project with auth enabled
- A profiles table linked to auth.users (or willingness to create one)
- The @supabase/supabase-js library installed in your project
- Your Supabase URL and anon key configured
Step-by-step guide
Create or update the profiles table with a phone column
Create or update the profiles table with a phone column
If you already have a profiles table, add a phone column to it. If not, create the canonical profiles table linked to auth.users with a phone column included. The phone column should be text type to accommodate international formats with country codes. Add a unique constraint if you want to prevent duplicate phone numbers across users.
1-- If creating a new profiles table2create table public.profiles (3 id uuid not null references auth.users(id) on delete cascade,4 display_name text,5 phone text,6 updated_at timestamptz default now(),7 primary key (id)8);910-- If adding phone to an existing profiles table11alter table public.profiles12 add column phone text;1314-- Optional: ensure phone numbers are unique15alter table public.profiles16 add constraint unique_phone unique (phone);Expected result: The profiles table has a phone column of type text, optionally with a unique constraint.
Set up a trigger to auto-create profiles on signup
Set up a trigger to auto-create profiles on signup
When a new user signs up, automatically create a row in the profiles table with their user ID. This trigger function runs after each insert on auth.users and populates the profile with any metadata passed during signup. Without this trigger, you would need to manually insert a profile row after every signup, which is error-prone.
1-- Trigger function to create profile on signup2create or replace function public.handle_new_user()3returns trigger4language plpgsql5security definer set search_path = ''6as $$7begin8 insert into public.profiles (id, display_name)9 values (10 new.id,11 new.raw_user_meta_data ->> 'display_name'12 );13 return new;14end;15$$;1617-- Attach trigger to auth.users18create trigger on_auth_user_created19 after insert on auth.users20 for each row execute function public.handle_new_user();Expected result: Every new user signup automatically creates a profile row with their user ID.
Write RLS policies for profile access and updates
Write RLS policies for profile access and updates
Enable Row Level Security on the profiles table and create policies that let users read and update only their own profile. The UPDATE policy uses both using and with check clauses to ensure the user owns the row before and after the update. Without these policies, profile updates will silently return no data.
1-- Enable RLS2alter table public.profiles enable row level security;34-- Users can read their own profile5create policy "Users can view own profile"6 on public.profiles for select7 to authenticated8 using ((select auth.uid()) = id);910-- Users can update their own profile11create policy "Users can update own profile"12 on public.profiles for update13 to authenticated14 using ((select auth.uid()) = id)15 with check ((select auth.uid()) = id);Expected result: RLS is enabled and authenticated users can only read and update their own profile row.
Update the phone number from client-side code
Update the phone number from client-side code
Use the Supabase JavaScript client to update the phone column in the authenticated user's profile. The RLS policy ensures users can only modify their own row, so you filter by the user's ID. Chain .select().single() to get the updated profile back in the response. Add client-side validation to ensure the phone number is in a valid format before sending the request.
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)78async function updatePhoneNumber(phone: string) {9 // Get the current user10 const { data: { user } } = await supabase.auth.getUser()11 if (!user) throw new Error('Not authenticated')1213 // Validate E.164 format14 const phoneRegex = /^\+[1-9]\d{1,14}$/15 if (!phoneRegex.test(phone)) {16 throw new Error('Phone must be in E.164 format: +14155551234')17 }1819 // Update the profile20 const { data, error } = await supabase21 .from('profiles')22 .update({ phone, updated_at: new Date().toISOString() })23 .eq('id', user.id)24 .select()25 .single()2627 if (error) throw error28 return data29}Expected result: The user's profile is updated with the new phone number and the updated record is returned.
Optionally verify the phone number with OTP
Optionally verify the phone number with OTP
If you want to verify that the phone number is real before saving it, use Supabase's built-in phone OTP. First, enable the Phone provider in Dashboard under Authentication > Providers. Then send an OTP to the user's phone using signInWithOtp, verify the code, and only save the phone number to the profile after successful verification. This requires a Twilio or MessageBird account configured in the Dashboard.
1// Step 1: Send OTP to the phone number2async function sendPhoneOtp(phone: string) {3 const { error } = await supabase.auth.signInWithOtp({4 phone,5 options: { shouldCreateUser: false }6 })7 if (error) throw error8}910// Step 2: Verify the OTP code11async function verifyPhoneOtp(phone: string, token: string) {12 const { data, error } = await supabase.auth.verifyOtp({13 phone,14 token,15 type: 'sms'16 })17 if (error) throw error1819 // Only save to profile after verification succeeds20 await updatePhoneNumber(phone)21 return data22}Expected result: The phone number is verified via SMS OTP, then saved to the user's profile only after successful verification.
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// Validate phone number format (E.164)9function isValidPhone(phone: string): boolean {10 return /^\+[1-9]\d{1,14}$/.test(phone)11}1213// Update phone number in the user's profile14async function updatePhoneNumber(phone: string) {15 const { data: { user } } = await supabase.auth.getUser()16 if (!user) throw new Error('Not authenticated')1718 if (!isValidPhone(phone)) {19 throw new Error('Phone must be in E.164 format: +14155551234')20 }2122 const { data, error } = await supabase23 .from('profiles')24 .update({ phone, updated_at: new Date().toISOString() })25 .eq('id', user.id)26 .select()27 .single()2829 if (error) throw error30 return data31}3233// Optional: Send OTP to verify phone before saving34async function sendPhoneOtp(phone: string) {35 if (!isValidPhone(phone)) {36 throw new Error('Phone must be in E.164 format: +14155551234')37 }3839 const { error } = await supabase.auth.signInWithOtp({40 phone,41 options: { shouldCreateUser: false }42 })43 if (error) throw error44}4546// Optional: Verify OTP and save phone to profile47async function verifyAndSavePhone(phone: string, token: string) {48 const { data, error } = await supabase.auth.verifyOtp({49 phone,50 token,51 type: 'sms'52 })53 if (error) throw error5455 await updatePhoneNumber(phone)56 return data57}5859// Get the current user's profile with phone60async function getProfile() {61 const { data: { user } } = await supabase.auth.getUser()62 if (!user) throw new Error('Not authenticated')6364 const { data, error } = await supabase65 .from('profiles')66 .select('id, display_name, phone, updated_at')67 .eq('id', user.id)68 .single()6970 if (error) throw error71 return data72}Common mistakes when adding a Phone Number to a Supabase User Profile
Why it's a problem: Storing phone numbers without a country code, making them ambiguous and incompatible with OTP verification
How to avoid: Always store phone numbers in E.164 format with the country code prefix (e.g., +14155551234). Validate the format before saving.
Why it's a problem: Forgetting to create an UPDATE RLS policy on the profiles table, causing phone number updates to silently fail
How to avoid: Create an explicit UPDATE policy that checks (select auth.uid()) = id. You also need a SELECT policy for the update to work.
Why it's a problem: Using signInWithOtp without setting shouldCreateUser: false, accidentally creating duplicate user accounts
How to avoid: When using phone OTP for verification (not signup), always set shouldCreateUser: false in the options object.
Best practices
- Store phone numbers in E.164 international format for consistency and compatibility
- Add a unique constraint on the phone column to prevent duplicate numbers across users
- Validate phone number format on the client before sending the update request
- Use getUser() instead of getSession() to get a server-verified user ID for profile lookups
- Create both SELECT and UPDATE RLS policies on the profiles table
- Use a trigger function to auto-create profile rows on user signup
- Verify phone numbers with OTP before saving them if you need confirmed contact information
- Update the updated_at timestamp whenever the profile changes for audit tracking
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I need to add a phone number field to my Supabase user profiles table. Show me the SQL to add the column, the RLS policies needed, and TypeScript code to update the phone number from the client with E.164 validation.
Help me add a phone number to my Supabase profiles table. I need the SQL migration, RLS policies for update, and client code to save the phone number. Also show me how to optionally verify it with SMS OTP.
Frequently asked questions
Should I store the phone number in auth.users or in a profiles table?
Store it in a profiles table in the public schema. The auth schema is not directly accessible via the API. A profiles table with a foreign key to auth.users is the standard Supabase pattern.
What format should I use for phone numbers?
Use E.164 format: a plus sign followed by the country code and number with no spaces or dashes (e.g., +14155551234). This is the international standard and works with Supabase phone OTP.
Do I need a Twilio account for phone OTP verification?
Yes. Supabase phone auth requires a Twilio or MessageBird account configured in Dashboard under Authentication > Providers > Phone. Without it, you can still store phone numbers but cannot send OTP codes.
Can I make the phone number optional?
Yes. Define the phone column without a NOT NULL constraint. It will default to null for users who have not added a phone number. Your client code should handle null values gracefully.
How do I prevent duplicate phone numbers?
Add a UNIQUE constraint: ALTER TABLE profiles ADD CONSTRAINT unique_phone UNIQUE (phone). This prevents two users from having the same phone number. If a duplicate is attempted, the insert or update will fail with a unique violation error.
Can RapidDev help build user profile features with Supabase?
Yes. RapidDev can help you build complete user profile systems including phone verification, avatar uploads, profile editing forms, and proper RLS security policies.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation