Class: Supabase::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/supabase/client.rb

Overview

Top-level client that combines every sub-library behind one object, mirroring supabase-py’s ‘supabase.create_client()`.

client = Supabase.create_client(
  supabase_url: "https://project.supabase.co",
  supabase_key: ENV["SUPABASE_ANON_KEY"]
)

client.auth.(email:, password:)
users = client.from("users").select("*").execute
client.storage.from("avatars").upload("a.png", bytes)
client.functions.invoke("hello-world", body: { name: "Ada" })
ch = client.realtime.channel("realtime:public:users")

Sub-clients are built lazily and memoized. Pass ‘async: true` to swap in the async-http-faraday variants for Auth / Postgrest / Storage / Functions; the Realtime client is transport-agnostic and ships sync regardless (a real WS transport is wired in by the caller — see lib/supabase/realtime/socket.rb).

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(supabase_url:, supabase_key:, options: {}, async: false) ⇒ Client

Returns a new instance of Client.

Raises:

  • (err)


65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/supabase/client.rb', line 65

def initialize(supabase_url:, supabase_key:, options: {}, async: false)
  # Use Supabase::SupabaseException once defined; fall back to ArgumentError
  # during early require cycles. Matches supabase-py's contract.
  err = defined?(Supabase::SupabaseException) ? Supabase::SupabaseException : ArgumentError
  raise err, "supabase_url is required" if supabase_url.to_s.empty?
  raise err, "supabase_key is required" if supabase_key.to_s.empty?
  raise err, "Invalid URL" unless supabase_url.to_s.match?(%r{^https?://.+})

  @supabase_url = supabase_url.to_s.chomp("/")
  @supabase_key = supabase_key
  # Plain Hash → ClientOptions: paritet with supabase-py, where every
  # option flows through a typed dataclass. The legacy nested
  # `{ auth: {...}, postgrest: {...}, global: { headers: {...} } }` shape
  # is kept as a raw Hash so existing callers don't break — anything
  # else is canonicalized into a ClientOptions struct so the per-sub-
  # client kwargs derivation has one code path.
  legacy_hash_shape = options.is_a?(Hash) && legacy_options_hash?(options)
  warn_stray_legacy_keys(options) if legacy_hash_shape

  @options =
    if options.is_a?(Hash) && !legacy_hash_shape
      ClientOptions.new(**options.transform_keys(&:to_sym))
    elsif options.is_a?(Supabase::ClientOptions)
      # Mirror supabase-py's `self.options = copy.copy(options)` followed by
      # `self.options.headers = {**options.headers, ...}` — both the struct
      # and its headers hash become unique to this client, so a downstream
      # `client.options.headers["X"] = ...` mutation can't leak across
      # clients constructed from the same `ClientOptions` instance (F-C?).
      isolated = options.dup
      isolated.headers = isolated.headers.dup
      isolated
    else
      options
    end
  @async        = async

  configured_headers =
    if @options.is_a?(Supabase::ClientOptions)
      @options.headers
    else
      @options[:global]&.dig(:headers) || @options.dig("global", "headers") || {}
    end

  @headers = {
    "apikey"        => @supabase_key,
    "Authorization" => "Bearer #{@supabase_key}"
  }.merge(configured_headers || {})

  # Current access token used to authorize the data-plane sub-clients
  # (postgrest/storage/functions) and the realtime socket. Starts as the
  # anon key and is rotated by #apply_auth on sign-in / token refresh. Held
  # explicitly (rather than re-derived from @headers) so that a realtime
  # client built LAZILY after a sign-in still picks up the session token
  # instead of the anon key — see #realtime.
  @access_token = @supabase_key
end

Instance Attribute Details

#headersObject (readonly)

Returns the value of attribute headers.



31
32
33
# File 'lib/supabase/client.rb', line 31

def headers
  @headers
end

#optionsObject (readonly)

Returns the value of attribute options.



31
32
33
# File 'lib/supabase/client.rb', line 31

def options
  @options
end

#supabase_keyObject (readonly)

Returns the value of attribute supabase_key.



31
32
33
# File 'lib/supabase/client.rb', line 31

def supabase_key
  @supabase_key
end

#supabase_urlObject (readonly)

Returns the value of attribute supabase_url.



31
32
33
# File 'lib/supabase/client.rb', line 31

def supabase_url
  @supabase_url
end

Class Method Details

.create(supabase_url:, supabase_key:, options: nil, async: false) ⇒ Object

Mirrors supabase-py’s ‘Client.create(…)`: builds a client, then — if no explicit Authorization was supplied via options — tries to pull a persisted session via the auth client and applies its access_token as the bearer token. Useful when bootstrapping from a session file the user previously signed into. Any error from get_session is swallowed so the client always returns successfully.



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/supabase/client.rb', line 39

def self.create(supabase_url:, supabase_key:, options: nil, async: false)
  configured_auth = nil
  if options.is_a?(Supabase::ClientOptions)
    configured_auth = options.headers["Authorization"] || options.headers[:Authorization]
  elsif options.is_a?(Hash)
    configured_headers = options[:global]&.dig(:headers) || options.dig("global", "headers") ||
                         options[:headers] || options["headers"] || {}
    configured_auth = configured_headers["Authorization"] || configured_headers[:Authorization]
  end

  client = new(supabase_url: supabase_url, supabase_key: supabase_key,
               options: options || {}, async: async)

  if configured_auth.nil?
    begin
      session = client.auth.get_session
      client.set_auth(session.access_token) if session&.access_token
    rescue StandardError
      # No persisted session, or auth storage unavailable — fall back to
      # the apikey-only bearer that initialize set up.
    end
  end

  client
end

Instance Method Details

#async?Boolean

Returns:

  • (Boolean)


122
123
124
# File 'lib/supabase/client.rb', line 122

def async?
  @async
end

#authObject

— Sub-clients ———————————————————



128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/supabase/client.rb', line 128

def auth
  return @auth if @auth

  @auth = auth_class.new(url: rest_url_for("auth/v1"), headers: @headers, **sub_options(:auth))
  # Mirror supabase-py's `self.auth.on_auth_state_change(self._listen_to_auth_events)`:
  # when the auth client emits SIGNED_IN / TOKEN_REFRESHED / SIGNED_OUT,
  # propagate the new token to every other sub-client.
  @auth.on_auth_state_change do |event, session|
    next unless %w[SIGNED_IN TOKEN_REFRESHED SIGNED_OUT].include?(event)

    apply_auth(session&.access_token)
  end
  @auth
end

#channel(topic, params: nil) ⇒ Object

Realtime shortcuts on the umbrella — mirror supabase-py so callers can do ‘client.channel(“public:users”)` instead of `client.realtime.channel(…)`. The `realtime:` topic prefix is still optional (handled inside the Realtime client).



191
192
193
# File 'lib/supabase/client.rb', line 191

def channel(topic, params: nil)
  realtime.channel(topic, params: params)
end

#from(table) ⇒ Object Also known as: table



173
174
175
# File 'lib/supabase/client.rb', line 173

def from(table)
  postgrest.from(table)
end

#functionsObject



148
149
150
151
# File 'lib/supabase/client.rb', line 148

def functions
  @functions ||= functions_class.new(base_url: rest_url_for("functions/v1"), headers: @headers,
                                     **sub_options(:functions))
end

#get_channelsObject



195
196
197
# File 'lib/supabase/client.rb', line 195

def get_channels
  realtime.get_channels
end

#postgrestObject

PostgREST is the only sub-library where the public API is reached via a bare method on the umbrella (‘client.from(’users’)‘) rather than a named accessor. We expose both for explicitness.



168
169
170
171
# File 'lib/supabase/client.rb', line 168

def postgrest
  @postgrest ||= postgrest_class.new(base_url: rest_url_for("rest/v1"), headers: @headers,
                                     **sub_options(:postgrest))
end

#realtimeObject



153
154
155
156
157
158
159
160
161
162
163
# File 'lib/supabase/client.rb', line 153

def realtime
  # Use the current access token (@access_token), not the anon key: if the
  # caller signed in before this lazy accessor first ran, apply_auth could
  # not push the token to a not-yet-built realtime client, so we must seed
  # the join auth from the rotated token here. apikey stays the anon key.
  @realtime ||= Realtime::Client.new(
    url:    realtime_url,
    params: { "apikey" => @supabase_key, "access_token" => @access_token },
    **sub_options(:realtime)
  )
end

#remove_all_channelsObject

Unsubscribe every realtime channel registered on this client. Mirrors supabase-py’s ‘Client.remove_all_channels`; same sync/async contract as #remove_channel.

See Also:

  • supabase/_sync/client.py:234


214
215
216
# File 'lib/supabase/client.rb', line 214

def remove_all_channels
  dispatch_realtime { realtime.remove_all_channels }
end

#remove_channel(channel) ⇒ Object

Unsubscribe a channel and drop it from the realtime registry. Mirrors supabase-py: the sync client blocks until the phx_leave frame is written; the async client (‘async def remove_channel`) lets callers await it. Under `async: true` we get the same shape via #dispatch_realtime — the call returns an `Async::Task` the caller may `.wait` on (US-050), so a slow `Socket#send` never stalls the calling fiber.

See Also:

  • supabase/_async/client.py:231


206
207
208
# File 'lib/supabase/client.rb', line 206

def remove_channel(channel)
  dispatch_realtime { realtime.remove_channel(channel) }
end

#rpc(func, params = {}, **opts) ⇒ Object



183
184
185
# File 'lib/supabase/client.rb', line 183

def rpc(func, params = {}, **opts)
  postgrest.rpc(func, params, **opts)
end

#schema(name) ⇒ Object

Return a Postgrest client scoped to ‘name` without mutating self. Matches supabase-py: `client.schema(“foo”).from_(“x”)` queries the foo schema but leaves `client.from(…)` (and other call sites) on the default schema.



221
222
223
# File 'lib/supabase/client.rb', line 221

def schema(name)
  postgrest.schema(name)
end

#set_auth(token) ⇒ Object

Update the Authorization header used by every sub-client. Useful after auth.sign_in returns a fresh JWT — the apikey stays the same but the bearer token becomes the user’s access token.

Breaking change vs <=3.1.1: ‘set_auth(nil)` no longer drops the memoized auth sub-client (and with it any persisted session). Call `auth.sign_out` to clear session state.



234
235
236
237
# File 'lib/supabase/client.rb', line 234

def set_auth(token)
  apply_auth(token)
  self
end

#storageObject



143
144
145
146
# File 'lib/supabase/client.rb', line 143

def storage
  @storage ||= storage_class.new(base_url: rest_url_for("storage/v1"), headers: @headers,
                                 **sub_options(:storage))
end