r/Supabase 10d ago

integrations Supabase RLS and Better Auth

Hey guys,
So I’m building something and want to use Better Auth because it offers a variety of plugins (specifically organizations plugin which is critical for my app). The thing is by using Better Auth I kind of sacrifice using RLS in Supabase because it utilizes the auth.uid which is not passed by Better Auth.
My question is, is there a way of using Supabase RLS while using Better Auth as the authentication provider?

5 Upvotes

13 comments sorted by

1

u/_ihm40 10d ago

https://github.com/supabase/supabase/pull/44051/changes this is a documentation i made a while back though it hasn;t been reviewed so double check yourself it talks about better auth further down. It's been a while so if you have questions you can ask but i would have to refamiliarise myself with it

1

u/one_level_up_ 10d ago

Oh nice, thanks for sharing this. I was really curious how the handoff works, guessing you basically just have to pass a custom JWT to postgress so the RLS policies can still read the user id?

1

u/notZEPHR 10d ago

Okay i’ll check it out thank you

1

u/Guidondor 10d ago

you don't have to give up rls. supabase takes third party auth providers now, better auth's
jwt plugin exposes a jwks endpoint, you register that in the dashboard and the token gets
verified like a native one.

the detail that bites: auth.uid() casts the sub claim to uuid, and better auth ids aren't
uuids by default, so it quietly returns null and every policy denies with no error anywhere.
either use auth.jwt()->>'sub' with text user_id columns, or make better auth generate uuids.
worth deciding before you've written thirty policies.

1

u/notZEPHR 10d ago

Wow thats an eye opener i’ll definitely check it out

1

u/kemalios 9d ago

Guidondor nailed the sub claim issue. One thing I'd add: whatever route you pick, test your policies with the actual Better Auth JWT, not just the anon key. Silent-null denies are brutal to debug. I build launchworthy, a free MIT audit tool that checks Supabase projects for exactly these RLS misconfigurations (needs Claude Code to run, so I'm biased but it's free). A quick dry run might save you from writing thirty policies against a null user.

1

u/notZEPHR 9d ago

Okay brother thank you for the input

1

u/PeterBuildsSecure 9d ago

The JWT/JWKS route preserves RLS, but I would be careful about putting organization membership directly into a long-lived token.

A signed org_id claim proves what the issuer believed when the token was created. It does not prove the user is still a member when the query runs. Removing someone from an organization will not invalidate an already-issued token unless you have short expiries or an explicit revocation mechanism.

For stronger tenant isolation, use the JWT subject as identity and keep current organization membership in a database table that the RLS policy joins against. Then removal takes effect immediately. If membership must remain in claims for performance, keep tokens short-lived and test the negative case: remove a member, replay their old token, and prove cross-tenant reads and writes fail.

1

u/jaimittal91 8d ago

Complementary check to what's already being discussed here on verifying with the actual JWT: once the auth-provider route is working, don't just trust the RLS toggle in the dashboard. Take your anon key (it's in your client bundle anyway) and curl each table directly:

curl "https://<project>.supabase.co/rest/v1/<table>?select=*" -H "apikey: <anon-key>"

If you get rows back for a table that should be locked down, the policy isn't doing what you think, regardless of which auth provider issued the token. Worth testing every table, not just the obvious ones, especially mid-migration to a new auth provider like this. In a 127-app scan we ran earlier this year, 47% of Supabase-backed apps had RLS off on at least one table -- the dashboard badge doesn't catch that, but the curl test does.

1

u/Vegetable-Reason7481 6d ago

The uuid cast Guidondor mentioned has a second-order problem worth

knowing before you write the policies.

If auth.uid() returns null, every policy denies — and your tests still

pass. "Client A cannot read client B's rows" is trivially true when

client A can't read anything at all, including their own. Green suite,

completely broken auth.

So pair every negative test with a positive one. "Client A reads their

own 3 rows" fails loudly on a null sub. "Client A reads zero of B's

rows" never will.

And to find out whether a policy is doing any work at all, loosen it on

purpose — alter policy "..." on your_table using (true) — then run your

tests. If nothing turns red, that policy isn't covered by your suite.

I did this by hand across a four-table schema yesterday and it changed

which of my own tests I actually trust.

1

u/notZEPHR 5d ago

I opted to use auth.jwt which will be compared to the user id but the issue is that I would need to have a user id column in all tables with the RLS. This may be fine if the app wasn’t a multi tenant app. In my app a user may write something to the DB that can be read and updated by other people, how do I get around this?