A C# client for Supabase Storage — upload, download, and serve files from buckets, with signed and public URLs.
Part of the Supabase C# SDK. Most projects
use it through the Supabase meta-package (supabase.Storage); reference
this package directly when you only need storage.
dotnet add package Supabase.StorageTargets .NET Standard 2.0.
Create a client pointed at your project's Storage URL, passing your key as a header:
using Supabase.Storage;
var client = new Client("https://PROJECT_ID.supabase.co/storage/v1", new Dictionary<string, string>
{
{ "apikey", SUPABASE_KEY },
{ "Authorization", $"Bearer {SUPABASE_KEY}" }
});await client.CreateBucket("avatars");
var bucket = await client.GetBucket("avatars");
var all = await client.ListBuckets();
await client.EmptyBucket("avatars");
await client.DeleteBucket("avatars");Work with a bucket's files through From:
var bucket = client.From("avatars");
// Upload from a local path or from bytes.
await bucket.Upload("./local.png", "me.png");
await bucket.Upload(bytes, "me.png");
// List, move, copy, remove.
var files = await bucket.List();
await bucket.Move("me.png", "old/me.png");
await bucket.Copy("old/me.png", "backup/me.png");
await bucket.Remove("old/me.png");
// Download to a local path (returns the path) or into memory (returns bytes).
await bucket.Download("me.png", "./downloaded.png");
byte[] data = await bucket.Download("me.png");// Public bucket: build a URL directly (pass null for no image transform).
var publicUrl = bucket.GetPublicUrl("me.png", null);
// Private bucket: sign a URL that expires (seconds).
var signedUrl = await bucket.CreateSignedUrl("me.png", 3600);Storage failures throw a SupabaseStorageException. Use Code for the exact service error (for
example, NoSuchKey) and Reason for the broader classification. Code is null when the response
does not include one. See the Storage error code reference.
The client emits traces and metrics through System.Diagnostics, so you can wire them into
OpenTelemetry (or any ActivityListener / MeterListener) without taking a dependency on the
OpenTelemetry packages. Emission is zero-cost while nothing is listening, so it is always on and stays
silent until you subscribe.
Register the client's ActivitySource and Meter by name. Use the StorageDiagnostics.SourceName
constant rather than hardcoding the string, so a typo becomes a compile error instead of a silent
no-op:
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
using Supabase.Storage;
// Requires OpenTelemetry.Extensions.Hosting and an exporter package (e.g. OTLP) in your app.
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddSource(StorageDiagnostics.SourceName)
.AddOtlpExporter())
.WithMetrics(metrics => metrics
.AddMeter(StorageDiagnostics.SourceName)
.AddOtlpExporter());Once subscribed you get:
- A client span per request, named
{METHOD} {path}and following OpenTelemetry HTTP conventions (method, status code, and a sanitized URL). The query string is never recorded — Storage signed URLs carry atokenthere. Upload and download spans additionally carry astorage.transfer.directiontag (upload/download). The resumable (TUS) upload is reported as a single operation span covering the whole transfer, rather than one span per underlying chunk request. supabase.storage.http.request.duration(seconds) for control-plane requests.supabase.storage.transfer.duration(seconds) andsupabase.storage.transfer.size(bytes) for uploads and downloads, tagged by direction — because a duration alone does not describe a file transfer.
If you are not using the OpenTelemetry SDK, a raw listener works too:
using System.Diagnostics;
using Supabase.Storage;
using var listener = new ActivityListener
{
ShouldListenTo = source => source.Name == StorageDiagnostics.SourceName,
Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllData,
ActivityStopped = activity => Console.WriteLine($"{activity.OperationName} {activity.Duration.TotalMilliseconds}ms {activity.Status}")
};
ActivitySource.AddActivityListener(listener);Contributions are welcome. See the repository root for how to build and test the SDK.