Skip to main content

Supabase Storage Provider

Vali-Blob.Supabase provides SupabaseStorageProvider, implementing IStorageProvider, IResumableUploadProvider, and IPresignedUrlProvider backed by Supabase Storage. Supabase Storage is built on S3 and exposes a REST API with PostgreSQL Row Level Security (RLS) for fine-grained access control.


Installation

dotnet add package Vali-Blob.Core
dotnet add package Vali-Blob.Supabase

SupabaseStorageOptions Reference

OptionTypeRequiredDescription
UrlstringYesSupabase project URL, e.g. "https://xyzabc.supabase.co".
ServiceKeystringYesservice_role JWT from your project API settings.
BucketNamestringYesStorage bucket name for all operations.

DI Registration

using Vali-Blob.Core;
using Vali-Blob.Supabase;

builder.Services
.AddVali-Blob(o => o.DefaultProvider = "supabase")
.AddProvider<SupabaseStorageProvider>("supabase", opts =>
{
opts.Url = builder.Configuration["Supabase:Url"]!;
opts.ServiceKey = builder.Configuration["Supabase:ServiceKey"]!;
opts.BucketName = builder.Configuration["Supabase:BucketName"]!;
})
.WithPipeline(p => p
.UseValidation(v =>
{
v.MaxFileSizeBytes = 50_000_000;
v.AllowedExtensions = [".jpg", ".png", ".pdf", ".mp4"];
})
.UseContentTypeDetection()
.UseConflictResolution(ConflictResolution.ReplaceExisting)
);

appsettings.json

{
"Supabase": {
"Url": "https://xyzabc.supabase.co",
"BucketName": "uploads"
}
}
Protect the service_role key

The service_role key bypasses all Row Level Security policies. Keep it exclusively on your server. For client-side uploads from browsers or mobile apps, use presigned URLs instead.


Getting Your Credentials

  1. Open the Supabase Dashboard
  2. Select your project
  3. Navigate to SettingsAPI
  4. Copy the Project URL → use as Url
  5. Under Project API keys, copy the service_role secret → use as ServiceKey

Store the ServiceKey using dotnet user-secrets for development:

dotnet user-secrets set "Supabase:ServiceKey" "eyJhbGci..."

Creating Buckets

Via Supabase Dashboard

  1. Navigate to Storage in your project sidebar
  2. Click New bucket
  3. Enter a name and choose Public or Private visibility

Via SQL (Migration)

-- Create a private bucket
INSERT INTO storage.buckets (id, name, public)
VALUES ('uploads', 'uploads', false);

Public vs Private Buckets

Bucket TypeRead AccessWrite AccessBest For
PublicAnyone with the URL — no authentication requiredAuthenticated users (controlled by RLS)Profile images, public assets
PrivateRequires a signed URL or authenticated request with RLSAuthenticated users (controlled by RLS)Documents, user files, sensitive data

Row Level Security (RLS) Policies

Supabase Storage integrates with PostgreSQL RLS. Define policies in StoragePolicies in the dashboard, or via SQL migrations:

Allow authenticated users to upload to their own path

CREATE POLICY "Users can upload their own files"
ON storage.objects FOR INSERT
TO authenticated
WITH CHECK (
bucket_id = 'uploads' AND
(storage.foldername(name))[1] = auth.uid()::text
);

Allow authenticated users to read their own files

CREATE POLICY "Users can read their own files"
ON storage.objects FOR SELECT
TO authenticated
USING (
bucket_id = 'uploads' AND
(storage.foldername(name))[1] = auth.uid()::text
);

Allow public read on the public subfolder

CREATE POLICY "Public read on public folder"
ON storage.objects FOR SELECT
TO anon
USING (
bucket_id = 'uploads' AND
(storage.foldername(name))[1] = 'public'
);

Allow users to delete only their own files

CREATE POLICY "Users can delete their own files"
ON storage.objects FOR DELETE
TO authenticated
USING (
bucket_id = 'uploads' AND
(storage.foldername(name))[1] = auth.uid()::text
);
RLS policies only apply to anon/authenticated keys

When Vali-Blob uses the service_role key, RLS is bypassed. RLS policies control access when clients interact directly with Supabase using the anon or user JWT keys.


Presigned URLs

SupabaseStorageProvider implements IPresignedUrlProvider. Signed URLs grant time-limited, unauthenticated access to specific objects — ideal for sharing private files or enabling direct client uploads without routing data through your server:

var provider = factory.Create("supabase");

if (provider is IPresignedUrlProvider presigned)
{
// Signed upload URL — client uploads directly to Supabase for 10 minutes
var uploadUrl = await presigned.GetPresignedUploadUrlAsync(
StoragePath.From("uploads", userId, "avatar.jpg"),
expiresIn: TimeSpan.FromMinutes(10));

// Signed download URL — time-limited access to a private file
var downloadUrl = await presigned.GetPresignedDownloadUrlAsync(
StoragePath.From("uploads", userId, "contract.pdf"),
expiresIn: TimeSpan.FromHours(24));

return Results.Ok(new
{
uploadUrl = uploadUrl.Value,
downloadUrl = downloadUrl.Value
});
}

Direct Client Upload Flow

loading...

Your server never handles file bytes in this pattern — it only generates URLs.


File Organization

Organize files using path segments within the bucket:

uploads/
{userId}/
avatars/
profile.jpg
documents/
contract.pdf
public/
banner.jpg
// Upload to a user-scoped path
await provider.UploadAsync(new UploadRequest
{
Path = StoragePath.From("uploads", userId, "avatars", "profile.jpg"),
Content = fileStream,
ContentType = "image/jpeg"
});

// List all files for a user
var files = await provider.ListFilesAsync($"uploads/{userId}/");

Image Transformations (Delivery-Time)

Supabase Storage supports on-the-fly image transformations for public buckets via URL parameters:

# Resize to 300×300, crop to fill
https://xyzabc.supabase.co/storage/v1/render/image/public/uploads/photo.jpg?width=300&height=300&resize=cover

# Convert to WebP
https://xyzabc.supabase.co/storage/v1/render/image/public/uploads/photo.jpg?format=origin

Vali-Blob's ImageProcessingMiddleware (via Vali-Blob.ImageSharp) handles upload-time transformations — these two approaches are complementary.


Resumable Uploads (TUS Protocol)

SupabaseStorageProvider implements IResumableUploadProvider using the TUS protocol, which Supabase Storage natively supports:

var provider = factory.Create("supabase");
var resumable = (IResumableUploadProvider)provider;

// Step 1: Start
var start = await resumable.StartResumableUploadAsync(new StartResumableUploadRequest
{
Path = StoragePath.From("uploads", userId, "large-video.mp4"),
TotalSize = 524_288_000, // 500 MB
ContentType = "video/mp4"
});

// Step 2: Upload chunks
var uploadId = start.Value.UploadId;
var chunkSize = 5 * 1024 * 1024; // 5 MB
// ... chunk loop (see Resumable Uploads documentation)

// Step 3: Complete
await resumable.CompleteResumableUploadAsync(uploadId);

Local Development with Supabase CLI

Use the Supabase CLI to run a full local Supabase stack including Storage:

# Install Supabase CLI
brew install supabase/tap/supabase

# Initialize project and start local stack
supabase init
supabase start

The local storage API runs at http://localhost:54321/storage/v1. Configure Vali-Blob:

.AddProvider<SupabaseStorageProvider>("supabase", opts =>
{
opts.Url = "http://localhost:54321";
opts.ServiceKey = "<service_key from supabase start output>";
opts.BucketName = "uploads";
})

Create buckets in the local stack:

# Create a bucket via the Supabase Management API
curl -X POST http://localhost:54321/storage/v1/bucket \
-H "Authorization: Bearer <service_key>" \
-H "Content-Type: application/json" \
-d '{"id": "uploads", "name": "uploads", "public": false}'

Supported Operations

OperationSupportedNotes
UploadAsyncYes
DownloadAsyncYes
DeleteAsyncYes
DeleteFolderAsyncYesBatch list + delete by prefix
ExistsAsyncYes
CopyAsyncYes
GetMetadataAsyncYes
SetMetadataAsyncYes
ListFilesAsyncYes
ListFoldersAsyncYes
GetUrlAsyncYesPublic URL or signed URL
StartResumableUploadAsyncYesTUS protocol
UploadChunkAsyncYesTUS PATCH
CompleteResumableUploadAsyncYesFinal TUS PATCH
AbortResumableUploadAsyncYesTUS DELETE
GetPresignedUploadUrlAsyncYesSupabase signed upload URL
GetPresignedDownloadUrlAsyncYesSupabase signed download URL