From 3001146c89deea7cbb4f015803f9461b693faf63 Mon Sep 17 00:00:00 2001 From: Lenny Date: Thu, 4 Dec 2025 02:27:24 -0600 Subject: [PATCH] docs(storage): signed resumable uploads docs and example (#40793) --- .../storage/uploads/resumable-uploads.mdx | 6 + .../resumable-upload-signed-uppy/README.md | 23 ++++ .../resumable-upload-signed-uppy/index.html | 113 ++++++++++++++++++ .../supabase-logo-wordmark--dark.png | Bin 0 -> 7297 bytes .../supabase/.gitignore | 4 + .../supabase/config.toml | 32 +++++ .../functions/create-upload-token/.npmrc | 3 + .../functions/create-upload-token/deno.json | 3 + .../functions/create-upload-token/index.ts | 29 +++++ .../migrations/20241128121139_storage_rls.sql | 1 + 10 files changed, 214 insertions(+) create mode 100644 examples/storage/resumable-upload-signed-uppy/README.md create mode 100644 examples/storage/resumable-upload-signed-uppy/index.html create mode 100644 examples/storage/resumable-upload-signed-uppy/supabase-logo-wordmark--dark.png create mode 100644 examples/storage/resumable-upload-signed-uppy/supabase/.gitignore create mode 100644 examples/storage/resumable-upload-signed-uppy/supabase/config.toml create mode 100644 examples/storage/resumable-upload-signed-uppy/supabase/functions/create-upload-token/.npmrc create mode 100644 examples/storage/resumable-upload-signed-uppy/supabase/functions/create-upload-token/deno.json create mode 100644 examples/storage/resumable-upload-signed-uppy/supabase/functions/create-upload-token/index.ts create mode 100644 examples/storage/resumable-upload-signed-uppy/supabase/migrations/20241128121139_storage_rls.sql diff --git a/apps/docs/content/guides/storage/uploads/resumable-uploads.mdx b/apps/docs/content/guides/storage/uploads/resumable-uploads.mdx index 2a1db32286c..4a9619a3847 100644 --- a/apps/docs/content/guides/storage/uploads/resumable-uploads.mdx +++ b/apps/docs/content/guides/storage/uploads/resumable-uploads.mdx @@ -302,6 +302,12 @@ Uppy has integrations with different frameworks: - [Vue](https://uppy.io/docs/vue/) - [Angular](https://uppy.io/docs/angular/) +### Presigned uploads + +Resumable uploads also supports using signed upload tokens to created time-limited URLs that you can share to your users by invoking the `createSignedUploadUrl` method on the SDK and including the returned token in the `x-signature` header of the resumable upload. + +See this [full example using Uppy with signed URLs](https://github.com/supabase/supabase/tree/master/examples/storage/resumable-upload-signed-uppy) for more context. + ## Overwriting files When uploading a file to a path that already exists, the default behavior is to return a `400 Asset Already Exists` error. diff --git a/examples/storage/resumable-upload-signed-uppy/README.md b/examples/storage/resumable-upload-signed-uppy/README.md new file mode 100644 index 00000000000..a762be9e617 --- /dev/null +++ b/examples/storage/resumable-upload-signed-uppy/README.md @@ -0,0 +1,23 @@ +## Resumable Uploads with Supabase Storage and Uppy + +This example shows how to use signed urls from [Supabase Storage](https://supabase.io/docs/reference/javascript/storage) with [Uppy](https://uppy.io/) to upload files to Supabase Storage using the TUS protocol (signed resumable uploads). + +This works by calling `createSignedUploadUrl()` to get a token for each file, and passing that token via the `x-signature` header when uploading the files + +### Running the example + +- Start local supabase project `supabase start` +- Open the index.html file and set `SUPABASE_PUBLISHABLE_KEY` to the value output when starting the supabase cli +- Serve the index.html file locally (e.g. with Python Simple HTTP Server or http-server npm package) and start uploading: + +```bash +# python http server +python3 -m http.server + +# npm http-server +npx http-server +``` + +### How it works + +In index.html the `uppy.on('file-added')` hook calls the [create-upload-token](supabase/functions/create-upload-token/index.ts) function which creates a token for each added file and attaches it to that file's header config as `x-signature`. diff --git a/examples/storage/resumable-upload-signed-uppy/index.html b/examples/storage/resumable-upload-signed-uppy/index.html new file mode 100644 index 00000000000..a1bba5f1b66 --- /dev/null +++ b/examples/storage/resumable-upload-signed-uppy/index.html @@ -0,0 +1,113 @@ + + + + + Signed Resumable Upload Supabase + UppyJS + + + + + + +
+ Read the docs. + + + + diff --git a/examples/storage/resumable-upload-signed-uppy/supabase-logo-wordmark--dark.png b/examples/storage/resumable-upload-signed-uppy/supabase-logo-wordmark--dark.png new file mode 100644 index 0000000000000000000000000000000000000000..dccffd4247d603a8394de9da4089269b91c471d2 GIT binary patch literal 7297 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 S+E0ohih``FOvO9X!2bbv^+*Q* literal 0 HcmV?d00001 diff --git a/examples/storage/resumable-upload-signed-uppy/supabase/.gitignore b/examples/storage/resumable-upload-signed-uppy/supabase/.gitignore new file mode 100644 index 00000000000..a3ad88055b7 --- /dev/null +++ b/examples/storage/resumable-upload-signed-uppy/supabase/.gitignore @@ -0,0 +1,4 @@ +# Supabase +.branches +.temp +.env diff --git a/examples/storage/resumable-upload-signed-uppy/supabase/config.toml b/examples/storage/resumable-upload-signed-uppy/supabase/config.toml new file mode 100644 index 00000000000..c54410aa307 --- /dev/null +++ b/examples/storage/resumable-upload-signed-uppy/supabase/config.toml @@ -0,0 +1,32 @@ +# A string used to distinguish different Supabase projects on the same host. Defaults to the +# working directory name when running `supabase init`. +project_id = "resumable-upload-uppy" + +[api] +# Disable data API since we are not using the PostgREST client in this example. +enabled = false + +[storage] +# The maximum file size allowed for all buckets in the project. +file_size_limit = "50MiB" + +[storage.image_transformation] +enabled = false + +[storage.buckets.uploads] +public = true +# file_size_limit = "50MiB" +# allowed_mime_types = ["image/png", "image/jpeg"] +# Uncomment to specify a local directory to upload objects to the bucket. +# objects_path = "./buckets/uploads" + +[functions.create-upload-token] +enabled = true +verify_jwt = true +import_map = "./functions/create-upload-token/deno.json" +# Uncomment to specify a custom file path to the entrypoint. +# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx +entrypoint = "./functions/create-upload-token/index.ts" +# Specifies static files to be bundled with the function. Supports glob patterns. +# For example, if you want to serve static HTML pages in your function: +# static_files = [ "./functions/create-upload-token/*.html" ] diff --git a/examples/storage/resumable-upload-signed-uppy/supabase/functions/create-upload-token/.npmrc b/examples/storage/resumable-upload-signed-uppy/supabase/functions/create-upload-token/.npmrc new file mode 100644 index 00000000000..48c63886380 --- /dev/null +++ b/examples/storage/resumable-upload-signed-uppy/supabase/functions/create-upload-token/.npmrc @@ -0,0 +1,3 @@ +# Configuration for private npm package dependencies +# For more information on using private registries with Edge Functions, see: +# https://supabase.com/docs/guides/functions/import-maps#importing-from-private-registries diff --git a/examples/storage/resumable-upload-signed-uppy/supabase/functions/create-upload-token/deno.json b/examples/storage/resumable-upload-signed-uppy/supabase/functions/create-upload-token/deno.json new file mode 100644 index 00000000000..f6ca8454c56 --- /dev/null +++ b/examples/storage/resumable-upload-signed-uppy/supabase/functions/create-upload-token/deno.json @@ -0,0 +1,3 @@ +{ + "imports": {} +} diff --git a/examples/storage/resumable-upload-signed-uppy/supabase/functions/create-upload-token/index.ts b/examples/storage/resumable-upload-signed-uppy/supabase/functions/create-upload-token/index.ts new file mode 100644 index 00000000000..ff8cdf0a674 --- /dev/null +++ b/examples/storage/resumable-upload-signed-uppy/supabase/functions/create-upload-token/index.ts @@ -0,0 +1,29 @@ +import 'jsr:@supabase/functions-js/edge-runtime.d.ts' +import { createClient } from 'jsr:@supabase/supabase-js' + +Deno.serve(async (req) => { + const SUPABASE_URL = Deno.env.get('SUPABASE_URL') ?? '' + const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '' + + const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY) + + try { + const { filename } = await req.json() + if (!filename) { + return new Response('Missing filename', { status: 400 }) + } + + const { data, error } = await supabase.storage.from('uploads').createSignedUploadUrl(filename) + + if (error) { + return new Response(error.message, { status: 500 }) + } + + return new Response(JSON.stringify({ token: data.token }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) + } catch (error) { + return new Response((error as Error).message, { status: 500 }) + } +}) diff --git a/examples/storage/resumable-upload-signed-uppy/supabase/migrations/20241128121139_storage_rls.sql b/examples/storage/resumable-upload-signed-uppy/supabase/migrations/20241128121139_storage_rls.sql new file mode 100644 index 00000000000..7769adecadb --- /dev/null +++ b/examples/storage/resumable-upload-signed-uppy/supabase/migrations/20241128121139_storage_rls.sql @@ -0,0 +1 @@ +CREATE POLICY "allow uploads" ON storage.objects FOR INSERT TO public WITH CHECK (bucket_id = 'uploads');