Skip to content

Realtime does not include global headers from createClient in WebSocket handshake — RLS policies that check request.headers fail #1797

@IdrisCelik

Description

@IdrisCelik

Describe the bug

Problem summary

When a client sets custom headers via createClient(..., { global: { headers: { 'x-rls': 'true' } } }), those headers are available on normal HTTP requests but are not present on the Realtime WebSocket connection used for Postgres postgres_changes events. If an RLS policy relies on current_setting('request.headers', true) (or otherwise expects that header during Realtime evaluation), the policy evaluates to false and the client receives no realtime messages. Turning RLS off makes the events deliver, confirming this is about missing connection metadata. This appears to be a mismatch between how the SDK sets global.headers (HTTP) and what the Realtime server actually receives during the WS handshake.

Why this is important

Developers commonly use custom headers for tenant identification or extra metadata in multi-tenant setups. RLS policies that depend on such headers work fine for REST requests (via request.headers) but break for Realtime because the WebSocket handshake from browsers doesn't carry arbitrary custom headers. That makes it difficult to implement secure, tenant-aware realtime subscriptions without embedding tenant info in the JWT or using other workarounds. These workarounds are bad since embedding the current tenant in JWT means that it breaks when wanting to use 2 different tenants on 2 different devices.

If websockets dont support headers, Supabase js sdk could just include the header as metadata when setting up the websocket and threat that as the header in RLS.

Library affected

supabase-js

Reproduction

No response

Steps to reproduce

Create following table with rls:

-- create table
create table public.realtime_test (
  id serial primary key,
  content text
);

-- enable RLS
alter table public.realtime_test enable row level security;

-- policy: allow SELECT only if header x-rls is present and equals 'true'
create policy "allow-select-if-x-rls-true" on public.realtime_test
  for select using (
    (current_setting('request.headers', true)::json ->> 'x-rls') = 'true'
  );

Have a client listen:

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

const SUPABASE_URL = 'https://your-project.supabase.co';
const SUPABASE_ANON_KEY = '...';

const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
  // developer sets a custom header globally
  global: {
    headers: {
      'x-rls': 'true'
    }
  }
});

// subscribe to postgres changes
const channel = supabase
  .channel('public:realtime_test')
  .on('postgres_changes', { schema: 'public', table: 'realtime_test', event: 'INSERT' }, payload => {
    console.log('received payload', payload);
  })
  .subscribe();

Insert a row and observe in the network tab no message appearing inside the WS connection

System Info

System:
    OS: Windows 11 10.0.26100
    CPU: (16) x64 AMD Ryzen 7 7840HS w/ Radeon 780M Graphics
    Memory: 2.89 GB / 15.29 GB
  Binaries:
    Node: 22.18.0 - C:\nvm4w\nodejs\node.EXE
    npm: 11.5.2 - C:\nvm4w\nodejs\npm.CMD
    pnpm: 10.18.2 - C:\nvm4w\nodejs\pnpm.CMD
  Browsers:
    Chrome: 141.0.7390.108
    Edge: Chromium (139.0.3405.102)
  npmPackages:
    @supabase/supabase-js: ^2.75.0 => 2.75.0
    supabase: ^2.51.0 => 2.51.0

Used Package Manager

pnpm

Logs

No response

Validations

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingrealtime-jsRelated to the realtime-js library.supabase-jsRelated to the supabase-js library.

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions