Exporting files to Supabase Buckets in Ruby
Supabase offers a cloud storage solution through its bucket system. You can interact with Supabase Storage using Ruby's built-in HTTP libraries. In this guide, we illustrate how to export files to Supabase storage buckets via the REST API.
Prerequisites
Before beginning, ensure you have:
- Ruby 2.7 or higher installed
- A Supabase account and project with storage enabled
- Basic familiarity with Ruby programming
- Your Supabase project URL, project publishable or legacy anon key, and a signed-in user's access JWT
- Storage policies granting that user the required insert, select, and delete permissions
Setting up your environment
Create a new Ruby project and set up the necessary dependencies. Although some libraries like
net/http and json come with Ruby, we include them for clarity, and add mime-types for
determining file content types:
mkdir supabase-storage-demo
cd supabase-storage-demo
bundle init
Add the following gems to your Gemfile:
source 'https://rubygems.org'
# Net/http and JSON are part of Ruby's standard library
gem 'mime-types'
Install the dependencies:
bundle install
Configuring a REST API client in Ruby
We'll build a small client using Ruby's Net::HTTP to interact with the Supabase Storage REST API. The project key identifies the project; the separate user JWT determines the user's access under Storage policies. Keep both in environment variables and refresh the user session when it expires.
require 'net/http'
require 'json'
require 'mime/types'
class SupabaseStorage
def initialize(project_url, api_key, access_token)
@project_url = project_url.chomp('/')
@api_key = api_key
@access_token = access_token
@storage_url = "#{@project_url}/storage/v1"
end
private
def headers
{
'Authorization' => "Bearer #{@access_token}",
'apikey' => @api_key
}
end
def make_request(uri, request)
Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
response = http.request(request)
unless response.is_a?(Net::HTTPSuccess)
raise "Storage request failed (HTTP #{response.code})"
end
response
end
end
def encode_segment(value)
URI.encode_www_form_component(value).gsub('+', '%20')
end
def encode_path(path)
path.split('/').map { |segment| encode_segment(segment) }.join('/')
end
public
# Upload a file to a specific bucket and destination path
def upload_file(bucket_name, file_path, destination_path)
uri = URI("#{@storage_url}/object/#{encode_segment(bucket_name)}/#{encode_path(destination_path)}")
request = Net::HTTP::Post.new(uri)
headers.each { |key, value| request[key] = value }
# Open the file in binary mode and set up the request stream
File.open(file_path, 'rb') do |file|
request.body_stream = file
request['Content-Type'] = MIME::Types.type_for(file_path).first&.to_s || 'application/octet-stream'
request['Content-Length'] = File.size(file_path).to_s
response = make_request(uri, request)
JSON.parse(response.body)
end
rescue StandardError => e
{ 'error' => e.message }
end
# List one page of immediate children of a prefix
def list_files(bucket_name, path = '', limit: 100, offset: 0)
uri = URI("#{@storage_url}/object/list/#{encode_segment(bucket_name)}")
request = Net::HTTP::Post.new(uri)
headers.each { |key, value| request[key] = value }
request['Content-Type'] = 'application/json'
request.body = JSON.generate(prefix: path, limit: limit, offset: offset,
sortBy: { column: 'name', order: 'asc' })
response = make_request(uri, request)
JSON.parse(response.body)
rescue StandardError => e
{ 'error' => e.message }
end
# Return true only when the API confirms deletion of the requested file
def delete_file(bucket_name, file_path)
uri = URI("#{@storage_url}/object/#{encode_segment(bucket_name)}")
request = Net::HTTP::Delete.new(uri)
headers.each { |key, value| request[key] = value }
request['Content-Type'] = 'application/json'
request.body = JSON.generate(prefixes: [file_path])
response = make_request(uri, request)
deleted = JSON.parse(response.body)
deleted.is_a?(Array) && deleted.any? { |object| object.is_a?(Hash) && object['name'] == file_path }
rescue StandardError
false
end
end
Usage examples
An empty deletion result is not confirmation: the file may be missing or inaccessible under the caller's storage policies. The helper reports success only when the response lists the requested file.
Below is an example of how to use the SupabaseStorage class to upload, list, and delete files from
a Supabase bucket.
# Initialize the client with your environment variables
storage = SupabaseStorage.new(
ENV.fetch('SUPABASE_URL'),
ENV.fetch('SUPABASE_PROJECT_KEY'),
ENV.fetch('SUPABASE_ACCESS_TOKEN')
)
# Upload a file
result = storage.upload_file(
'my-bucket',
'path/to/local/image.jpg',
'uploads/image.jpg'
)
if result['error']
puts "Upload failed: #{result['error']}"
else
puts 'File uploaded successfully'
end
# List files in a bucket
files = storage.list_files('my-bucket', 'uploads/')
if files.is_a?(Array)
files.each do |file|
puts "File: #{file['name']}, Size: #{file.dig('metadata', 'size')}"
end
else
puts "Error listing files: #{files['error']}"
end
# Delete a file
if storage.delete_file('my-bucket', 'uploads/image.jpg')
puts 'File deleted successfully'
else
puts 'Failed to delete file'
end
list_files returns one page of immediate children, including folder rows. Increase offset by
the page size until a shorter page is returned when you need more than 100 entries. This example
does not recursively list subfolders. Uploads create new objects; existing paths require an
intentional upsert and the corresponding update permissions.
Error handling and file validation
It is important to validate files before uploading to avoid errors and ensure security. The following validator checks for regular files, an application-specific size limit, and allowed extensions. Extension and MIME registry lookups do not inspect content or establish that it is safe.
class FileValidator
MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB
ALLOWED_TYPES = %w[.jpg .jpeg .png .pdf .doc .docx]
def self.validate!(file_path)
raise 'File not found' unless File.file?(file_path)
size = File.size(file_path)
raise 'File exceeds maximum size' if size > MAX_FILE_SIZE
extension = File.extname(file_path).downcase
raise 'Invalid file type' unless ALLOWED_TYPES.include?(extension)
true
end
end
# Example usage:
begin
FileValidator.validate!('path/to/file.jpg')
result = storage.upload_file('my-bucket', 'path/to/file.jpg', 'uploads/file.jpg')
raise result['error'] if result['error']
rescue StandardError => e
puts "Validation failed: #{e.message}"
end
Best practices
Generate unique filenames
Creating unique filenames helps prevent collisions in storage. For example:
require 'securerandom'
def generate_unique_filename(original_filename)
extension = File.extname(original_filename)
basename = File.basename(original_filename, extension)
timestamp = Time.now.strftime('%Y%m%d-%H%M%S')
"#{basename}-#{timestamp}-#{SecureRandom.hex(4)}#{extension}"
end
Organize files by date
Storing files in date-based directories can simplify management:
def generate_storage_path(filename)
date = Time.now
"uploads/#{date.year}/#{date.month}/#{filename}"
end
Handling large files
The client above streams a standard upload but cannot resume an interrupted request. Supabase
supports resumable uploads using tus
at /storage/v1/upload/resumable. Use a compatible tus client for that protocol rather than
uploading independent chunks to the standard object endpoint. Check the project's configured
file size limits before transferring large files.
Common use cases
- User avatar uploads
- Document storage systems
- Media file management
- Backup solutions
- Content delivery systems
Security considerations
- Validate files before uploading to prevent issues.
- Store your Supabase project URL and API key in environment variables.
- Implement proper CORS policies if exposing your API.
- Set appropriate bucket permissions.
- Use secure URLs for sensitive content.
Conclusion
Using Ruby's Net::HTTP and the Supabase REST API provides a method for managing file storage. With proper validation, error handling, and security measures, you can efficiently export files to Supabase storage buckets in your Ruby applications.
For additional media processing needs, consider exploring Transloadit's features to further enhance your workflow.
