# Amazon S3

- Tool: Bubble
- Difficulty: Advanced
- Time required: 2–3 hours
- Last updated: July 2026

## TL;DR

Bubble cannot compute AWS SigV4 signatures natively — your IAM secret must never touch the browser. The correct pattern: a Backend Workflow calls a small external proxy (Lambda, Cloudflare Worker, or Supabase Edge Function) that generates a presigned PUT URL; the user's browser then uploads the file directly to S3 using that URL without passing through Bubble. S3 CORS must be configured for your Bubble domain. Requires a paid Bubble plan for Backend Workflows.

## Bubble + Amazon S3: The Presigned URL Proxy Pattern

Bubble has its own built-in file storage — when a user uploads a file via Bubble's native File Uploader element, it goes into Bubble's managed S3 bucket under Bubble's AWS account. You have no control over that bucket, no access to configure CORS or lifecycle rules, and no ability to set expiring access URLs on a per-file basis. For most app ideas, Bubble's built-in storage is fine. But when you need your own S3 bucket — for compliance, cost control, CDN integration, or custom access expiry — you are setting up a completely separate system that has nothing to do with Bubble's native storage.

The reason a proxy function is non-negotiable: AWS S3 requires all API requests to be signed using AWS Signature Version 4 (SigV4). This algorithm takes your IAM Secret Access Key as an input. If you put the Secret Access Key in a Bubble client-side action or expose it in browser network requests, any user who opens DevTools can steal it and perform unlimited operations on your S3 bucket and potentially your entire AWS account. Bubble's API Connector Private headers protect secrets from browser exposure, but SigV4 signing requires running an algorithm, not just passing a header — and Bubble's visual workflow engine cannot execute cryptographic algorithms.

The presigned URL pattern solves this cleanly. A serverless function (Lambda is the most natural fit since it's already in AWS, but Cloudflare Workers, Supabase Edge Functions, or any serverless platform works) holds your IAM credentials securely. It receives a request with the filename and content type, runs the SigV4 signing algorithm using the AWS SDK, and returns a time-limited presigned PUT URL. This URL contains the signature in its query parameters. The browser can PUT a file to this URL with no additional auth headers — the signature is already there. Your IAM secret never leaves the Lambda environment.

The upload flow in Bubble has three stages: (1) Backend Workflow calls the Lambda proxy via API Connector to get a presigned PUT URL; (2) the URL is passed to the client side via return data or App State; (3) a client-side action (using the Toolbox plugin's Run JavaScript or a custom plugin) PUTs the file bytes to the presigned URL. The actual S3 upload happens browser-to-S3 — it does not consume Bubble Workload Units for the data transfer itself, only for the Backend Workflow call.

S3 CORS configuration is also mandatory. The browser's Same-Origin Policy blocks cross-origin PUT requests unless the target server (S3) explicitly allows them. You configure this in the S3 bucket's CORS rules, specifying your Bubble domain in the AllowedOrigins array.

Note on this page vs the aws-s3 page: both pages cover AWS S3, but this 'amazon-s3' page focuses on file upload and storage use cases (uploading user files, serving them back). The separate 'aws-s3' page (Database & Backend category) focuses on using S3 as a backend data store for app configuration and structured data. The integration pattern is similar but the use case and step focus differ.

## Before you start

- A Bubble app on a paid plan (Starter or above) — Backend Workflows required for the presigned URL call are not available on the free plan
- An AWS account with permissions to create IAM users, S3 buckets, and Lambda functions
- An S3 bucket created in the AWS region closest to your users, with Block Public Access settings reviewed
- Basic familiarity with the AWS Console (IAM, Lambda, and S3 sections)
- The Bubble API Connector plugin installed (Plugins tab → Add plugins → search 'API Connector' by Bubble)

## Step-by-step guide

### 1. Create an S3 bucket and configure CORS

In the AWS Console, go to S3 → Create bucket. Choose a name and region. Under 'Block Public Access', keep all blocks enabled (your files will be served via presigned URLs, not public URLs, so the bucket should be private).

After creating the bucket, go to the bucket → Permissions tab → CORS configuration → Edit. Paste the following CORS rule, replacing the AllowedOrigins values with your actual Bubble app domains:

```json
[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["PUT", "GET"],
    "AllowedOrigins": [
      "https://yourapp.bubbleapps.io",
      "https://www.yourcustomdomain.com"
    ],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3000
  }
]
```

This CORS rule is mandatory for browser-side uploads. Without it, the browser's Same-Origin Policy rejects the PUT request to S3 even if the presigned URL is valid — the browser sends a preflight OPTIONS request first, and S3 must respond with the correct CORS headers for the actual PUT to proceed.

Include all Bubble domains your app uses: the default bubbleapps.io subdomain and any custom domains you have connected. CORS changes take effect immediately — no redeployment needed. Wait 1-2 minutes before testing if you just updated an existing CORS rule.

Also go to the bucket's Properties tab and note the bucket name and region — you will need both for the Lambda function in the next step.

```
[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["PUT", "GET"],
    "AllowedOrigins": [
      "https://yourapp.bubbleapps.io",
      "https://www.yourcustomdomain.com"
    ],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3000
  }
]
```

**Expected result:** S3 bucket created in your chosen region with Block Public Access enabled. CORS rule saved allowing PUT and GET from your Bubble app domain.

### 2. Create an IAM user with least-privilege S3 permissions

In the AWS Console, go to IAM → Users → Create user. Name it `bubble-s3-upload`. Under permissions, choose 'Attach policies directly' → Create policy. In the JSON tab, paste a policy that grants only the minimum required operations on your specific bucket:

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject"
      ],
      "Resource": "arn:aws:s3:::your-bucket-name/*"
    }
  ]
}
```

Replace `your-bucket-name` with your actual bucket name. Click through to create the policy (name it `BubbleS3Policy`) and attach it to the IAM user.

After creating the user, go to the user's Security credentials tab → Create access key. Choose 'Application running outside AWS'. Download the CSV file — it contains the Access Key ID and Secret Access Key. The Secret Access Key is shown only once; if you close this dialog without copying it, you must create a new key.

This IAM user can only put and get objects in your specific bucket. It cannot list bucket contents, delete files, access other buckets, or perform any other AWS operations. This least-privilege principle limits the damage if the key is ever exposed.

Note: for even better security, use an IAM Role attached to your Lambda function's execution role instead of an IAM user with access keys. Lambda can then use its execution role credentials, and no access keys need to be managed or rotated. This is covered in the tip below.

```
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject"
      ],
      "Resource": "arn:aws:s3:::your-bucket-name/*"
    }
  ]
}
```

**Expected result:** IAM user (or Lambda execution role) with BubbleS3Policy attached — restricted to s3:PutObject and s3:GetObject on your specific bucket only. Access Key ID and Secret Access Key downloaded and stored securely.

### 3. Deploy a presigned URL Lambda function

In the AWS Console, go to Lambda → Create function → Author from scratch. Name it `s3-presign`, runtime Node.js 20.x. Under Configuration → Environment variables, add `S3_BUCKET` = your bucket name, `S3_REGION` = your bucket's region. Attach the `BubbleS3Policy` to the function's execution role (or add it to the existing role).

In the Lambda code editor, paste this function:

```javascript
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { randomUUID } from 'crypto';

const s3 = new S3Client({ region: process.env.S3_REGION });

export const handler = async (event) => {
  const body = JSON.parse(event.body || '{}');
  const { fileName, contentType, folder = 'uploads', operation = 'put' } = body;

  if (!fileName) {
    return {
      statusCode: 400,
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ error: 'fileName is required' })
    };
  }

  const key = operation === 'get'
    ? fileName
    : `${folder}/${randomUUID()}/${fileName}`;

  const command = operation === 'get'
    ? new GetObjectCommand({ Bucket: process.env.S3_BUCKET, Key: key })
    : new PutObjectCommand({ Bucket: process.env.S3_BUCKET, Key: key, ContentType: contentType });

  const url = await getSignedUrl(s3, command, {
    expiresIn: operation === 'get' ? 86400 : 900
  });

  return {
    statusCode: 200,
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ url, key })
  };
};
```

Deploy the function. Add an API Gateway trigger: Function overview → Add trigger → API Gateway → Create new REST API → Open. Note the generated API endpoint URL. Optionally, protect the endpoint with an API key in API Gateway usage plans to prevent unauthorized presign requests.

Test the function from the AWS Console Test tab with event body: `{"fileName": "test.jpg", "contentType": "image/jpeg"}`. The response should contain a `url` (presigned PUT URL) and `key` (S3 object key).

```
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { randomUUID } from 'crypto';

const s3 = new S3Client({ region: process.env.S3_REGION });

export const handler = async (event) => {
  const body = JSON.parse(event.body || '{}');
  const { fileName, contentType, folder = 'uploads', operation = 'put' } = body;

  if (!fileName) {
    return {
      statusCode: 400,
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ error: 'fileName is required' })
    };
  }

  const key = operation === 'get'
    ? fileName
    : `${folder}/${randomUUID()}/${fileName}`;

  const command = operation === 'get'
    ? new GetObjectCommand({ Bucket: process.env.S3_BUCKET, Key: key })
    : new PutObjectCommand({ Bucket: process.env.S3_BUCKET, Key: key, ContentType: contentType });

  const url = await getSignedUrl(s3, command, {
    expiresIn: operation === 'get' ? 86400 : 900
  });

  return {
    statusCode: 200,
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ url, key })
  };
};
```

**Expected result:** Lambda function deployed with API Gateway endpoint. Test call from AWS Console returns `{url: 'https://your-bucket.s3.amazonaws.com/uploads/uuid/test.jpg?X-Amz-...', key: 'uploads/uuid/test.jpg'}`.

### 4. Configure Bubble API Connector to call the proxy

In your Bubble app, go to Plugins tab → Add plugins → install 'API Connector' by Bubble if not already installed. Click 'API Connector' in the left panel.

Click 'Add another API'. Name it 'S3 Presign Proxy'. If your API Gateway has an API key, add a shared header:
- Header name: `x-api-key`
- Value: your API Gateway API key
- Check the **Private** checkbox

Click 'Add another call':
- Name: `Get Presigned URL`
- Method: POST
- URL: `https://your-api-gateway-id.execute-api.us-east-1.amazonaws.com/default/s3-presign`
- Body type: JSON
- Body fields: `fileName` (text, dynamic), `contentType` (text, dynamic), `folder` (text, value 'uploads'), `operation` (text, value 'put')

The call configuration:

```json
{
  "method": "POST",
  "url": "https://your-api-gateway-id.execute-api.us-east-1.amazonaws.com/default/s3-presign",
  "headers": {
    "x-api-key": "<private>",
    "Content-Type": "application/json"
  },
  "body": {
    "fileName": "<dynamic>",
    "contentType": "<dynamic>",
    "folder": "uploads",
    "operation": "put"
  }
}
```

Set 'Use as' to **Action** (triggered by user click, not data binding). Click **Initialize call**: enter test values `fileName = test.jpg`, `contentType = image/jpeg`, click Run. Bubble should detect two response fields: `url` (text — the presigned PUT URL) and `key` (text — the S3 object key). Click Save.

If Initialize call returns 'There was an issue setting up your call', test the Lambda directly in the AWS Console first. A non-200 response from Lambda will block the initialization. Check that your API Gateway URL is correct and the Lambda is deployed (not just saved).

```
{
  "method": "POST",
  "url": "https://your-api-gateway-id.execute-api.us-east-1.amazonaws.com/default/s3-presign",
  "headers": {
    "x-api-key": "<private>",
    "Content-Type": "application/json"
  },
  "body": {
    "fileName": "<dynamic>",
    "contentType": "<dynamic>",
    "folder": "uploads",
    "operation": "put"
  }
}
```

**Expected result:** Bubble API Connector group 'S3 Presign Proxy' is configured with a Private API key header. The 'Get Presigned URL' call is initialized and Bubble detects the url and key response fields.

### 5. Build the Backend Workflow and browser-side upload action

In Bubble's Backend Workflows section, click 'New API Workflow'. Name it `Get S3 Upload URL`. Add parameters: `file_name` (text), `content_type` (text).

In the workflow body:
1. Action: Plugins → API Connector → Get Presigned URL. Set `fileName` = Parameter file_name, `contentType` = Parameter content_type.
2. Action: Return data from backend workflow. Return fields: `presigned_url` = Step 1 result → url, `object_key` = Step 1 result → key.

On the client side, set up the upload button workflow:
1. Trigger: Button 'Upload to S3' is clicked
2. Action: Backend Workflows → Schedule API Workflow → `Get S3 Upload URL` → pass `file_name` = FileUploader A's value's name, `content_type` = FileUploader A's value's file type
3. Action: Set App State 'presigned_url' = result of Step 2's presigned_url
4. Action: Set App State 'object_key' = result of Step 2's object_key
5. Action: (Toolbox plugin) Run JavaScript — execute the browser-side PUT:

```javascript
const file = bubble_fn_fileuploader_value; // Reference to file uploader value
const presignedUrl = bubble_fn_presigned_url; // From App State
const contentType = file.type;

await fetch(presignedUrl, {
  method: 'PUT',
  body: file,
  headers: { 'Content-Type': contentType }
});
```

6. After the PUT succeeds, create a database Thing (data type: S3File) with fields: `s3_key` = App State object_key, `original_filename` = FileUploader A's value's name, `uploaded_by` = Current User.

**Privacy rules**: Go to Data tab → Data types → S3File → Privacy. Add a rule: when Current User is the uploaded_by user → Allow find and all field reads. Without this, all logged-in users can query all S3File records.

RapidDev's team has built Bubble apps with the full S3 presigned URL architecture — if you get stuck on the JavaScript upload step or the Backend Workflow return data pattern, book a free scoping call at rapidevelopers.com/contact.

```
// Run JavaScript action (Toolbox plugin) — browser-side S3 PUT
async function uploadToS3(presignedUrl, file) {
  const response = await fetch(presignedUrl, {
    method: 'PUT',
    body: file,
    headers: {
      'Content-Type': file.type
    }
  });
  if (!response.ok) {
    throw new Error(`S3 upload failed: ${response.status} ${response.statusText}`);
  }
  return true;
}
```

**Expected result:** File upload flow works end-to-end: button click triggers Backend Workflow → gets presigned URL → browser PUTs file to S3 → S3 key saved in Bubble database. S3File records are protected by privacy rules.

### 6. Test the full upload flow and set up monitoring

Run your Bubble app in preview mode. Select a test image (JPEG, under 5 MB) and click the upload button.

**What to watch:**
1. Bubble Logs tab → Workflow logs: confirm the Backend Workflow `Get S3 Upload URL` runs and returns a presigned_url and object_key
2. Browser DevTools → Network tab: look for a PUT request to s3.amazonaws.com with a long URL containing `X-Amz-Signature` query parameters. The response should be `200 OK`
3. AWS Console → S3 bucket → Objects tab: the uploaded file should appear under the `uploads/` prefix

**Common failure points:**
- If the Backend Workflow runs but the browser PUT fails with CORS error: your S3 CORS rule does not include your Bubble preview domain. Add `https://yourapp.bubbleapps.io` to AllowedOrigins.
- If the PUT returns 403 SignatureDoesNotMatch: the Content-Type header in the browser PUT does not match what was signed into the URL. Use the exact MIME type from the file uploader's `file type` output.
- If nothing happens and no Backend Workflow appears in Logs: confirm you are on a paid Bubble plan. Backend Workflows are silently unavailable on the free plan.

**Monitor WU consumption**: Each Backend Workflow call to get a presigned URL costs Bubble WUs. In Settings → Workload, check the consumption after a few test uploads. If you are running a feature that allows many uploads per session, consider batching multiple file keys in a single Lambda call to reduce WU cost.

**Test file retrieval**: After uploading, trigger a new Backend Workflow call with `operation: 'get'` and the stored object_key. The returned URL should open the uploaded file in a new browser tab.

**Expected result:** Full end-to-end upload flow verified: presigned URL generated by Lambda, file PUT to S3 by browser, S3 key stored in Bubble database, presigned GET URL generated on demand returns the correct file. WU consumption monitored.

## Best practices

- Store only the S3 object key in Bubble's database — never the presigned URL itself. Presigned URLs expire; S3 keys do not. Generate fresh GET URLs on demand from the stored key each time a user needs to access a file.
- Scope the IAM policy to the minimum permissions your app actually needs: s3:PutObject and s3:GetObject on the specific bucket ARN only. Never grant s3:DeleteObject or s3:ListBucket unless your app explicitly needs those operations.
- Use randomized prefixes in S3 key names (e.g., `uploads/{uuid}/{filename}`) to avoid key collisions and benefit from S3's automatic partition distribution for high-throughput workloads.
- Configure S3 bucket CORS with your specific Bubble domain, not a wildcard (*). A wildcard AllowedOrigins in production means any website can use a stolen presigned URL to upload files to your bucket.
- Set Bubble privacy rules on every data type that stores S3 file references (keys, filenames, user associations). S3 bucket policies protect files at the storage layer; Bubble privacy rules protect the metadata in Bubble's database.
- Keep presigned PUT URL expiry short (900 seconds / 15 minutes). Long-lived PUT URLs increase the risk window if intercepted. GET URL expiry can be longer (1–24 hours) since they are read-only.
- Monitor S3 costs in AWS Cost Explorer and set a billing alert. A surge in PUT requests might indicate your Lambda endpoint is being called by unauthorized sources — add API Gateway usage plans and API keys to rate-limit access.
- For apps with significant upload volume, use S3 lifecycle rules to automatically transition files to cheaper storage classes (S3 Standard-IA or Glacier) after a set number of days, reducing long-term storage costs without any Bubble changes.

## Use cases

### User-generated media storage

Let users upload profile photos, portfolio images, or documents from your Bubble app into your own S3 bucket, giving you full control over storage costs, folder structure (by user ID or tenant), lifecycle rules for auto-deletion, and CDN delivery via CloudFront.

Prompt example:

```
Build a Bubble workflow where clicking 'Upload Photo' calls a Backend Workflow to get a presigned PUT URL from Lambda, then uploads the selected image file directly to S3 from the browser, and saves the returned S3 object key to the user's Bubble profile record.
```

### Private document access with expiring links

Store sensitive documents (contracts, invoices, medical records) in a private S3 bucket. Generate short-lived presigned GET URLs (15–60 minutes) in a Backend Workflow when an authorized user requests access — the URL expires and cannot be shared permanently.

Prompt example:

```
When an authenticated Bubble user clicks 'Download Contract', trigger a Backend Workflow that calls Lambda to generate a 30-minute presigned GET URL for the stored document S3 key, then display the URL as a download link in a popup that auto-closes after 5 minutes.
```

### Multi-tenant SaaS file storage

Organize uploads by customer account using S3 key prefixes (e.g., `tenants/{accountId}/{uuid}/{filename}`) so each tenant's files are logically isolated. Use IAM bucket policies to enforce access boundaries at the AWS level independent of Bubble's database queries.

Prompt example:

```
Pass the current user's account ID to the Backend Workflow so the Lambda generates a presigned PUT URL with key prefix `tenants/{accountId}/{uuid}/{filename}`. Store the full key in an Uploads data type linked to the account, and generate per-file GET URLs on demand for authorized users only.
```

## Troubleshooting

### Browser shows a CORS error on the S3 PUT request despite a valid presigned URL

Cause: The S3 bucket CORS configuration does not include your Bubble app's exact origin, or AllowedMethods does not include PUT.

Solution: Open S3 bucket → Permissions → CORS configuration. Verify AllowedOrigins contains your full Bubble domain with https:// (e.g., 'https://yourapp.bubbleapps.io'). Confirm AllowedMethods includes 'PUT'. CORS changes take 1-2 minutes to propagate — wait and retry before debugging further. Note that CORS applies to the S3 bucket, not to your Lambda proxy endpoint.

### S3 returns 403 SignatureDoesNotMatch on the PUT request

Cause: The Content-Type header sent in the browser PUT does not exactly match the contentType that was signed into the presigned URL when the Lambda generated it.

Solution: Ensure you pass the file's exact MIME type from Bubble's File Uploader element (use the 'file type' dynamic value) to the Backend Workflow and then to the Lambda. Do not hardcode content types. In the browser PUT fetch call, set the Content-Type header to the exact same MIME type. Even 'image/jpg' vs 'image/jpeg' will cause a SignatureDoesNotMatch error.

### Upload button does nothing and no Backend Workflow appears in the Logs tab

Cause: Backend Workflows are not available on Bubble's free plan. Scheduling API Workflows silently does nothing if the app is on the free tier.

Solution: Go to your Bubble app Settings → Plan and confirm you are on a paid plan (Starter or above). Upgrade if needed. After upgrading, retry the upload — the Backend Workflow should now appear in the Logs tab when triggered.

### Initialize call in the API Connector returns 'There was an issue setting up your call'

Cause: The Lambda function returned a non-200 response during the initialization test. Common causes: incorrect API Gateway URL, Lambda not deployed yet, or test parameters missing from the request body.

Solution: First test the Lambda directly from the AWS Console Test tab with the event body `{"body": "{\"fileName\": \"test.jpg\", \"contentType\": \"image/jpeg\"}"}`. Check the function's response and any CloudWatch log errors. After confirming the Lambda works, retry Initialize call in Bubble with fileName = 'test.jpg' and contentType = 'image/jpeg'. Check that the API Gateway URL in the Connector matches exactly what's shown in the API Gateway console.

### Presigned PUT URL works but the uploaded file cannot be retrieved — presigned GET URL returns 403

Cause: The IAM policy attached to the Lambda execution role does not include s3:GetObject, or the object key used for the GET URL does not match the key that was stored after the PUT.

Solution: Verify the IAM policy includes both s3:PutObject and s3:GetObject on the bucket ARN. Confirm that the object_key returned by the PUT presign call is exactly what you stored in Bubble's database, and that you pass this exact key (not a modified version) when requesting the GET presigned URL. Test from the AWS Console: go to S3 bucket → find the uploaded object → Object actions → Share with a presigned URL to confirm the key exists.

## Frequently asked questions

### Can I use Bubble's built-in File Uploader to upload to my own S3 bucket?

No. Bubble's native File Uploader stores files in Bubble's own managed S3 bucket under Bubble's AWS account — you have no access to configure or manage that bucket. To upload to your own S3 bucket, you must implement the presigned URL pattern described in this guide, which bypasses Bubble's native file handling entirely. The two systems are completely separate.

### Is this the same as the aws-s3 Bubble integration page?

They cover the same underlying service (Amazon S3) but focus on different use cases. This 'amazon-s3' page (Storage & Files category) focuses specifically on user file upload and serving — the presigned URL proxy pattern, CORS setup, and storing object keys in Bubble. The 'aws-s3' page (Database & Backend category) focuses on using S3 as a data store for app configuration, exports, and structured backend data. The core presigned URL pattern is similar in both.

### What happens if the presigned URL expires before the user finishes uploading?

S3 returns a 403 Access Denied error on the PUT request. Presigned PUT URLs expire at the time specified during generation (900 seconds / 15 minutes in this setup). If users typically take longer to select files, increase the expiry in the Lambda function (up to 7 days for IAM user credentials, up to 1 hour for IAM role credentials). You can also add a client-side check: if more than 12 minutes have passed since the presigned URL was generated, re-call the Backend Workflow to get a fresh URL before attempting the upload.

### Can I implement this on Bubble's free plan?

No. The presigned URL generation step requires a Backend Workflow, which is only available on paid Bubble plans (Starter and above). On the free plan, all workflows run client-side. Putting IAM credentials in client-side workflows would expose them to any user who opens browser DevTools. There is no safe way to implement custom S3 uploads on Bubble's free plan.

### Why do I need a Lambda function — can't I generate presigned URLs inside Bubble?

Bubble's visual workflow engine cannot execute the AWS SigV4 cryptographic signing algorithm, which requires your IAM Secret Access Key as an input. Any attempt to put the secret in a Bubble workflow exposes it to the browser. A Lambda function (or Cloudflare Worker or Supabase Edge Function) runs outside Bubble, holds the secret securely in environment variables, performs the signing, and returns only the time-limited presigned URL — never the secret itself.

---

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