• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / supabase-js / 16805913259

07 Aug 2025 01:29PM UTC coverage: 74.8%. Remained the same
16805913259

push

github

web-flow
feat: fallback to key - update realtime js to 2.15.0 (#1523)

* fix: fallback to key when no access token available

* feat: install realtime-js 2.13.0

* test: handle channel error recovery in WebSocket integration test

* feat: update realtime-js to 2.15.0

65 of 104 branches covered (62.5%)

Branch coverage included in aggregate %.

0 of 1 new or added line in 1 file covered. (0.0%)

122 of 146 relevant lines covered (83.56%)

34.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.0
/src/SupabaseClient.ts
1
import { FunctionsClient } from '@supabase/functions-js'
33✔
2
import { AuthChangeEvent } from '@supabase/auth-js'
3
import {
33✔
4
  PostgrestClient,
5
  PostgrestFilterBuilder,
6
  PostgrestQueryBuilder,
7
} from '@supabase/postgrest-js'
8
import {
33✔
9
  RealtimeChannel,
10
  RealtimeChannelOptions,
11
  RealtimeClient,
12
  RealtimeClientOptions,
13
} from '@supabase/realtime-js'
14
import { StorageClient as SupabaseStorageClient } from '@supabase/storage-js'
33✔
15
import {
33✔
16
  DEFAULT_GLOBAL_OPTIONS,
17
  DEFAULT_DB_OPTIONS,
18
  DEFAULT_AUTH_OPTIONS,
19
  DEFAULT_REALTIME_OPTIONS,
20
} from './lib/constants'
21
import { fetchWithAuth } from './lib/fetch'
33✔
22
import { ensureTrailingSlash, applySettingDefaults } from './lib/helpers'
33✔
23
import { SupabaseAuthClient } from './lib/SupabaseAuthClient'
33✔
24
import { Fetch, GenericSchema, SupabaseClientOptions, SupabaseAuthClientOptions } from './lib/types'
25

26
/**
27
 * Supabase Client.
28
 *
29
 * An isomorphic Javascript client for interacting with Postgres.
30
 */
31
export default class SupabaseClient<
33✔
32
  Database = any,
33
  SchemaName extends string & keyof Database = 'public' extends keyof Database
34
    ? 'public'
35
    : string & keyof Database,
36
  Schema extends GenericSchema = Database[SchemaName] extends GenericSchema
37
    ? Database[SchemaName]
38
    : any
39
> {
40
  /**
41
   * Supabase Auth allows you to create and manage user sessions for access to data that is secured by access policies.
42
   */
43
  auth: SupabaseAuthClient
44
  realtime: RealtimeClient
45
  /**
46
   * Supabase Storage allows you to manage user-generated content, such as photos or videos.
47
   */
48
  storage: SupabaseStorageClient
49

50
  protected realtimeUrl: URL
51
  protected authUrl: URL
52
  protected storageUrl: URL
53
  protected functionsUrl: URL
54
  protected rest: PostgrestClient<Database, SchemaName, Schema>
55
  protected storageKey: string
56
  protected fetch?: Fetch
57
  protected changedAccessToken?: string
58
  protected accessToken?: () => Promise<string | null>
59

60
  protected headers: Record<string, string>
61

62
  /**
63
   * Create a new client for use in the browser.
64
   * @param supabaseUrl The unique Supabase URL which is supplied when you create a new project in your project dashboard.
65
   * @param supabaseKey The unique Supabase Key which is supplied when you create a new project in your project dashboard.
66
   * @param options.db.schema You can switch in between schemas. The schema needs to be on the list of exposed schemas inside Supabase.
67
   * @param options.auth.autoRefreshToken Set to "true" if you want to automatically refresh the token before expiring.
68
   * @param options.auth.persistSession Set to "true" if you want to automatically save the user session into local storage.
69
   * @param options.auth.detectSessionInUrl Set to "true" if you want to automatically detects OAuth grants in the URL and signs in the user.
70
   * @param options.realtime Options passed along to realtime-js constructor.
71
   * @param options.storage Options passed along to the storage-js constructor.
72
   * @param options.global.fetch A custom fetch implementation.
73
   * @param options.global.headers Any additional headers to send with each network request.
74
   */
75
  constructor(
76
    protected supabaseUrl: string,
63✔
77
    protected supabaseKey: string,
63✔
78
    options?: SupabaseClientOptions<SchemaName>
79
  ) {
80
    if (!supabaseUrl) throw new Error('supabaseUrl is required.')
63✔
81
    if (!supabaseKey) throw new Error('supabaseKey is required.')
60✔
82

83
    const _supabaseUrl = ensureTrailingSlash(supabaseUrl)
57✔
84
    const baseUrl = new URL(_supabaseUrl)
57✔
85

86
    this.realtimeUrl = new URL('realtime/v1', baseUrl)
57✔
87
    this.realtimeUrl.protocol = this.realtimeUrl.protocol.replace('http', 'ws')
57✔
88
    this.authUrl = new URL('auth/v1', baseUrl)
57✔
89
    this.storageUrl = new URL('storage/v1', baseUrl)
57✔
90
    this.functionsUrl = new URL('functions/v1', baseUrl)
57✔
91

92
    // default storage key uses the supabase project ref as a namespace
93
    const defaultStorageKey = `sb-${baseUrl.hostname.split('.')[0]}-auth-token`
57✔
94
    const DEFAULTS = {
57✔
95
      db: DEFAULT_DB_OPTIONS,
96
      realtime: DEFAULT_REALTIME_OPTIONS,
97
      auth: { ...DEFAULT_AUTH_OPTIONS, storageKey: defaultStorageKey },
98
      global: DEFAULT_GLOBAL_OPTIONS,
99
    }
100

101
    const settings = applySettingDefaults(options ?? {}, DEFAULTS)
57✔
102

103
    this.storageKey = settings.auth.storageKey ?? ''
57!
104
    this.headers = settings.global.headers ?? {}
57!
105

106
    if (!settings.accessToken) {
57✔
107
      this.auth = this._initSupabaseAuthClient(
54✔
108
        settings.auth ?? {},
162!
109
        this.headers,
110
        settings.global.fetch
111
      )
112
    } else {
113
      this.accessToken = settings.accessToken
3✔
114

115
      this.auth = new Proxy<SupabaseAuthClient>({} as any, {
3✔
116
        get: (_, prop) => {
117
          throw new Error(
3✔
118
            `@supabase/supabase-js: Supabase Client is configured with the accessToken option, accessing supabase.auth.${String(
119
              prop
120
            )} is not possible`
121
          )
122
        },
123
      })
124
    }
125

126
    this.fetch = fetchWithAuth(supabaseKey, this._getAccessToken.bind(this), settings.global.fetch)
57✔
127
    this.realtime = this._initRealtimeClient({
57✔
128
      headers: this.headers,
129
      accessToken: this._getAccessToken.bind(this),
130
      ...settings.realtime,
131
    })
132
    this.rest = new PostgrestClient(new URL('rest/v1', baseUrl).href, {
57✔
133
      headers: this.headers,
134
      schema: settings.db.schema,
135
      fetch: this.fetch,
136
    })
137

138
    this.storage = new SupabaseStorageClient(
57✔
139
      this.storageUrl.href,
140
      this.headers,
141
      this.fetch,
142
      options?.storage
171✔
143
    )
144

145
    if (!settings.accessToken) {
57✔
146
      this._listenForAuthEvents()
54✔
147
    }
148
  }
149

150
  /**
151
   * Supabase Functions allows you to deploy and invoke edge functions.
152
   */
153
  get functions(): FunctionsClient {
154
    return new FunctionsClient(this.functionsUrl.href, {
3✔
155
      headers: this.headers,
156
      customFetch: this.fetch,
157
    })
158
  }
159

160
  // NOTE: signatures must be kept in sync with PostgrestClient.from
161
  from<
162
    TableName extends string & keyof Schema['Tables'],
163
    Table extends Schema['Tables'][TableName]
164
  >(relation: TableName): PostgrestQueryBuilder<Schema, Table, TableName>
165
  from<ViewName extends string & keyof Schema['Views'], View extends Schema['Views'][ViewName]>(
166
    relation: ViewName
167
  ): PostgrestQueryBuilder<Schema, View, ViewName>
168
  /**
169
   * Perform a query on a table or a view.
170
   *
171
   * @param relation - The table or view name to query
172
   */
173
  from(relation: string): PostgrestQueryBuilder<Schema, any, any> {
174
    return this.rest.from(relation)
×
175
  }
176

177
  // NOTE: signatures must be kept in sync with PostgrestClient.schema
178
  /**
179
   * Select a schema to query or perform an function (rpc) call.
180
   *
181
   * The schema needs to be on the list of exposed schemas inside Supabase.
182
   *
183
   * @param schema - The schema to query
184
   */
185
  schema<DynamicSchema extends string & keyof Database>(
186
    schema: DynamicSchema
187
  ): PostgrestClient<
188
    Database,
189
    DynamicSchema,
190
    Database[DynamicSchema] extends GenericSchema ? Database[DynamicSchema] : any
191
  > {
192
    return this.rest.schema<DynamicSchema>(schema)
3✔
193
  }
194

195
  // NOTE: signatures must be kept in sync with PostgrestClient.rpc
196
  /**
197
   * Perform a function call.
198
   *
199
   * @param fn - The function name to call
200
   * @param args - The arguments to pass to the function call
201
   * @param options - Named parameters
202
   * @param options.head - When set to `true`, `data` will not be returned.
203
   * Useful if you only need the count.
204
   * @param options.get - When set to `true`, the function will be called with
205
   * read-only access mode.
206
   * @param options.count - Count algorithm to use to count rows returned by the
207
   * function. Only applicable for [set-returning
208
   * functions](https://www.postgresql.org/docs/current/functions-srf.html).
209
   *
210
   * `"exact"`: Exact but slow count algorithm. Performs a `COUNT(*)` under the
211
   * hood.
212
   *
213
   * `"planned"`: Approximated but fast count algorithm. Uses the Postgres
214
   * statistics under the hood.
215
   *
216
   * `"estimated"`: Uses exact count for low numbers and planned count for high
217
   * numbers.
218
   */
219
  rpc<FnName extends string & keyof Schema['Functions'], Fn extends Schema['Functions'][FnName]>(
220
    fn: FnName,
221
    args: Fn['Args'] = {},
3✔
222
    options: {
6✔
223
      head?: boolean
224
      get?: boolean
225
      count?: 'exact' | 'planned' | 'estimated'
226
    } = {}
227
  ): PostgrestFilterBuilder<
228
    Schema,
229
    Fn['Returns'] extends any[]
230
      ? Fn['Returns'][number] extends Record<string, unknown>
231
        ? Fn['Returns'][number]
232
        : never
233
      : never,
234
    Fn['Returns'],
235
    FnName,
236
    null
237
  > {
238
    return this.rest.rpc(fn, args, options)
9✔
239
  }
240

241
  /**
242
   * Creates a Realtime channel with Broadcast, Presence, and Postgres Changes.
243
   *
244
   * @param {string} name - The name of the Realtime channel.
245
   * @param {Object} opts - The options to pass to the Realtime channel.
246
   *
247
   */
248
  channel(name: string, opts: RealtimeChannelOptions = { config: {} }): RealtimeChannel {
12✔
249
    return this.realtime.channel(name, opts)
12✔
250
  }
251

252
  /**
253
   * Returns all Realtime channels.
254
   */
255
  getChannels(): RealtimeChannel[] {
256
    return this.realtime.getChannels()
9✔
257
  }
258

259
  /**
260
   * Unsubscribes and removes Realtime channel from Realtime client.
261
   *
262
   * @param {RealtimeChannel} channel - The name of the Realtime channel.
263
   *
264
   */
265
  removeChannel(channel: RealtimeChannel): Promise<'ok' | 'timed out' | 'error'> {
266
    return this.realtime.removeChannel(channel)
3✔
267
  }
268

269
  /**
270
   * Unsubscribes and removes all Realtime channels from Realtime client.
271
   */
272
  removeAllChannels(): Promise<('ok' | 'timed out' | 'error')[]> {
273
    return this.realtime.removeAllChannels()
3✔
274
  }
275

276
  private async _getAccessToken() {
277
    if (this.accessToken) {
×
278
      return await this.accessToken()
×
279
    }
280

281
    const { data } = await this.auth.getSession()
×
282

NEW
283
    return data.session?.access_token ?? this.supabaseKey
×
284
  }
285

286
  private _initSupabaseAuthClient(
287
    {
288
      autoRefreshToken,
289
      persistSession,
290
      detectSessionInUrl,
291
      storage,
292
      storageKey,
293
      flowType,
294
      lock,
295
      debug,
296
    }: SupabaseAuthClientOptions,
297
    headers?: Record<string, string>,
298
    fetch?: Fetch
299
  ) {
300
    const authHeaders = {
57✔
301
      Authorization: `Bearer ${this.supabaseKey}`,
302
      apikey: `${this.supabaseKey}`,
303
    }
304
    return new SupabaseAuthClient({
57✔
305
      url: this.authUrl.href,
306
      headers: { ...authHeaders, ...headers },
307
      storageKey: storageKey,
308
      autoRefreshToken,
309
      persistSession,
310
      detectSessionInUrl,
311
      storage,
312
      flowType,
313
      lock,
314
      debug,
315
      fetch,
316
      // auth checks if there is a custom authorizaiton header using this flag
317
      // so it knows whether to return an error when getUser is called with no session
318
      hasCustomAuthorizationHeader: 'Authorization' in this.headers,
319
    })
320
  }
321

322
  private _initRealtimeClient(options: RealtimeClientOptions) {
323
    return new RealtimeClient(this.realtimeUrl.href, {
57✔
324
      ...options,
325
      params: { ...{ apikey: this.supabaseKey }, ...options?.params },
171!
326
    })
327
  }
328

329
  private _listenForAuthEvents() {
330
    let data = this.auth.onAuthStateChange((event, session) => {
54✔
331
      this._handleTokenChanged(event, 'CLIENT', session?.access_token)
54!
332
    })
333
    return data
54✔
334
  }
335

336
  private _handleTokenChanged(
337
    event: AuthChangeEvent,
338
    source: 'CLIENT' | 'STORAGE',
339
    token?: string
340
  ) {
341
    if (
54!
342
      (event === 'TOKEN_REFRESHED' || event === 'SIGNED_IN') &&
108!
343
      this.changedAccessToken !== token
344
    ) {
345
      this.changedAccessToken = token
×
346
    } else if (event === 'SIGNED_OUT') {
54!
347
      this.realtime.setAuth()
×
348
      if (source == 'STORAGE') this.auth.signOut()
×
349
      this.changedAccessToken = undefined
×
350
    }
351
  }
352
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc