Connect FlutterFlow to Wasabi by deploying a Firebase Cloud Function or Supabase Edge Function that generates presigned PUT and GET URLs using the AWS S3 SDK pointed at your Wasabi regional endpoint. FlutterFlow then calls your proxy to get a presigned URL and PUTs the file bytes directly to Wasabi — your secret key never touches the app. Wasabi charges a flat ~$6.99/TB/month with no egress or request fees.
| Fact | Value |
|---|---|
| Tool | Wasabi |
| Category | Storage & Files |
| Method | FlutterFlow API Call |
| Difficulty | Intermediate |
| Time required | 60 minutes |
| Last updated | July 2026 |
Wasabi in FlutterFlow: presigned URLs, zero egress fees, and the SigV4 proxy pattern
Wasabi is fully S3-compatible — you point the same AWS SDK calls at a regional endpoint like s3.us-east-1.wasabisys.com and they work exactly as they would against AWS S3. For most developers this is a simple price upgrade: Wasabi charges approximately $6.99/TB/month for hot storage with no egress fees and no per-request charges, while AWS S3 bills separately for storage, requests, and egress. For media-heavy apps where users download files frequently, the no-egress-fee model can be dramatically cheaper (verify current pricing at wasabi.com/pricing).
The challenge for FlutterFlow is authentication. Every S3 request must be signed with an AWS Signature V4 — an HMAC computed over the request metadata and content using your secret access key. Computing that signature in Dart requires your secret key to be present in the compiled app, where it can be extracted from the APK or IPA binary. That is not acceptable for any production app.
The standard pattern for mobile and web clients is presigned URLs: your backend function holds the secret key, generates a time-limited URL that pre-authorizes a specific operation (PUT a specific file, GET a specific object), and returns only that URL to the client. FlutterFlow executes a plain HTTP PUT or GET to the presigned URL — no auth headers, no signing, no secret key. The presigned URL is safe to use once and expires (typically in 15 minutes) even if it is intercepted.
Integration method
Wasabi uses AWS Signature V4 authentication, which requires an HMAC over the request using your secret access key. That signing computation cannot be done in Dart without exposing the secret key inside the compiled app. The solution is a backend proxy — a Firebase Cloud Function or Supabase Edge Function — that uses the AWS S3 SDK to generate a time-limited presigned URL. FlutterFlow receives the presigned URL and executes a simple PUT (upload) or GET (download) with no authentication headers needed.
Prerequisites
- A Wasabi account (free trial at wasabi.com — note Wasabi bills a minimum of 1 TB per month even for smaller usage; verify current terms)
- A Wasabi bucket created in the Wasabi console with the desired region noted (e.g., us-east-1, eu-central-1)
- A Wasabi access key pair (Access Key ID and Secret Access Key) created in the Wasabi console
- A Firebase project (for the Cloud Function proxy) or a Supabase project (for the Edge Function proxy)
- A FlutterFlow project (API Calls available on all plans)
Step-by-step guide
Create a Wasabi bucket and generate access keys
Log in to the Wasabi console at console.wasabisys.com. Click Buckets in the left sidebar, then click Create Bucket. Give the bucket a unique name (all lowercase, no spaces), select the region closest to your users (for example us-east-1, eu-central-1, ap-southeast-1), and leave versioning and logging settings as default for a basic setup. Click Create Bucket. Note the exact region you chose — you will need it to construct the correct Wasabi endpoint URL. Each Wasabi region has its own endpoint: us-east-1 uses s3.us-east-1.wasabisys.com, us-west-1 uses s3.us-west-1.wasabisys.com, eu-central-1 uses s3.eu-central-1.wasabisys.com, and so on. Using the wrong endpoint for your bucket's region causes signature mismatches and 403 errors. Next, click Access Keys in the left sidebar and then Create Access Key. Give it a name and optionally restrict it to specific buckets. Click Create and copy both the Access Key ID and the Secret Access Key immediately — the Secret Access Key is shown only once. These credentials will live only in your backend function environment variables, never in FlutterFlow.
Pro tip: Create a dedicated IAM-style access key for your app rather than using your root account key. Wasabi's console lets you create sub-account users with limited bucket permissions.
Expected result: You have a Wasabi bucket in a known region and an Access Key ID + Secret Access Key pair stored securely (not yet in FlutterFlow).
Deploy a presign proxy function that generates time-limited upload and download URLs
Deploy a backend function that holds your Wasabi credentials and generates presigned URLs on demand. The function uses the AWS SDK for JavaScript (or Python's boto3 equivalent) pointed at your Wasabi regional endpoint instead of the default AWS S3 endpoint. In your Firebase console, create a new HTTP Cloud Function named wasabiPresign. The function accepts a POST request with a JSON body containing the desired operation (PUT for upload or GET for download), the object key (the path and filename within the bucket), and optionally the content type for PUT operations. It uses the AWS SDK's getSignedUrlPromise to generate a presigned URL valid for 900 seconds (15 minutes) and returns it in the response. For the S3 client configuration, set the endpoint to your Wasabi region's URL, set the region to match, and provide your Wasabi access key ID and secret. The AWS SDK treats Wasabi's endpoint as an S3-compatible service — this is the whole point of the S3-compatible interface. For listing objects (to display a file browser), add a separate /list endpoint that calls s3.listObjectsV2 and returns a clean JSON array of object keys. This avoids sending the raw XML response from Wasabi to FlutterFlow.
1// Firebase Cloud Function — Wasabi presign proxy2// functions/index.js (Node.js)3const functions = require('firebase-functions');4const AWS = require('aws-sdk');56const s3 = new AWS.S3({7 endpoint: 'https://s3.us-east-1.wasabisys.com', // Change to your region8 region: 'us-east-1',9 accessKeyId: functions.config().wasabi.access_key_id,10 secretAccessKey: functions.config().wasabi.secret_access_key,11 signatureVersion: 'v4',12});1314const BUCKET = functions.config().wasabi.bucket;1516exports.wasabiPresign = functions.https.onRequest(async (req, res) => {17 res.set('Access-Control-Allow-Origin', '*');18 if (req.method === 'OPTIONS') return res.status(204).send('');1920 const { operation, key, contentType } = req.body;21 if (!operation || !key) {22 return res.status(400).json({ error: 'Missing operation or key' });23 }2425 const params = {26 Bucket: BUCKET,27 Key: key,28 Expires: 900, // 15 minutes29 ...(operation === 'PUT' && contentType ? { ContentType: contentType } : {}),30 };3132 const method = operation === 'PUT' ? 'putObject' : 'getObject';33 const url = await s3.getSignedUrlPromise(method, params);34 res.json({ url, expires_in: 900 });35});3637exports.wasabiList = functions.https.onRequest(async (req, res) => {38 res.set('Access-Control-Allow-Origin', '*');39 if (req.method === 'OPTIONS') return res.status(204).send('');4041 const { prefix } = req.query;42 const data = await s3.listObjectsV2({43 Bucket: BUCKET,44 Prefix: prefix || '',45 }).promise();4647 const objects = data.Contents.map(obj => ({48 key: obj.Key,49 size: obj.Size,50 last_modified: obj.LastModified,51 }));52 res.json({ objects });53});54Pro tip: Set firebase functions:config:set wasabi.secret_access_key=YOUR_SECRET to keep credentials out of source code. The bucket name and region can be config values too.
Expected result: Two deployed Cloud Function URLs — /wasabiPresign for upload/download URLs and /wasabiList for object listings — that return clean JSON to FlutterFlow without exposing Wasabi credentials.
Create FlutterFlow API Groups for the presign proxy and for direct Wasabi PUT
In FlutterFlow, you need two API Groups. The first talks to your backend proxy to get the presigned URL. The second makes the actual PUT to Wasabi using the presigned URL. Create API Group 1 — name it WasabiProxy. Set the base URL to your Firebase function base URL (for example https://us-central1-your-project.cloudfunctions.net). Add a Content-Type header with value application/json. Add an API Call named getPresignedUrl: method POST, endpoint /wasabiPresign. In the Request Body, set type JSON with body {"operation":"{{operation}}","key":"{{key}}","contentType":"{{contentType}}"}. Add variables: operation (String), key (String), contentType (String). In the Response & Test tab, generate JSON paths including $.url. Add a second API Call named listObjects: method GET, endpoint /wasabiList?prefix={{prefix}}. Add a prefix variable (String, default empty). Generate JSON paths for $.objects[*].key and $.objects[*].last_modified. Create API Group 2 — name it WasabiUpload. Set the base URL to https://s3.{your-region}.wasabisys.com. This group will have only one API Call named uploadToPresignedUrl: method PUT, endpoint /{{presignedPath}} (the path portion of the presigned URL). Add a Content-Type header with value {{contentType}} and a variable for contentType. The body type is Binary. Note: in practice you will PUT directly to the full presigned URL returned by your proxy — in FlutterFlow this means using an API Call with the complete presigned URL as the endpoint and no additional auth headers, since the signature is baked into the URL.
1// WasabiProxy API Group — reference2{3 "base_url": "https://us-central1-your-project.cloudfunctions.net",4 "headers": { "Content-Type": "application/json" },5 "calls": [6 {7 "name": "getPresignedUrl",8 "method": "POST",9 "endpoint": "/wasabiPresign",10 "body": {11 "operation": "{{operation}}",12 "key": "{{key}}",13 "contentType": "{{contentType}}"14 }15 },16 {17 "name": "listObjects",18 "method": "GET",19 "endpoint": "/wasabiList"20 }21 ]22}23Pro tip: Presigned URLs are single-operation — a PUT presigned URL cannot be used for GET. Request a fresh presigned URL for each operation rather than caching them.
Expected result: Two API Groups appear in the API Calls panel: WasabiProxy (talks to your backend) and WasabiUpload (PUTs bytes to Wasabi using the presigned URL).
Configure bucket CORS for web builds and wire the upload action flow
If your FlutterFlow app will run in a browser (web build), Wasabi must allow cross-origin PUT requests from your app's domain. Without CORS configuration, browser uploads fail with an XMLHttpRequest error even though the presigned URL is valid. In the Wasabi console, select your bucket → Properties → CORS Configuration (or Bucket Policies → CORS). Enter a CORS policy that allows PUT and GET from your app's origin. For development, allow all origins with * — tighten this to your specific domain before production. Now wire the upload action flow in FlutterFlow. On your upload button, create this sequence in the Action Flow Editor: 1) File Picker action — lets the user select a file and returns file bytes and the original filename. Store both in Page State variables (fileBytes, fileName). 2) API Call action → WasabiProxy → getPresignedUrl — pass operation=PUT, key={{fileName}}, contentType=image/jpeg (or the appropriate MIME type). Store the returned $.url in a Page State variable named presignedPutUrl. 3) API Call action → WasabiUpload → uploadToPresignedUrl — set the endpoint to {{presignedPutUrl}} (the full URL from step 2), body type Binary bound to fileBytes, Content-Type header matching what you passed to getPresignedUrl. 4) On success, call WasabiProxy → listObjects to refresh the file list. For the object list view, add a ListView with backend query WasabiProxy → listObjects and display $.objects[*].key as the file name in each list item.
Pro tip: The presigned URL contains query parameters including the signature — do not add any additional headers (especially Authorization) when using a presigned URL, as extra headers alter the signed request and cause a 403 SignatureDoesNotMatch error.
Expected result: Web and mobile users can upload files to Wasabi via the presigned URL flow. The ListView refreshes automatically after each upload showing the new object key.
Test uploads on device and browser, and handle presigned URL expiry
Test the upload flow on both platforms. On mobile (Android APK or iOS TestFlight build), the PUT to a presigned URL should work without any CORS configuration since mobile apps do not enforce CORS. On web, open the browser developer tools Network tab to confirm the PUT request to the Wasabi presigned URL returns a 200 status. If you see a CORS error, return to the Wasabi console and verify the CORS policy is saved and references your app's domain or *. Presigned URLs expire after the configured window (900 seconds in the example proxy). If a user spends more than 15 minutes on the upload screen before tapping Upload, the URL will be expired and the PUT will return a 403 AccessDenied error. Handle this in the action flow: before executing the PUT, check whether more than 10 minutes have passed since requesting the URL (use a Page State variable to track the request timestamp), and if so, call getPresignedUrl again to fetch a fresh URL. For displaying uploaded images, construct the public object URL: https://s3.{region}.wasabisys.com/{bucket}/{key} if the bucket has public read access, or call your proxy's /wasabiPresign endpoint with operation=GET to generate a presigned download URL for private objects. If you need help building or tuning the presign proxy or the FlutterFlow upload flow, RapidDev's team handles FlutterFlow integrations like this regularly — schedule a free scoping call at rapidevelopers.com/contact.
Pro tip: For public media (images the whole app can see), make the Wasabi bucket public and use the direct HTTPS URL for display — no presigned GET URL needed, which simplifies the Image widget binding.
Expected result: Files upload successfully on both mobile and web. Presigned URL expiry is handled gracefully. Uploaded objects are accessible via public or presigned URLs in the app.
Common use cases
User-generated content app with cheap media storage
A FlutterFlow community app where users upload profile photos, short videos, and audio clips. Media is stored in Wasabi rather than Firebase Storage because the no-egress-fee model keeps costs flat even as the user base grows and downloads increase. The presign proxy generates PUT URLs for uploads and public GET URLs (or presigned GET URLs for private content) for display in the app.
Build a community profile page where users can upload a profile photo that gets stored in Wasabi and displayed back in the app using the public Wasabi object URL.
Copy this prompt to try it in FlutterFlow
Podcast hosting backend for a creator app
A FlutterFlow app where podcast creators upload episode MP3 files (often 30-150 MB) and the app serves them to listeners. Wasabi's flat storage pricing and zero download fees make it significantly cheaper than S3 or Firebase Storage for audio content that is downloaded repeatedly by many listeners. The proxy generates presigned PUT URLs for creator uploads and either public object URLs or presigned GET URLs for listener playback.
Create a creator dashboard where podcasters can upload new episode MP3 files to Wasabi, and a listener page that streams episodes from Wasabi using an Audio Player widget.
Copy this prompt to try it in FlutterFlow
Document archival app with long-term storage
A FlutterFlow app for small businesses to archive scanned documents (PDFs, images) to Wasabi as a cost-effective alternative to local storage or expensive SaaS document management. The app uploads new scans via presigned PUT URLs, lists archived files via a proxy list endpoint that returns clean JSON, and generates time-limited presigned GET URLs for viewing archived documents.
Build an archive browser that shows a list of stored documents from a Wasabi bucket, lets users tap to view a document in a WebView using a presigned URL, and provides an upload button for new scans.
Copy this prompt to try it in FlutterFlow
Troubleshooting
PUT to presigned URL returns 403 AccessDenied with SignatureDoesNotMatch
Cause: Either the presigned URL expired before the upload was attempted, the wrong regional endpoint was used when generating the URL, or additional headers (like Authorization) were added to the PUT request alongside the presigned URL.
Solution: Verify the Wasabi endpoint in your proxy function exactly matches the bucket's region (e.g., s3.us-east-1.wasabisys.com for us-east-1 buckets). Do not add any Authorization headers to a PUT request that uses a presigned URL — the signature is already in the URL. If the URL expired, add logic to refresh it before uploading.
Web upload fails with XMLHttpRequest error and CORS is mentioned in browser console
Cause: The Wasabi bucket does not have a CORS policy allowing PUT requests from the browser origin, or the CORS policy was saved incorrectly.
Solution: In the Wasabi console, open your bucket → Properties → CORS Configuration. Add an AllowedOrigin of * (for development) or your specific domain (for production), AllowedMethod of PUT and GET, and AllowedHeader of *. Save the policy. CORS policies can take a few minutes to propagate.
Proxy function returns 403 InvalidAccessKeyId or NoSuchBucket
Cause: The Access Key ID or Secret Access Key in the function's environment variables is incorrect, or the bucket name or region in the S3 client configuration does not match the actual bucket.
Solution: In your Firebase console, verify the function config values (firebase functions:config:get) match your Wasabi console access key and bucket name exactly. Wasabi bucket names and regions are case-sensitive and must match what is shown in the Wasabi console.
Wasabi is billed for the full minimum even though the app stores only a small amount of data
Cause: Wasabi applies a 1 TB minimum per month billing regardless of actual storage used. This is a common surprise for developers who signed up expecting AWS S3-style pay-per-byte billing.
Solution: This is a Wasabi pricing feature, not a bug. For apps with less than 1 TB of storage, Wasabi may be more expensive than AWS S3 or Firebase Storage. Verify current pricing and minimums at wasabi.com/pricing before committing to Wasabi for small-scale use cases.
Best practices
- Never put your Wasabi secret access key in FlutterFlow or in Dart code — always route signing through a backend proxy function that generates presigned URLs.
- Match the Wasabi endpoint URL in your proxy to the exact region of your bucket — using the wrong regional endpoint causes SignatureDoesNotMatch errors that are misleading to debug.
- Set a short presigned URL expiry (15 minutes is standard) to minimize the window of misuse if a URL is intercepted, and refresh it if the user waits before uploading.
- Configure bucket CORS rules before testing web builds — even if mobile works fine, CORS blocks all browser PUT requests to Wasabi buckets without an explicit policy.
- Do not add any Authorization or X-Amz headers to PUT requests that use presigned URLs — the signature is baked into the URL parameters, and extra headers change the signed payload.
- For public-read content like profile photos or podcast audio, set the bucket to public and use direct object URLs for display — simpler than generating presigned GET URLs for every image load.
- Factor in Wasabi's minimum billing when choosing it for small apps — the no-egress benefit becomes significant primarily once your download volume is high.
Alternatives
Backblaze B2 has the same presigned URL pattern and 10 GB free tier, making it a better fit for small apps that don't yet have 1 TB of data to justify Wasabi's minimum billing.
Dropbox connects to users' personal cloud storage with OAuth rather than requiring you to manage a storage bucket — better when users need to access their own files rather than app-owned storage.
OneDrive via Microsoft Graph is best for apps in the Microsoft 365 ecosystem where users already have OneDrive storage allocated — not for developer-managed app storage.
Frequently asked questions
Is Wasabi actually S3-compatible? Can I use the standard AWS SDK?
Yes, Wasabi is fully S3-compatible at the API level. You use the standard AWS SDK (JavaScript aws-sdk, Python boto3, etc.) and simply override the endpoint URL to point at your Wasabi region instead of the default AWS endpoint. All standard S3 operations — putObject, getObject, listObjectsV2, deleteObject, getSignedUrl — work without any code changes.
How does Wasabi pricing compare to AWS S3 for a media-heavy app?
Wasabi charges approximately $6.99/TB/month for storage with no egress fees and no per-request charges (verify current pricing at wasabi.com/pricing). AWS S3 charges for storage, requests, AND egress. For apps where users download content frequently (video streaming, podcast playback, image-heavy feeds), Wasabi's zero-egress model can save significantly. However, Wasabi has a 1 TB minimum, so very small apps may pay more than AWS S3.
Can I use Wasabi instead of Firebase Storage for user profile photos?
Yes. The pattern is the same: your presign proxy generates a PUT URL, the user uploads their photo, and you store the resulting object key in Firestore or Supabase. For public profile photos, make the bucket public and construct the URL directly as https://s3.{region}.wasabisys.com/{bucket}/{key}. For private content, generate presigned GET URLs via the proxy before each display.
What is the maximum file size I can upload using a presigned PUT URL?
Wasabi supports single-part uploads up to 5 GB via a presigned PUT URL. Files larger than 5 GB require the S3 multipart upload API (createMultipartUpload, uploadPart, completeMultipartUpload). For most FlutterFlow use cases — photos, audio clips, short videos, documents — single-part presigned PUT is sufficient.
Do I need a different Wasabi endpoint for each region?
Yes. Wasabi has region-specific endpoints, and using the wrong endpoint causes a SignatureDoesNotMatch error because the signing includes the endpoint host. Common endpoints are s3.us-east-1.wasabisys.com, s3.us-west-1.wasabisys.com, s3.eu-central-1.wasabisys.com, and s3.ap-southeast-1.wasabisys.com. Find your bucket's region in the Wasabi console and use the matching endpoint in both the proxy function and any direct URL construction.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation