# Amazon S3

- Tool: FlutterFlow
- Difficulty: Advanced
- Time required: 60 minutes
- Last updated: July 2026

## TL;DR

Connect FlutterFlow to Amazon S3 using a pre-signed URL proxy pattern. A Firebase Cloud Function (or Supabase Edge Function) holds your AWS credentials server-side and mints short-lived upload and download URLs. FlutterFlow's Upload Media action gets the file bytes, then a PUT API Call sends the file directly to S3 using the pre-signed URL — no AWS keys ever touch the compiled Flutter app.

## Upload Files to Amazon S3 from FlutterFlow Using Pre-Signed URLs

Amazon S3 is the world's most widely used object storage service, and it is a natural fit for storing product images, user avatars, and generated files in a FlutterFlow app. The challenge is authentication: S3 uses AWS SigV4, a cryptographic signing algorithm that requires an AWS Secret Access Key. If you signed requests in Dart code running on the user's device, that secret key would be compiled into your Flutter app — visible to anyone who decompiles the APK or IPA. This is a critical security risk that would give an attacker full write (or delete) access to your S3 bucket.

The solution used by every serious S3 integration is the pre-signed URL pattern. Your server — in this case a Firebase Cloud Function — holds the AWS credentials safely in environment variables. When your FlutterFlow app needs to upload a file, it asks the Cloud Function for a temporary upload URL. The Cloud Function creates a pre-signed PUT URL: an S3 URL with the AWS signature baked into the query parameters and an expiration time (typically 5–15 minutes). The app then PUTs the file bytes directly to that URL. S3 validates the embedded signature without the app ever holding an AWS key. The same pattern works for generating temporary download URLs for private objects.

Amazon S3 pricing starts with a free tier of 5 GB storage, 20,000 GET requests, and 2,000 PUT requests per month for the first 12 months (verify current free tier details in AWS documentation, as limits may change). After that, storage costs approximately $0.023 per GB per month for S3 Standard in US regions — verify pricing for your region in the AWS pricing calculator. S3's throughput is extremely high (thousands of requests per second per prefix), so it scales effortlessly for even large app user bases.

## Before you start

- An AWS account (free tier is sufficient to start)
- An S3 bucket created in the AWS Console with a meaningful name (e.g., my-app-uploads)
- An IAM user with an access key that has only s3:PutObject and s3:GetObject permissions on your specific bucket
- A Firebase project for the Cloud Function (or a Supabase project if you prefer Supabase Edge Functions)
- A FlutterFlow project on any plan — the Upload Media action is available on all plans

## Step-by-step guide

### 1. Create an S3 bucket and IAM user with least-privilege permissions

Log in to the AWS Management Console at console.aws.amazon.com and navigate to the S3 service. Click Create Bucket. Choose a unique name (bucket names are globally unique across all AWS accounts) and select the AWS region closest to your users — this affects latency and data residency. For initial setup, leave Block all public access enabled; you do not want a public write bucket. Click Create Bucket. Next, navigate to IAM in the AWS Console. Click Users in the left sidebar, then click Add Users. Name the user something like flutterflow-s3-uploader. On the Permissions screen, click Attach policies directly and then click Create Policy. In the policy editor, switch to JSON mode and paste a policy that grants only s3:PutObject and s3:GetObject on your specific bucket (see the code block below) — never grant s3:* or broad administrative permissions. Complete the policy creation, attach it to the user, and then open the user's Security Credentials tab to generate an Access Key. Download or copy the Access Key ID and Secret Access Key. Store these securely — you will add them to Firebase environment variables, not to your FlutterFlow project.

```
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowFlutterFlowUploads",
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject"
      ],
      "Resource": "arn:aws:s3:::YOUR-BUCKET-NAME/*"
    }
  ]
}
```

**Expected result:** You have an S3 bucket created and an IAM user with an access key and a minimal policy that allows only PutObject and GetObject on that bucket.

### 2. Deploy a Firebase Cloud Function to mint pre-signed URLs

The Cloud Function is the only place AWS credentials will ever exist. Open the Firebase Console, navigate to your project, and go to Cloud Functions. Create a new function named generateS3PresignedUrl. The function receives a filename and optional contentType from FlutterFlow in the request body, uses the AWS SDK for Node.js to create a pre-signed PUT URL with an expiration of 600 seconds (10 minutes), and returns the signed URL and the resulting S3 object key to the app. Set the REQUIRED environment variables — AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, and S3_BUCKET_NAME — using Firebase environment config (or Secret Manager for production). The function also validates that the caller is authenticated with Firebase Auth before issuing a URL, preventing unauthenticated users from uploading files to your bucket. For download URL generation, add a second export named generateS3DownloadUrl that follows the same pattern but uses getObject instead of putObject. Deploy both functions using the Firebase Console's inline editor or your preferred deployment method (without using the CLI on this device).

```
// Firebase Cloud Function: index.js
const functions = require('firebase-functions');
const AWS = require('aws-sdk');

const s3 = new AWS.S3({
  accessKeyId: process.env.AWS_ACCESS_KEY_ID,
  secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
  region: process.env.AWS_REGION,
});

// Generate pre-signed upload URL
exports.generateS3PresignedUrl = functions.https.onCall(async (data, context) => {
  if (!context.auth) {
    throw new functions.https.HttpsError('unauthenticated', 'Login required');
  }

  const { filename, contentType } = data;
  const bucket = process.env.S3_BUCKET_NAME;
  // Organize by user so files are isolated
  const key = `uploads/${context.auth.uid}/${Date.now()}-${filename}`;

  const params = {
    Bucket: bucket,
    Key: key,
    ContentType: contentType || 'application/octet-stream',
    Expires: 600, // 10 minutes
  };

  const uploadUrl = await s3.getSignedUrlPromise('putObject', params);
  return { uploadUrl, objectKey: key };
});

// Generate pre-signed download URL for private objects
exports.generateS3DownloadUrl = functions.https.onCall(async (data, context) => {
  if (!context.auth) {
    throw new functions.https.HttpsError('unauthenticated', 'Login required');
  }

  const { objectKey } = data;
  const params = {
    Bucket: process.env.S3_BUCKET_NAME,
    Key: objectKey,
    Expires: 3600, // 1 hour
  };

  const downloadUrl = await s3.getSignedUrlPromise('getObject', params);
  return { downloadUrl };
});
```

**Expected result:** Both Cloud Functions deploy successfully. Testing the generateS3PresignedUrl function with a filename in the Firebase Console returns a pre-signed S3 URL with a signature in the query parameters.

### 3. Configure S3 CORS for FlutterFlow web builds

Native iOS and Android builds bypass CORS entirely — the operating system does not enforce the same-origin policy. However, if your FlutterFlow app targets the web (either for web deployment or for testing in FlutterFlow's web Run Mode), the browser will block PUT requests to S3 unless the bucket has a CORS configuration that allows your origin. Open the AWS Console, navigate to your S3 bucket, click the Permissions tab, and scroll down to Cross-origin resource sharing (CORS). Click Edit and paste the CORS configuration from the code block below. This allows PUT and GET requests from any origin (replace * with your specific app domains in production for tighter security), passes the required Content-Type header, and allows the browser to see the response. After saving, wait a minute for CORS changes to propagate before testing in the browser.

```
[
  {
    "AllowedHeaders": [
      "Content-Type",
      "Content-Length",
      "Authorization"
    ],
    "AllowedMethods": [
      "PUT",
      "GET",
      "HEAD"
    ],
    "AllowedOrigins": [
      "*"
    ],
    "ExposeHeaders": [
      "ETag"
    ],
    "MaxAgeSeconds": 3000
  }
]
```

**Expected result:** The S3 bucket CORS configuration is saved. Preflight OPTIONS requests from a browser to the S3 bucket no longer return a CORS error, and PUT requests from the FlutterFlow web preview succeed.

### 4. Build the file upload flow in FlutterFlow

Now wire up the upload flow in FlutterFlow. First, create an API Group named S3Proxy pointing at your Firebase Cloud Function's base URL (the HTTPS trigger URL base, not the specific function path). Add two API Calls: GetPresignedUploadUrl (POST to the generateS3PresignedUrl function URL) and PutFileToS3 (PUT to a dynamic URL you will supply at runtime). For the PutFileToS3 call, set the Method to PUT, leave the URL field as a variable {{presignedUrl}}, and set the body to receive binary file data. In your FlutterFlow canvas, add an Upload button widget to a page. In the Action Flow Editor for that button, add the following action sequence: First, add an Upload Media action — set it to allow images or files as needed and store the result in a page variable called uploadedFile. Second, add an API Call action to call GetPresignedUploadUrl, passing the filename (from the uploadedFile's name property) and the content type. Store the returned uploadUrl in a page variable presignedUrl and the returned objectKey in a page variable s3ObjectKey. Third, add a second API Call action to call PutFileToS3, passing the presignedUrl as the URL and the uploadedFile bytes as the body. Fourth, on success, save the s3ObjectKey to your Firestore or Supabase database linked to the current user or product record.

```
// Action sequence pseudocode (implement in Action Flow Editor)
// Step 1: Upload Media
//   Store result in: pageVariable.uploadedFile

// Step 2: Call GetPresignedUploadUrl
//   Body: { "filename": uploadedFile.name, "contentType": uploadedFile.mimeType }
//   Store response.uploadUrl → pageVariable.presignedUrl
//   Store response.objectKey → pageVariable.s3ObjectKey

// Step 3: Call PutFileToS3 
//   URL: pageVariable.presignedUrl (the full pre-signed URL from S3)
//   Method: PUT
//   Body: uploadedFile.bytes (raw binary)
//   Headers: Content-Type: uploadedFile.mimeType
//   NOTE: No Authorization header needed — signature is in the URL

// Step 4: On success (status 200)
//   Save s3ObjectKey to Firestore/Supabase under the current document
```

**Expected result:** Tapping the Upload button triggers the three-step sequence: the Upload Media picker opens, the user selects a file, the file is uploaded to S3, and the S3 object key is saved to the database. The S3 bucket shows the uploaded file in the AWS Console.

### 5. Display S3 files with pre-signed download URLs

Private S3 objects cannot be accessed with a public URL — you need a pre-signed GET URL, just like the upload pattern. Create a second API Group named S3DownloadProxy (or add to the existing S3Proxy group) pointing at the generateS3DownloadUrl Cloud Function. When your FlutterFlow app needs to display an image or link to a file stored in S3, trigger this API call with the objectKey from your database, store the returned downloadUrl in a page variable, and bind it to an Image widget's Network Image URL or a link widget's URL. Pre-signed download URLs have an expiration time (set to 1 hour in the example Cloud Function — adjust based on your use case). Do not persist pre-signed URLs in your database; always generate a fresh one when the page loads. For frequently-accessed public assets like product thumbnails, consider making the S3 objects public (or using CloudFront as a CDN in front of S3) to avoid generating a URL on every screen load.

```
// Display flow in Action Flow Editor
// Trigger: On page load or when productId changes

// Step 1: Fetch the objectKey from Firestore/Supabase
//   Store: pageVariable.objectKey

// Step 2: Call generateS3DownloadUrl API
//   Body: { "objectKey": pageVariable.objectKey }
//   Store: response.downloadUrl → pageVariable.downloadUrl

// Step 3: Bind to widget
//   Image widget → Network Image URL: pageVariable.downloadUrl
//   Link widget → URL: pageVariable.downloadUrl

// For public bucket objects (thumbnails, product images):
// Direct URL: https://YOUR-BUCKET.s3.YOUR-REGION.amazonaws.com/path/to/file
// No pre-signed URL needed for public objects
```

**Expected result:** The Image widget on your FlutterFlow page displays the correct S3-stored image, fetched via the pre-signed download URL generated by the Cloud Function. Private bucket objects are visible in-app but inaccessible from public URLs.

## Best practices

- Never embed AWS Access Key IDs or Secret Access Keys in FlutterFlow — always mint pre-signed URLs from a server-side function that holds credentials in environment variables.
- Use IAM least-privilege access: grant only s3:PutObject and s3:GetObject on your specific bucket — never s3:* or administrator access to the IAM user used by your app.
- Set a sensible pre-signed URL expiry: 10 minutes for uploads (enough time to complete the upload) and 1 hour for downloads (long enough for a user session). Regenerate on every page load for private assets.
- Configure S3 CORS if your FlutterFlow app targets the web, even if you are primarily building for mobile — web Run Mode in FlutterFlow is browser-based and enforces CORS.
- Organize S3 objects by user ID in the key path (e.g., uploads/{uid}/filename) to simplify per-user access control and make it easy to clean up user data if needed.
- Consider using CloudFront in front of S3 for frequently accessed public assets like product thumbnails — it reduces latency and S3 GET request costs for high-traffic apps.
- Store only the S3 object key in your database (Firestore or Supabase), not the pre-signed URL — URLs expire, but the object key is permanent and can generate a fresh URL on demand.
- Test the full upload flow on a real device or simulator before deploying — FlutterFlow's web Run Mode handles file uploads differently than native builds and may not accurately reflect the production experience.

## Use cases

### Product image uploader for an e-commerce management app

Build a FlutterFlow app that lets store staff upload product photos directly from their phone to an S3 bucket. The app uses FlutterFlow's Upload Media action to capture the image, requests a pre-signed PUT URL from a Firebase Cloud Function, and sends the file to S3. The returned S3 object URL is then saved in a Firestore document alongside the product record.

Prompt example:

```
Let me take or choose a photo from my phone and upload it as the product image for the selected product. Show a progress indicator while uploading and display the uploaded image when complete.
```

### User profile photo upload and display

Create a FlutterFlow profile screen that lets users upload a new profile photo. The app stores uploaded images in a private S3 bucket, then requests a short-lived pre-signed GET URL to display the current photo in an Image widget. The private bucket prevents public access to user photos while still allowing the app to render them.

Prompt example:

```
Show my current profile photo and let me tap it to upload a new one from my camera roll. After uploading, update the displayed photo immediately without reloading the page.
```

### Order attachment uploader for a B2B ordering app

Build a FlutterFlow screen that lets business customers attach PDF purchase orders, spec sheets, or design files to their orders. Files are uploaded to an S3 bucket organized by order ID. The Cloud Function mints a pre-signed GET URL when the operations team opens an order, so they can preview the attachment in-app using FlutterFlow's in-app browser or PDF viewer widget.

Prompt example:

```
Let me attach a PDF or image file to the current order. Show the filename and file size after uploading, and let me tap it to preview the file.
```

## Troubleshooting

### XMLHttpRequest error or 'Network request failed' when PUT-ing to the pre-signed URL in web build

Cause: The S3 bucket CORS configuration is missing or does not include the PUT method and Content-Type header. The browser blocks the cross-origin PUT request even though native builds would succeed.

Solution: Open the S3 bucket in the AWS Console → Permissions tab → Cross-origin resource sharing (CORS). Ensure AllowedMethods includes PUT and GET, AllowedHeaders includes Content-Type, and AllowedOrigins includes the FlutterFlow preview domain or *. Wait 1–2 minutes for CORS changes to propagate and test again.

### 403 SignatureDoesNotMatch when PUT-ing to the pre-signed URL

Cause: The Content-Type header sent with the PUT request does not match the contentType used when the pre-signed URL was generated, or the pre-signed URL has expired.

Solution: Ensure you pass the same contentType string (e.g., 'image/jpeg') to both the Cloud Function when generating the URL and as the Content-Type header on the PUT request. If the URL expired (after the configured TTL, e.g., 10 minutes), request a new pre-signed URL and retry the upload.

### Cloud Function returns 403 or 'unauthenticated' error when FlutterFlow calls it

Cause: The FlutterFlow app is calling the Cloud Function without a Firebase Auth session, or the Firebase Auth ID token is not being passed in the request.

Solution: Ensure the user is logged in with Firebase Auth before triggering the upload flow. In the Action Flow Editor, add a Conditional Check at the start that verifies the user is authenticated, and redirect to the login page if not. The Firebase callable function SDK handles token passing automatically when called via the Firebase SDK — if you are calling the HTTP trigger URL directly via API Call, add the Firebase Auth ID token as a Bearer header.

### Uploaded file appears in S3 but its Content-Type is set to 'application/octet-stream' instead of the correct type

Cause: The contentType was not passed to the Cloud Function when generating the pre-signed URL, so S3 defaulted to the generic binary content type.

Solution: In the FlutterFlow Upload Media action result, access the file's MIME type property and pass it explicitly to the GetPresignedUploadUrl API Call body as the contentType field. Common values are 'image/jpeg', 'image/png', and 'application/pdf'.

## Frequently asked questions

### Why can't I just put the AWS Access Key in the FlutterFlow API Call header?

AWS SigV4 signing requires a Secret Access Key, and anyone who decompiles your APK or IPA can extract strings from the binary. A leaked AWS key gives an attacker write and potentially delete access to your S3 bucket. The pre-signed URL pattern keeps the secret exclusively on the server, which is the industry-standard approach for S3 access from mobile apps.

### Do I need a Firebase Cloud Function, or can I use Supabase Edge Functions instead?

Either works. If your FlutterFlow project already uses Supabase, a Supabase Edge Function (Deno-based) can generate the pre-signed URL using the AWS SDK for Deno, and you call it from a FlutterFlow API Call. The logic is identical — the only difference is the runtime and how you set environment variables.

### How do I handle large file uploads without timing out?

The pre-signed URL approach bypasses your Cloud Function for the actual upload — the file goes directly from FlutterFlow to S3, so Cloud Function timeouts are not a concern for large files. S3 supports objects up to 5 TB. For files above 5 GB, you would need S3 multipart upload, which is a more complex pattern not covered here.

### Can I use S3 to serve product images in a public e-commerce catalog?

Yes. For public catalog images, you can enable public read access on the specific S3 path (e.g., /product-images/*) and serve them via a stable URL like https://your-bucket.s3.us-east-1.amazonaws.com/product-images/product-123.jpg. For better performance, add CloudFront as a CDN layer in front of S3. Public read still protects against public writes — keep the IAM user's write credentials server-side. If you'd like help configuring the CDN and security policies for a production catalog, RapidDev's team can set this up — free scoping call at rapidevelopers.com/contact.

### How long are pre-signed URLs valid, and what happens when they expire?

You set the expiry when generating the URL in the Cloud Function. Typical values are 10 minutes for uploads and 1 hour for downloads. If a user tries to use an expired URL, S3 returns a 403 SignatureDoesNotMatch error. The fix is to always generate a fresh pre-signed URL on page load rather than caching or persisting them in the database.

---

Source: https://www.rapidevelopers.com/flutterflow-integrations/amazon-s3
© RapidDev — https://www.rapidevelopers.com/flutterflow-integrations/amazon-s3
