Skip to main content

How to write RLS Policies to ensure users can only edit their own profile data?

· 5 min read
Serhii Hrekov
software engineer, creator, artist, programmer, projects founder

In a traditional setup, you'd write a bunch of backend code to check if session.user.id === profile.id. In Supabase, you just tell the database the rules and let it act as the bouncer.

Row Level Security (RLS) is the engine that ensures your users can see a thousand profiles but can only click "Edit" on their own. Here is how to configure it correctly in 2026.

🏗️ The "ID Match" Logic

The secret sauce of Supabase RLS is a built-in function called auth.uid(). This function extracts the Unique ID of the user making the request directly from their JWT (JSON Web Token).

To secure a profiles table, we simply compare the ID of the row being touched to the ID of the user currently logged in.


💻 The SQL Implementation

You can run these commands in the Supabase SQL Editor.

1. Enable RLS

By default, your table is either open to the public or locked completely. You must explicitly turn on the "Security Valve" first.

ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;

2. Allow "Read Only" for Everyone

Usually, you want users to be able to see each other's basic profile info (like a name or avatar).

CREATE POLICY "Profiles are viewable by everyone"
ON profiles FOR SELECT
USING (true); -- 'true' means no restriction on viewing

3. Restrict "Updates" to the Owner

This is the policy that prevents someone from opening the console and changing your username.

CREATE POLICY "Users can update their own profiles"
ON profiles FOR UPDATE
USING (auth.uid() = id); -- Only succeeds if the IDs match

📊 Standard Policy Templates

Action (CMD)Policy Logic (USING / WITH CHECK)Result
SELECTtruePublicly viewable profiles.
INSERTauth.uid() = idUsers can only create their own record.
UPDATEauth.uid() = idOnly the owner can edit.
DELETEfalseNobody can delete (safer for profiles).

🛡️ Critical: USING vs WITH CHECK

When writing these policies, you'll see two different clauses. Here is the difference:

  • USING: Applies to rows already in the database. (e.g., "Can I see this row?" or "Can I find this row to update it?")
  • WITH CHECK: Applies to the new data you are trying to push. (e.g., "Is the new ID I'm trying to insert actually my ID?")

Pro Tip: For an UPDATE policy, it is often best to use both to prevent a user from accidentally changing their id to someone else's: ... USING (auth.uid() = id) WITH CHECK (auth.uid() = id);


⚠️ Common Pitfalls

1. The Service Role Bypass

Remember that the service_role key (used in backend scripts or Edge Functions) bypasses RLS entirely. If your code isn't working as expected, double-check that your frontend is using the anon key.

2. Table Column Names

In this example, I assumed your primary key is named id. If your column is named user_id or owner, you must update the policy: USING (auth.uid() = owner_id);

3. Testing with "Impersonation"

Supabase has a "User Impersonation" feature in the dashboard. Use it! You can select a specific user and browse the data to see exactly what they can (and cannot) see based on your new policies.


📚 Sources & Technical Refs

  • [1.1] Supabase Docs: Row Level Security - Official guide on the auth schema.
  • [2.1] PostgreSQL Docs: CREATE POLICY - Technical deep-dive into the Postgres policy engine.
  • [3.1] PostgREST: Lifting the Veil - How JWTs are translated into database roles.

Related articles