supabase-js icon indicating copy to clipboard operation
supabase-js copied to clipboard

Supabase Realtime not working with RLS

Open RentfireFounder opened this issue 2 years ago • 28 comments

Bug report

  • [x] I confirm this is a bug with Supabase, not with my own application.
  • [x] I confirm I have searched the Docs, GitHub Discussions, and Discord.

Describe the bug

Supabase Realtime doesn't get emit events when provided with table name, but works without table name in localhost

To Reproduce

Steps to reproduce the behavior, please provide code snippets or a repository:

  1. Went to /database/replication and turned on supabase_realtime
Screenshot 2023-10-10 at 3 07 12 PM
  1. Turned on table for which I want realtime events
Screenshot 2023-10-10 at 3 07 20 PM

In Javascript, If I do something like this, this won't work

supabase
            .client 
            .channel('user_list_changes')
            .on(
                'postgres_changes',
                {
                    event: '*',
                    schema: 'public',
                    table: 'users_list',
                },
                (payload: any) => console.log('payload', payload),
            )
            .subscribe();

but this would

supabase
           .client 
           .channel('user_list_changes')
           .on(
               'postgres_changes',
               {
                   event: '*',
                   schema: 'public',
               },
               (payload: any) => console.log('payload', payload),
           )
           .subscribe();

Expected behavior

I was expecting to receive events when provided with table_name

Screenshots

If applicable, add screenshots to help explain your problem.

System information

  • OS: Mac
  • Browser: Brave
  • Version of supabase-js:
   "@supabase/supabase-js@^2.10.0":
   "@supabase/functions-js" "^2.1.0"
    "@supabase/gotrue-js" "^2.46.1"
    "@supabase/postgrest-js" "^1.8.0"
    "@supabase/realtime-js" "^2.7.4"
    "@supabase/storage-js" "^2.5.1"
    ```
- Version of Node.js: 18.3.0

RentfireFounder avatar Oct 10 '23 09:10 RentfireFounder

I cannot reproduce this issue. Are you able to share a concrete repro-case and possibly console logs?

image

supabase.channel('filtered-channel')
  .on(
    'postgres_changes',
    {
      event: '*',
      schema: 'public',
      table: 'users_list'
    },
    (payload) => console.log('Filtered Channel Payload:', payload),
  )
  .subscribe();

supabase.channel('global-channel')
  .on(
    'postgres_changes',
    {
      event: '*',
      schema: 'public',
    },
    (payload) => console.log('Global Channel Payload:', payload),
  )
  .subscribe();

console.log('Listening for events');

kamilogorek avatar Oct 11 '23 10:10 kamilogorek

@kamilogorek

Both of these don't log on update when I have enabled RLS policy on table

    React.useEffect(() => {
        console.log('here');
        serviceSubscription.current = supabase
            .getClient()
            .channel(`service-data-${serviceName}`)
            .on(
                'postgres_changes',
                {
                    event: '*',
                    schema: 'public',
                    table: 'services_connected',
                },
                (payload) => console.log('Change', payload),
            )
            .subscribe();

        supabase
            .getClient()
            .channel(`service-data`)
            .on(
                'postgres_changes',
                {
                    event: '*',
                    schema: 'public',
                },
                (payload) => console.log('Global', payload),
            )
            .subscribe();
    }, []);

Where

    
class Supabase {
  client: SupabaseClient<Database>;

  constructor() {
    const token = localStorage.getItem(SUPABASE_LOCAL_STORAGE_TOKEN_NAME);
    if (token) {
      this.client = createClient<Database>(VITE_SUPABASE_URL, VITE_SUPABASE_ANON_KEY, {
        global: {
          headers: {
            Authorization: `Bearer ${token}`,
          },
        },
      });
    } else {
      this.client = createClient<Database>(VITE_SUPABASE_URL, VITE_SUPABASE_ANON_KEY);
    }
  }

  createClient(authToken: string) {
    this.client = createClient<Database>(VITE_SUPABASE_URL, VITE_SUPABASE_ANON_KEY, {
      global: {
        headers: {
          Authorization: `Bearer ${authToken}`,
        },
      },
    });
  }

  getClient(): SupabaseClient<Database> {
    return this.client;
  }
}

const supabase = new Supabase();

Here is my RLS policy. (Normal Select queries work as expected).

Screenshot 2023-10-21 at 5 52 35 AM Screenshot 2023-10-21 at 5 52 21 AM

RentfireFounder avatar Oct 21 '23 00:10 RentfireFounder

@kamilogorek Any update on this?

RentfireFounder avatar Oct 30 '23 12:10 RentfireFounder

@RentfireFounder it's working locally but not working when using Supabase infrastructure?

filipecabaco avatar Oct 30 '23 14:10 filipecabaco

@filipecabaco no it's working when RLS is disabled but not working when RLS is enabled.

With RLS enabled, the select/update and normal queries work but realtime doesn't

if this is the problem with my RLS rules, why is normal queries working and not realtime?

Also, more on this comment: https://github.com/supabase/supabase-js/issues/1733

RentfireFounder avatar Oct 30 '23 19:10 RentfireFounder

@RentfireFounder just to confirm you're spinning up the Supabase stack locally correct? This is a locally running version of Realtime that RLS is not working for you. Is that right?

w3b6x9 avatar Oct 30 '23 20:10 w3b6x9

@w3b6x9 Yap!

RentfireFounder avatar Oct 30 '23 20:10 RentfireFounder

@w3b6x9 @filipecabaco @kamilogorek were you able to replicate it? any updatea?

RentfireFounder avatar Oct 31 '23 22:10 RentfireFounder

@RentfireFounder sorry for the radio silence, we had a couple of outstanding issues...

One of them could be related with this: what is the URL you are using to connect?

filipecabaco avatar Nov 20 '23 19:11 filipecabaco

Hello, i got the same error... my RLS are well executed (log in a postgres function) but the realtime does not return the change @filipecabaco

victorbillaud avatar Dec 12 '23 18:12 victorbillaud

Exact same thing is happening with me, (working with only prod) with RLS turned on i only get DELETE events. No matter what RLS policy I add I dont see any events coming in. Like the OP said on turning RLS off again postgres changes start flowing again.

UPDATE: I added a SELECT policy with anon role set as TRUE (not recommended but it worked for now and can carry on with the dev)

roxxid avatar Dec 29 '23 16:12 roxxid

@filipecabaco @w3b6x9 i am using localhost

RentfireFounder avatar Jan 03 '24 07:01 RentfireFounder

hey everyone, sorry we were working on a lot of changes and were not able to dedicate enough time to close this issue.

is this issue persisted? were you able to tackle it?

filipecabaco avatar Mar 27 '24 17:03 filipecabaco

@filipecabaco nope, Also, just in case, did you see this comment? could it be because I am creating my own jwt and using that>

RentfireFounder avatar Mar 27 '24 20:03 RentfireFounder

We do have an issue currently with custom jwts where the errors are not properly shown to the user 😞 I wonder if this is related 🤔

filipecabaco avatar Mar 27 '24 21:03 filipecabaco

Exact same thing is happening with me, (working with only prod) with RLS turned on i only get DELETE events. No matter what RLS policy I add I dont see any events coming in. Like the OP said on turning RLS off again postgres changes start flowing again.

UPDATE: I added a SELECT policy with anon role set as TRUE (not recommended but it worked for now and can carry on with the dev)

I'm having the same issue. Any updates here?

rogaha avatar Jan 17 '25 00:01 rogaha

Having the same issue. Has anyone been able to solve this?

kafkas avatar Jan 28 '25 14:01 kafkas

Same for me, anyone had any luck?

therealtimhawkins avatar Feb 16 '25 22:02 therealtimhawkins

I'm also experiencing this issue. I'm using an RLS policy that checks a custom JWT created using the supabase jwt secret (with the user matching a third party auth token). My RLS policy is working fine with regular requests but not realtime requests.

When I turn off the RLS (or just set it to USING (true)) I get all the events. When I have it on I only get DELETE events.

andrew-wang-polar avatar Apr 03 '25 00:04 andrew-wang-polar

I was able to get it working. See this article: https://liviogamassia.medium.com/using-supabase-rls-with-firebase-auth-custom-auth-provider-357eaad9c70f for a solution.

In my case I was already correctly setting role and aud = 'authenticated', but the thing I needed to do was also use realtime.setAuth(supabaseJwt):

const supabaseJwt = generateSupabaseJwt(
  supabaseJwtInfo,
  SUPABASE_JWT_SECRET
);
const supabaseClient = createClient<Database>(
  SUPABASE_URL,
  SUPABASE_ANON_KEY,
  {
    auth: {
      persistSession: false,
      autoRefreshToken: false,
      detectSessionInUrl: false,
    },
    global: {
      headers: {
        Authorization: `Bearer ${supabaseJwt}`,
      },
    },
  }
);
supabaseClient.realtime.setAuth(supabaseJwt);

andrew-wang-polar avatar Apr 03 '25 01:04 andrew-wang-polar

Root Cause When using custom JWT tokens with Supabase: Regular API calls use the Authorization header in global.headers Realtime subscriptions need a separate authentication call via realtime.setAuth() Without realtime.setAuth(), realtime runs as anonymous and RLS blocks it

Key Points ✅ Both global.headers.Authorization AND realtime.setAuth() are required ✅ Call realtime.setAuth() immediately after client creation ✅ Call realtime.setAuth() whenever you update tokens ✅ This works with any custom JWT token (Firebase Auth, custom auth, etc.)

@filipecabaco I think this can get closed?

melmathari avatar Sep 19 '25 08:09 melmathari

Potentially yes but we did release a new version of supabase-js that I hope will remove the need to have setAuth

https://github.com/supabase/supabase-js/pull/1551

the issue seemed to have been a mix of problems:

  • not calling it for sign in changes ( 🤦 )
  • using binded functions might have caused issues a timing issue when assigning the token so calling it directly will reduce the probability of that happening

filipecabaco avatar Sep 19 '25 15:09 filipecabaco

Wondering if my issues are something to do with this. Since updating

"@supabase/supabase-js": "2.53.0" -> "@supabase/supabase-js": "2.57.4", which included a few real time changes from this package, my realtime subscriptions broke 9/10 times. Occasionally they would work surprisingly but most of the time they wouldn't.

Downgrading my package again fixed the issue.

I have realtime all throughout my app and have a base hook that higher-order hooks leverage:

const realtimeState = useRealtimeBase({
    config: {
      channelName: `item:${id}:items`,
      table: 'my_table',
      event: RealtimeEvent.All,
      filter: `myId=eq.${myId}`,
      enabled: enabled && !!myId,
      debugLabel: '...',
    },
    onPayload: handlePayload,
    dependencies: [threadId],
  });

and in my realtime base I have something like

const channel = supabase
        .channel(channelName)
        .on(
          'postgres_changes',
          {
            schema: 'public',
            table,
            event,
            ...(filter && { filter }),
          },
          (payload: RealtimePostgresChangesPayload<Record<string, object>>) => {
            enableLogging && log('Received payload:', payload);
            onPayloadRef.current?.(payload);
          },
        )
        .subscribe(handleStatus);

      channelRef.current = channel;
    };

maxtuzz avatar Sep 25 '25 05:09 maxtuzz

Hi @mandarini ,

Are there plans to fix this issue, this issue makes realtime impossible for multi tenant apps that use a x-org-slug header to determine what access to allow.

IdrisCelik avatar Oct 22 '25 13:10 IdrisCelik

@IdrisCelik for multi tenant applications I would advise to use a custom JWT that includes the organisation so that way you can use those claims in the RLS policies instead of using headers.

Headers are very limitative when it comes to establishing websocket connections so we opted to go with this route instead

filipecabaco avatar Oct 23 '25 09:10 filipecabaco

Check out the docs here:

  • https://supabase.com/docs/guides/auth/auth-hooks/custom-access-token-hook
  • https://supabase.com/docs/guides/auth/jwts?queryGroups=language&language=ts Specifically this: https://supabase.com/docs/guides/auth/jwts?queryGroups=language&language=ts#using-custom-or-third-party-jwts

mandarini avatar Oct 23 '25 10:10 mandarini

@IdrisCelik for multi tenant applications I would advise to use a custom JWT that includes the organisation so that way you can use those claims in the RLS policies instead of using headers.

Headers are very limitative when it comes to establishing websocket connections so we opted to go with this route instead

Hi @filipecabaco ,

Thanks for your reply! Unfortunatly this doesn't cut it for my app requirements. Having a custom claim currentTenantId would definitly work. But it will break when the same auth user wants to use one tenant on one device, and another on another device. Since claims are set for the auth user and not a specific device for the auth user.

IdrisCelik avatar Oct 23 '25 10:10 IdrisCelik

Hey, I was experiencing similar issues where RLS was setup correctly, and Realtime worked fine when RLS was disabled. However, when RLS was enabled, no changes were sent back to the client (even though standard SELECT queries worked fine).

The issue for me was that the socket was establishing the connection before the Auth Session was fully loaded. When the component mounts, there is a brief delay while Supabase retrieves the session token from storage. If you call .subscribe() immediately, the WebSocket connects using the anon role. Consequently, any RLS policy relying on auth.uid() fails silently, blocking the events.

I fixed it by wrapping the subscription in an async function and explicitly awaiting the session before subscribing.

Maybe this will be of help to someone :D

`useEffect(() => { const supabase = createClient(); let channel = null;

const setupSubscription = async () => { // 1. CRITICAL: Wait for the session to load. // Without this, the socket connects as 'anon' and RLS fails. const { data: { session } } = await supabase.auth.getSession();

if (!session) return;

// 2. Now subscribe (Supabase will use the authenticated token)
channel = supabase
  .channel("my_channel")
  .on(
    "postgres_changes",
    { event: "INSERT", schema: "public", table: "my_table" },
    (payload) => {
      console.log("Change received!", payload);
    }
  )
  .subscribe();

};

setupSubscription();

return () => { if (channel) supabase.removeChannel(channel); }; }, []);`

Alexiukas avatar Nov 23 '25 01:11 Alexiukas