# How to Add a Phone Number to a Supabase User Profile

- Tool: Supabase
- Difficulty: Beginner
- Time required: 15-20 min
- Compatibility: Supabase (all plans), @supabase/supabase-js v2+
- Last updated: March 2026

## TL;DR

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.

## Before you start

- 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

### 1. 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.

```
-- If creating a new profiles table
create table public.profiles (
  id uuid not null references auth.users(id) on delete cascade,
  display_name text,
  phone text,
  updated_at timestamptz default now(),
  primary key (id)
);

-- If adding phone to an existing profiles table
alter table public.profiles
  add column phone text;

-- Optional: ensure phone numbers are unique
alter table public.profiles
  add constraint unique_phone unique (phone);
```

**Expected result:** The profiles table has a phone column of type text, optionally with a unique constraint.

### 2. 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.

```
-- Trigger function to create profile on signup
create or replace function public.handle_new_user()
returns trigger
language plpgsql
security definer set search_path = ''
as $$
begin
  insert into public.profiles (id, display_name)
  values (
    new.id,
    new.raw_user_meta_data ->> 'display_name'
  );
  return new;
end;
$$;

-- Attach trigger to auth.users
create trigger on_auth_user_created
  after insert on auth.users
  for each row execute function public.handle_new_user();
```

**Expected result:** Every new user signup automatically creates a profile row with their user ID.

### 3. 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.

```
-- Enable RLS
alter table public.profiles enable row level security;

-- Users can read their own profile
create policy "Users can view own profile"
  on public.profiles for select
  to authenticated
  using ((select auth.uid()) = id);

-- Users can update their own profile
create policy "Users can update own profile"
  on public.profiles for update
  to authenticated
  using ((select auth.uid()) = id)
  with check ((select auth.uid()) = id);
```

**Expected result:** RLS is enabled and authenticated users can only read and update their own profile row.

### 4. 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.

```
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

async function updatePhoneNumber(phone: string) {
  // Get the current user
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) throw new Error('Not authenticated')

  // Validate E.164 format
  const phoneRegex = /^\+[1-9]\d{1,14}$/
  if (!phoneRegex.test(phone)) {
    throw new Error('Phone must be in E.164 format: +14155551234')
  }

  // Update the profile
  const { data, error } = await supabase
    .from('profiles')
    .update({ phone, updated_at: new Date().toISOString() })
    .eq('id', user.id)
    .select()
    .single()

  if (error) throw error
  return data
}
```

**Expected result:** The user's profile is updated with the new phone number and the updated record is returned.

### 5. 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.

```
// Step 1: Send OTP to the phone number
async function sendPhoneOtp(phone: string) {
  const { error } = await supabase.auth.signInWithOtp({
    phone,
    options: { shouldCreateUser: false }
  })
  if (error) throw error
}

// Step 2: Verify the OTP code
async function verifyPhoneOtp(phone: string, token: string) {
  const { data, error } = await supabase.auth.verifyOtp({
    phone,
    token,
    type: 'sms'
  })
  if (error) throw error

  // Only save to profile after verification succeeds
  await updatePhoneNumber(phone)
  return data
}
```

**Expected result:** The phone number is verified via SMS OTP, then saved to the user's profile only after successful verification.

## Complete code example

File: `phone-profile.ts`

```typescript
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

// Validate phone number format (E.164)
function isValidPhone(phone: string): boolean {
  return /^\+[1-9]\d{1,14}$/.test(phone)
}

// Update phone number in the user's profile
async function updatePhoneNumber(phone: string) {
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) throw new Error('Not authenticated')

  if (!isValidPhone(phone)) {
    throw new Error('Phone must be in E.164 format: +14155551234')
  }

  const { data, error } = await supabase
    .from('profiles')
    .update({ phone, updated_at: new Date().toISOString() })
    .eq('id', user.id)
    .select()
    .single()

  if (error) throw error
  return data
}

// Optional: Send OTP to verify phone before saving
async function sendPhoneOtp(phone: string) {
  if (!isValidPhone(phone)) {
    throw new Error('Phone must be in E.164 format: +14155551234')
  }

  const { error } = await supabase.auth.signInWithOtp({
    phone,
    options: { shouldCreateUser: false }
  })
  if (error) throw error
}

// Optional: Verify OTP and save phone to profile
async function verifyAndSavePhone(phone: string, token: string) {
  const { data, error } = await supabase.auth.verifyOtp({
    phone,
    token,
    type: 'sms'
  })
  if (error) throw error

  await updatePhoneNumber(phone)
  return data
}

// Get the current user's profile with phone
async function getProfile() {
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) throw new Error('Not authenticated')

  const { data, error } = await supabase
    .from('profiles')
    .select('id, display_name, phone, updated_at')
    .eq('id', user.id)
    .single()

  if (error) throw error
  return data
}
```

## Common mistakes

- **Storing phone numbers without a country code, making them ambiguous and incompatible with OTP verification** — undefined Fix: Always store phone numbers in E.164 format with the country code prefix (e.g., +14155551234). Validate the format before saving.
- **Forgetting to create an UPDATE RLS policy on the profiles table, causing phone number updates to silently fail** — undefined Fix: Create an explicit UPDATE policy that checks (select auth.uid()) = id. You also need a SELECT policy for the update to work.
- **Using signInWithOtp without setting shouldCreateUser: false, accidentally creating duplicate user accounts** — undefined Fix: 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

## 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.

---

Source: https://www.rapidevelopers.com/supabase-tutorial/how-to-add-a-phone-number-to-supabase-user-profile
© RapidDev — https://www.rapidevelopers.com/supabase-tutorial/how-to-add-a-phone-number-to-supabase-user-profile
