How to write RLS Policies to ensure users can only edit their own profile data?
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 |
|---|---|---|
| SELECT | true | Publicly viewable profiles. |
| INSERT | auth.uid() = id | Users can only create their own record. |
| UPDATE | auth.uid() = id | Only the owner can edit. |
| DELETE | false | Nobody 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
UPDATEpolicy, it is often best to use both to prevent a user from accidentally changing theiridto 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
authschema. - [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.
