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)


64
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
# File 'lib/supabase/client.rb', line 64

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) &&
      options.keys.any? { |k| %i[auth postgrest storage functions realtime global].include?(k.to_sym) }

  @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 || {})
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
# 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)
    global_headers = options[:global]&.dig(:headers) || options.dig("global", "headers") || {}
    configured_auth = global_headers["Authorization"] || global_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)


114
115
116
# File 'lib/supabase/client.rb', line 114

def async?
  @async
end

#authObject

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



120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/supabase/client.rb', line 120

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).



179
180
181
# File 'lib/supabase/client.rb', line 179

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

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



161
162
163
# File 'lib/supabase/client.rb', line 161

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

#functionsObject



140
141
142
143
# File 'lib/supabase/client.rb', line 140

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

#get_channelsObject



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

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.



156
157
158
159
# File 'lib/supabase/client.rb', line 156

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

#realtimeObject



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

def realtime
  @realtime ||= Realtime::Client.new(
    url:    realtime_url,
    params: { "apikey" => @supabase_key, "access_token" => @supabase_key },
    **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


202
203
204
# File 'lib/supabase/client.rb', line 202

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


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

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

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



171
172
173
# File 'lib/supabase/client.rb', line 171

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.



209
210
211
# File 'lib/supabase/client.rb', line 209

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.



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

def set_auth(token)
  apply_auth(token)
  self
end

#storageObject



135
136
137
138
# File 'lib/supabase/client.rb', line 135

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