Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions packages/core/supabase-js/src/SupabaseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,10 +369,29 @@ export default class SupabaseClient<
})
}

private _initRealtimeClient(options: RealtimeClientOptions) {
private _initRealtimeClient(
options: RealtimeClientOptions & { includeHeadersInParams?: string[] }
) {
const { includeHeadersInParams, ...realtimeOptions } = options

// Copy specified headers to params for WebSocket handshake
const paramsFromHeaders: Record<string, string> = {}
if (includeHeadersInParams && Array.isArray(includeHeadersInParams)) {
includeHeadersInParams.forEach((headerName) => {
const headerValue = this.headers[headerName]
if (headerValue !== undefined) {
paramsFromHeaders[headerName] = headerValue
}
})
}

return new RealtimeClient(this.realtimeUrl.href, {
...options,
params: { ...{ apikey: this.supabaseKey }, ...options?.params },
...realtimeOptions,
params: {
apikey: this.supabaseKey,
...paramsFromHeaders,
...realtimeOptions?.params, // User params can override
},
})
}

Expand Down
18 changes: 17 additions & 1 deletion packages/core/supabase-js/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,23 @@ export type SupabaseClientOptions<SchemaName> = {
/**
* Options passed to the realtime-js instance
*/
realtime?: RealtimeClientOptions
realtime?: RealtimeClientOptions & {
/**
* Array of header names from `global.headers` to include as WebSocket query parameters.
* This enables RLS policies that check `request.headers` to work with realtime subscriptions.
*
* @example
* ```typescript
* createClient(url, key, {
* global: { headers: { 'x-tenant-id': 'tenant-123' } },
* realtime: { includeHeadersInParams: ['x-tenant-id'] }
* })
* ```
*
* Security: Only explicitly listed headers are included. Avoid including sensitive data.
*/
includeHeadersInParams?: string[]
}
storage?: StorageClientOptions
global?: {
/**
Expand Down
107 changes: 107 additions & 0 deletions packages/core/supabase-js/test/unit/SupabaseClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,113 @@ describe('SupabaseClient', () => {
})
})

describe('Realtime Headers in Params', () => {
test('should copy specified headers to realtime params', () => {
const client = createClient(URL, KEY, {
global: {
headers: {
'x-tenant-id': 'tenant-123',
'x-other-header': 'other-value',
},
},
realtime: {
includeHeadersInParams: ['x-tenant-id'],
},
})

// @ts-ignore - accessing private property
expect(client.realtime.params['x-tenant-id']).toBe('tenant-123')
// @ts-ignore - accessing private property
expect(client.realtime.params['x-other-header']).toBeUndefined()
})

test('should not copy headers if includeHeadersInParams is not specified', () => {
const client = createClient(URL, KEY, {
global: {
headers: {
'x-tenant-id': 'tenant-123',
},
},
})

// @ts-ignore - accessing private property
expect(client.realtime.params['x-tenant-id']).toBeUndefined()
})

test('should allow explicit realtime params to override headers', () => {
const client = createClient(URL, KEY, {
global: {
headers: {
'x-tenant-id': 'tenant-from-header',
},
},
realtime: {
includeHeadersInParams: ['x-tenant-id'],
params: {
'x-tenant-id': 'tenant-override',
},
},
})

// @ts-ignore - accessing private property
expect(client.realtime.params['x-tenant-id']).toBe('tenant-override')
})

test('should handle missing headers gracefully', () => {
const client = createClient(URL, KEY, {
global: {
headers: {
'x-existing': 'value',
},
},
realtime: {
includeHeadersInParams: ['x-non-existent', 'x-existing'],
},
})

// @ts-ignore - accessing private property
expect(client.realtime.params['x-non-existent']).toBeUndefined()
// @ts-ignore - accessing private property
expect(client.realtime.params['x-existing']).toBe('value')
})

test('should handle empty includeHeadersInParams array', () => {
const client = createClient(URL, KEY, {
global: {
headers: {
'x-tenant-id': 'tenant-123',
},
},
realtime: {
includeHeadersInParams: [],
},
})

// @ts-ignore - accessing private property
expect(client.realtime.params['x-tenant-id']).toBeUndefined()
})

test('should maintain backward compatibility when no includeHeadersInParams', () => {
const client = createClient(URL, KEY, {
global: {
headers: {
'x-tenant-id': 'tenant-123',
},
},
realtime: {
params: {
'custom-param': 'custom-value',
},
},
})

// @ts-ignore - accessing private property
expect(client.realtime.params['custom-param']).toBe('custom-value')
// @ts-ignore - accessing private property
expect(client.realtime.params['x-tenant-id']).toBeUndefined()
})
})

describe('Schema Management', () => {
test('should switch schema', () => {
const client = createClient<Database>(URL, KEY)
Expand Down
Loading