# Backblaze B2

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

## TL;DR

Connect FlutterFlow to Backblaze B2 using the S3-compatible endpoint — point an AWS S3 SDK at s3.{region}.backblazeb2.com and use your B2 application key as the secret access key. Because SigV4 signing requires the secret key, a Firebase Cloud Function or Supabase Edge Function generates presigned PUT and GET URLs. FlutterFlow calls the proxy for a presigned URL, then PUTs file bytes directly to B2.

## Backblaze B2 S3-compatible API in FlutterFlow: port your S3 knowledge, pay less

If you already know AWS S3, you know how to use Backblaze B2's S3-compatible layer. The AWS SDK, the presigned URL pattern, the bucket and object key model — all of it transfers directly. The only change is the endpoint URL: instead of s3.amazonaws.com, you point the SDK at s3.{region}.backblazeb2.com (for example s3.us-west-004.backblazeb2.com for the us-west-004 region). Your B2 keyID becomes the AWS access key ID and your B2 applicationKey becomes the AWS secret access key.

The appeal for FlutterFlow founders building media-heavy apps is straightforward: Backblaze B2 offers 10 GB of free storage, then approximately $6/TB/month for additional storage. Egress costs approximately $0.01/GB after a free daily allowance of 3× your total stored data, and there are no minimum charges for small apps (unlike Wasabi's 1 TB minimum). For video, audio, and image storage where you control the bucket rather than connecting to a user's personal cloud, B2 S3 is a pragmatic, cost-effective choice.

The same SigV4 constraint that applies to AWS S3 applies here: you cannot compute a SigV4 signature in Dart without putting the secret key in the compiled app. The presigned URL proxy pattern is mandatory — your backend function holds the secret, generates the URL, and FlutterFlow makes a simple HTTP PUT with no auth headers. Verify current B2 pricing at backblaze.com/b2/cloud-storage-pricing.html before sharing specific numbers with users.

## Before you start

- A Backblaze account (free at backblaze.com — 10 GB free storage, no credit card required for the free tier)
- A B2 bucket created in the Backblaze console with the region and bucket name noted
- A B2 application key (keyID + applicationKey) with read/write access to the bucket
- A Firebase project (Cloud Functions) or Supabase project (Edge Functions) for the presign proxy
- A FlutterFlow project (API Calls available on all plans)

## Step-by-step guide

### 1. Create a B2 bucket and note the S3-compatible endpoint for your region

Log in to the Backblaze console at secure.backblaze.com. Click B2 Cloud Storage → Buckets in the left sidebar, then click Create a Bucket. Give the bucket a lowercase name, choose Private or Public depending on whether you need access-controlled downloads, and click Create a Bucket.

After creation, click the bucket name to open its details. Look for the Endpoint field — it will look like s3.us-west-004.backblazeb2.com or similar. Copy this endpoint exactly — the region number (like us-west-004) is specific to your bucket and must match in your proxy function. Using the wrong endpoint causes SignatureDoesNotMatch errors even if your credentials are correct.

Next, create an application key. Click App Keys in the left sidebar → Add a New Application Key. Name it, restrict it to the specific bucket you just created, and grant Read and Write access. Click Create New Key. Copy both the keyID and applicationKey immediately — the applicationKey is shown only once. These two values map to AWS credentials: keyID → access key ID, applicationKey → secret access key.

**Expected result:** You have a B2 bucket, an exact S3-compatible endpoint URL for that bucket's region, and a keyID + applicationKey pair copied and stored for use in the proxy function.

### 2. Deploy a presign proxy using the AWS S3 SDK pointed at B2

Deploy a Firebase Cloud Function or Supabase Edge Function that uses the standard AWS SDK with B2's S3-compatible endpoint. The function accepts requests with an operation type (putObject or getObject) and an object key (the file path within the bucket), generates a presigned URL valid for 15 minutes, and returns it to FlutterFlow.

The key configuration change versus a standard AWS S3 proxy is the endpoint: instead of using the default AWS endpoint, you pass the Wasabi-like endpoint to the S3 client constructor. The AWS SDK handles the SigV4 signing against this custom endpoint transparently.

Deploy the function and note its URL. Also add a list endpoint that calls listObjectsV2 and returns a clean JSON array of object keys — this powers the file browser in FlutterFlow.

Set the function's environment variables using firebase functions:config:set b2.key_id=YOUR_KEY_ID b2.secret_key=YOUR_APP_KEY b2.bucket=YOUR_BUCKET_NAME b2.endpoint=https://s3.YOUR-REGION.backblazeb2.com to keep credentials out of source code.

```
// Firebase Cloud Function — B2 S3-compatible presign proxy
// functions/index.js (Node.js)
const functions = require('firebase-functions');
const AWS = require('aws-sdk');

const s3 = new AWS.S3({
  endpoint: functions.config().b2.endpoint, // e.g. https://s3.us-west-004.backblazeb2.com
  region: 'us-west-004', // Match your bucket region
  accessKeyId: functions.config().b2.key_id,
  secretAccessKey: functions.config().b2.secret_key,
  signatureVersion: 'v4',
  s3ForcePathStyle: true, // Required for B2 S3-compatible endpoint
});

const BUCKET = functions.config().b2.bucket;

exports.b2Presign = functions.https.onRequest(async (req, res) => {
  res.set('Access-Control-Allow-Origin', '*');
  if (req.method === 'OPTIONS') return res.status(204).send('');
  if (req.method !== 'POST') return res.status(405).send('Method Not Allowed');

  const { operation, key, contentType } = req.body;
  if (!operation || !key) return res.status(400).json({ error: 'Missing operation or key' });

  const params = {
    Bucket: BUCKET,
    Key: key,
    Expires: 900, // 15 minutes
    ...(operation === 'PUT' && contentType ? { ContentType: contentType } : {}),
  };

  const method = operation === 'PUT' ? 'putObject' : 'getObject';
  try {
    const url = await s3.getSignedUrlPromise(method, params);
    res.json({ url, expires_in: 900 });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

exports.b2List = functions.https.onRequest(async (req, res) => {
  res.set('Access-Control-Allow-Origin', '*');
  if (req.method === 'OPTIONS') return res.status(204).send('');
  const { prefix } = req.query;
  const data = await s3.listObjectsV2({ Bucket: BUCKET, Prefix: prefix || '' }).promise();
  res.json({
    objects: data.Contents.map(o => ({
      key: o.Key,
      size: o.Size,
      last_modified: o.LastModified,
    }))
  });
});
```

**Expected result:** Two Cloud Function URLs — /b2Presign and /b2List — deployed and returning presigned URLs and object listings from Backblaze B2.

### 3. Create FlutterFlow API Groups for the proxy and for direct B2 PUT uploads

In FlutterFlow, you need two API Groups working together to complete a file upload.

Create API Group 1 — name it B2Proxy. Set the base URL to your Firebase function base URL (https://us-central1-your-project.cloudfunctions.net). Add a Content-Type header: application/json.

Add API Call 1 — name it getPresignedUrl. Method POST, endpoint /b2Presign. Body: {"operation":"{{operation}}","key":"{{key}}","contentType":"{{contentType}}"}. Add variables: operation (String), key (String), contentType (String). In the Response & Test tab, add the JSON path $.url. Click Test — pass operation=GET, key=test-image.jpg, contentType=image/jpeg and confirm you get a long URL back.

Add API Call 2 — name it listObjects. Method GET, endpoint /b2List. Optionally add a prefix variable for folder filtering. Add JSON paths $.objects[*].key, $.objects[*].size, $.objects[*].last_modified.

Create API Group 2 — name it B2DirectUpload. This group makes the actual PUT to B2 using the presigned URL. The presigned URL returned by B2 includes the hostname — so the full URL is the endpoint. Set base URL to a placeholder (you will override the full endpoint dynamically). Add one API Call named uploadToB2: method PUT. Body type Binary. Add a Content-Type header variable. The endpoint should be a variable {{presignedUrl}}.

Note: FlutterFlow's API Call endpoint field must be a path relative to the API Group's base URL. To PUT to a fully dynamic URL, set the API Group base URL to empty and pass the entire presigned URL as the endpoint variable — or use the HTTP Package custom action approach if the variable endpoint is not supported in your FlutterFlow version.

```
// B2Proxy API Group reference
{
  "base_url": "https://us-central1-your-project.cloudfunctions.net",
  "headers": { "Content-Type": "application/json" },
  "calls": [
    {
      "name": "getPresignedUrl",
      "method": "POST",
      "endpoint": "/b2Presign",
      "body": {
        "operation": "{{operation}}",
        "key": "{{key}}",
        "contentType": "{{contentType}}"
      },
      "response_paths": ["$.url"]
    },
    {
      "name": "listObjects",
      "method": "GET",
      "endpoint": "/b2List",
      "response_paths": ["$.objects[*].key", "$.objects[*].size", "$.objects[*].last_modified"]
    }
  ]
}
```

**Expected result:** B2Proxy API Group with getPresignedUrl and listObjects calls is working in FlutterFlow's test tab, returning B2 presigned URLs.

### 4. Configure B2 bucket CORS and wire the upload action flow in FlutterFlow

For FlutterFlow web builds, browser uploads to B2's S3-compatible endpoint require a bucket CORS policy. In the Backblaze console, open your bucket → Bucket Settings → CORS Rules. Add a rule that allows PUT and GET operations from your app's origin (use * for development, restrict to your domain for production). B2 accepts CORS rules in JSON format — save the policy and wait a few minutes for it to propagate. Native mobile builds (iOS and Android) do not enforce CORS.

Now wire the upload flow in FlutterFlow. Add an Upload button on your file page. In the Action Flow Editor, add these actions in sequence:

1. File Picker action — lets the user choose a file. Store the bytes in Page State variable fileBytes and the name in Page State variable fileName.

2. API Call action → B2Proxy → getPresignedUrl. Pass operation=PUT, key={{fileName}} (or a constructed key like uploads/{{userId}}/{{fileName}}), contentType=image/jpeg (or appropriate MIME type). Store the returned $.url in Page State variable presignedPutUrl.

3. API Call action → B2DirectUpload → uploadToB2. Set the endpoint to {{presignedPutUrl}}, Content-Type header to match the contentType you used when generating the URL, body type Binary bound to fileBytes. Crucially — add NO Authorization header, no X-Amz headers. The presigned URL is self-authorizing.

4. On success (response code 200), show a Snackbar and refresh the listObjects backend query on the ListView.

For displaying uploaded objects, construct the public object URL as https://s3.{region}.backblazeb2.com/{bucket}/{key} for public buckets, or call getPresignedUrl with operation=GET to get a time-limited download URL for private buckets.

**Expected result:** The upload button launches a file picker, requests a presigned URL from the proxy, and PUTs the file to B2. The file list refreshes after a successful upload.

### 5. Test on mobile and web, store object keys, and handle URL expiry

Test the upload flow on Android or iOS first (no CORS to worry about) using FlutterFlow's Run mode or a real device build. Select a small image, trigger the upload, and verify the file appears in the Backblaze console under your bucket. Check the Firebase function logs for any errors in the presign step.

For web testing, open the browser developer tools Network tab and watch the PUT request. A 200 response means the upload succeeded. If you see a CORS error or a 403 from B2, return to the Backblaze console and confirm the CORS policy is saved and includes your domain.

Once the upload works, make sure you are storing only the object key (for example uploads/user123/photo.jpg) in your database — not the presigned URL. Presigned URLs expire after 15 minutes. When you need to display the file later, call getPresignedUrl with operation=GET and the stored key to generate a fresh download URL. For public buckets, construct the permanent URL directly: https://s3.{region}.backblazeb2.com/{bucket}/{key}.

For very large files (over 100 MB), consider implementing the S3 multipart upload API in a Custom Action. The standard presigned PUT URL works for single-part uploads up to 5 GB, which covers most mobile use cases. For files you know will be large, request a presigned URL for each 5 MB part from the proxy and upload them sequentially in Dart.

**Expected result:** Files upload to B2 on both mobile and web. Object keys are stored in the database for permanent reference. Fresh presigned GET URLs are generated on demand for displaying private files.

## Best practices

- Never put your B2 applicationKey in FlutterFlow or Dart code — keep it exclusively in the backend proxy function's environment variables.
- Use s3ForcePathStyle: true when configuring the AWS SDK for B2 — without it, the SDK uses virtual-hosted-style URL construction that B2 does not support.
- Request a fresh presigned URL immediately before each upload in the action flow rather than caching URLs from page load.
- Store only the object key in Firestore or Supabase — the presigned URL expires in 15 minutes but the key is permanent, so generate fresh URLs when displaying files.
- Configure bucket CORS rules before testing in a browser-based web build — mobile native builds skip CORS but browser builds enforce it strictly.
- Create scoped application keys restricted to a single bucket rather than using the master key — minimizes exposure if credentials are ever compromised.
- For existing apps that already use the AWS S3 presigned URL pattern, migrating to B2 S3 requires only changing the endpoint URL and credentials in the proxy — FlutterFlow API Calls need no changes.

## Use cases

### Photo-sharing app with B2 as the image store

A FlutterFlow community or social app where users upload photos to B2 via presigned PUT URLs. Public photos are served directly from B2's CDN-friendly URLs; private photos use presigned GET URLs. The no-minimum pricing of B2 means a small app pays only for what it actually stores and downloads, unlike Wasabi's 1 TB minimum.

Prompt example:

```
Build a photo upload page where users select an image from their camera roll, see a progress indicator while it uploads to Backblaze B2, and then see the photo displayed in a grid using the B2 public object URL.
```

### Audio recording app that stores clips in B2

A FlutterFlow app for voice memo or audio note-taking where recordings are uploaded to B2 after capture. The app uses presigned PUT URLs for upload and presigned GET URLs for playback. B2's per-GB egress pricing (with a generous free daily allowance based on stored volume) keeps costs low for apps with moderate playback volume.

Prompt example:

```
Create a voice memo recorder where users tap to record, see the recording in a list after stopping, and can play back each memo — audio files stored in Backblaze B2, fetched via presigned GET URLs.
```

### S3-to-B2 migration for an existing FlutterFlow app

An existing FlutterFlow app already uses a backend presign proxy for AWS S3 uploads. Migrating to B2 S3 requires only changing the endpoint URL and credentials in the proxy function — the FlutterFlow API Calls and action flows need no changes at all. The result is cheaper storage with the same developer experience.

Prompt example:

```
I have a FlutterFlow app with an S3 presign proxy. Help me update the proxy to use Backblaze B2 instead of AWS S3, keeping all FlutterFlow API Calls the same.
```

## Troubleshooting

### PUT to presigned URL returns 403 with SignatureDoesNotMatch

Cause: The endpoint URL used when generating the presigned URL does not match the bucket's actual S3 region endpoint, or an Authorization header was added to the PUT request alongside the presigned URL.

Solution: Verify the endpoint in your proxy function exactly matches the endpoint shown in the Backblaze console for your bucket (for example s3.us-west-004.backblazeb2.com). Remove any Authorization or X-Amz-* headers from the direct B2 PUT API Call — the presigned URL is self-authorizing and any extra headers alter the signed payload.

### Web upload fails with XMLHttpRequest error referencing CORS

Cause: The B2 bucket does not have a CORS policy allowing PUT from the browser's origin, or the CORS policy was saved but has not yet propagated.

Solution: In the Backblaze console, open your bucket → Bucket Settings → CORS Rules. Add AllowedOrigins: ["*"] (for development), AllowedMethods: ["PUT", "GET"], AllowedHeaders: ["*"]. Save and wait 2-3 minutes. Re-test the upload in a web build.

### Presigned URL expires before the user finishes the upload

Cause: The presigned URL was generated too early (for example on page load) and 15 minutes passed before the user tapped Upload.

Solution: Move the getPresignedUrl API Call to be the first action in the upload button action flow, immediately followed by the PUT action. Never pre-generate presigned URLs at page load and cache them — generate them on demand just before use.

### listObjects returns an empty objects array even though files were uploaded

Cause: The prefix filter in the listObjects call may not match the object keys used during upload, or the S3ForcePathStyle setting is missing and the SDK is using an incorrect URL format.

Solution: In the proxy's listObjectsV2 call, confirm the Prefix parameter is either empty (list all) or correctly matches the path prefix used for uploaded keys. In the S3 client constructor, verify s3ForcePathStyle: true is set — this is required for B2's S3-compatible endpoint.

## Frequently asked questions

### How is this different from using the native Backblaze B2 API?

This page uses B2's S3-compatible endpoint, where you use standard AWS SDK tooling and presigned URLs. The backblaze-b2-cloud-storage page uses B2's native API (b2_authorize_account, b2_get_upload_url), which has a dynamic apiUrl, single-use upload URLs, and access to B2-exclusive features like download authorization tokens. For most apps, the S3 layer is simpler. Use the native API only if you specifically need download authorization tokens or B2's large-file part API.

### Can I migrate an existing AWS S3 integration in FlutterFlow to Backblaze B2 without changing the app?

Yes — that is one of the main advantages of the S3-compatible layer. If your FlutterFlow app already uses a proxy that generates presigned S3 URLs, you only need to update the proxy function to change the S3 client endpoint from the AWS URL to your B2 endpoint (s3.{region}.backblazeb2.com) and replace the AWS credentials with your B2 keyID and applicationKey. The FlutterFlow API Groups and action flows need no changes.

### Does Backblaze B2 have a free tier?

Yes — 10 GB of free storage per account with no time limit, and the first 3× your total stored data per day is free for downloads. After the free storage, the rate is approximately $6/TB/month. Unlike Wasabi, Backblaze B2 has no minimum billing amount, so small apps with less than 10 GB pay nothing. Verify current rates at backblaze.com/b2/cloud-storage-pricing.html.

### Can I make uploaded files publicly accessible without generating presigned URLs each time?

Yes. If you created a Public bucket in the Backblaze console, uploaded objects are accessible at https://s3.{region}.backblazeb2.com/{bucket}/{key} or via the Backblaze CDN URL without any authorization. For Image widgets in FlutterFlow, bind the URL property to this constructed URL and no presigned URL generation is needed. Use a Private bucket and presigned GET URLs only for content that should not be freely accessible.

### What is the maximum file size for a single presigned PUT upload?

B2's S3-compatible layer supports single-part uploads up to 5 GB via a presigned PUT URL. Files larger than 5 GB require S3-compatible multipart upload (createMultipartUpload → uploadPart × N → completeMultipartUpload). For typical mobile use cases — photos, audio clips, short videos, PDFs — 5 GB is more than sufficient for a single-part upload.

---

Source: https://www.rapidevelopers.com/flutterflow-integrations/backblaze-b2
© RapidDev — https://www.rapidevelopers.com/flutterflow-integrations/backblaze-b2
