# Backblaze B2 Cloud Storage

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

## TL;DR

Connect FlutterFlow to Backblaze B2 using the native B2 API — a two-step auth flow where b2_authorize_account returns a dynamic apiUrl and auth token, followed by b2_get_upload_url for each upload. Because the application key and the dynamic apiUrl cannot safely live in a compiled Flutter app, a Firebase Cloud Function or Supabase Edge Function handles the authorize step and caches the token. FlutterFlow calls the proxy to get upload URLs, then PUTs file bytes directly.

## Backblaze B2 native API in FlutterFlow: two-step auth, dynamic URLs, and exclusive B2 features

This page covers the native Backblaze B2 API — not the S3-compatible endpoint covered on the backblaze-b2 page. Choose the native API when you need features the S3 layer does not expose: download authorization tokens (per-file, time-limited access for private buckets), the large-file chunked upload API, and B2-specific lifecycle rules. For founders who just want cheap S3-compatible storage, the backblaze-b2 page is simpler.

The defining characteristic of the native B2 API is its dynamic base URL. Calling b2_authorize_account (Basic auth with your keyID:applicationKey) returns an authorizationToken AND an apiUrl that is specific to your account and changes with each token. Every subsequent call must use that apiUrl as the base — you cannot hardcode it in a FlutterFlow API Group. Upload URLs are even more dynamic: b2_get_upload_url returns a one-time URL and a per-upload auth token that is single-use. Reusing an upload URL causes an error.

Backblaze B2 pricing: 10 GB free, then approximately $6/TB/month for storage. First 3× the daily stored capacity is free for downloads, then approximately $0.01/GB egress. Verify current pricing at backblaze.com/b2/cloud-storage-pricing.html before sharing with users.

## Before you start

- A Backblaze account (free at backblaze.com — 10 GB free storage, no credit card required)
- A B2 bucket created in the Backblaze console (choose private for files that need download authorization)
- A B2 application key created with read/write permissions for the bucket
- 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

### 1. Create a B2 bucket and application key in the Backblaze console

Log in to the Backblaze console at secure.backblaze.com. In the left sidebar, click B2 Cloud Storage → Buckets. Click Create a Bucket. Give the bucket a name (lowercase letters, numbers, and hyphens only). For Files in Bucket, choose Private — this is important if you want to use download authorization tokens. Leave the default encryption settings unless your compliance requirements need SSE-B2. Click Create a Bucket.

Next, click App Keys in the left sidebar. Click Add a New Application Key. Give it a descriptive name. Under Allow access to Bucket(s), select the specific bucket you just created rather than all buckets — this limits the blast radius if the key is ever compromised. Under Type of Access, select Read and Write. Optionally set an expiration date. Click Create New Key.

Copy both the keyID and the applicationKey that are displayed — the applicationKey is shown only once. These two values go into your backend function's environment variables only. The keyID is analogous to an AWS access key ID; the applicationKey is analogous to the secret access key. You will use them together as HTTP Basic Auth credentials in the b2_authorize_account call.

**Expected result:** You have a private B2 bucket and a scoped application key (keyID + applicationKey) copied and stored securely for use in your backend function.

### 2. Deploy a proxy function that handles b2_authorize_account and provides stable upload endpoints

The core of this integration is a backend proxy that handles the dynamic nature of the B2 auth flow. Deploy a Firebase Cloud Function or Supabase Edge Function with the following logic:

The function calls b2_authorize_account at https://api.backblazeb2.com/b2api/v2/b2_authorize_account using HTTP Basic Auth (keyID:applicationKey Base64-encoded in the Authorization header). The response contains: authorizationToken (a Bearer token for subsequent calls), apiUrl (the account-specific base URL for all B2 API calls), and downloadUrl (the base URL for file downloads). Cache these three values in the function's memory or in Firestore — the token is valid for 24 hours.

Next, expose a /getUploadUrl endpoint that: calls b2_get_upload_url at {apiUrl}/b2api/v2/b2_get_upload_url with the Authorization header set to {authorizationToken} and the bucketId in the request body. This returns a one-time uploadUrl and a per-upload authorizationToken. Return both to FlutterFlow.

Also expose a /getDownloadAuth endpoint that calls b2_get_download_authorization with the bucket name, file name prefix, and validDurationInSeconds, returning a downloadAuthorizationToken. FlutterFlow uses this token as a query parameter to construct private download URLs.

Finally, expose a /listFiles endpoint that calls b2_list_file_names and returns a clean JSON array of file objects.

```
// Firebase Cloud Function — B2 native API proxy
// functions/index.js (Node.js)
const functions = require('firebase-functions');
const axios = require('axios');

const KEY_ID = functions.config().b2.key_id;
const APP_KEY = functions.config().b2.app_key;
const BUCKET_ID = functions.config().b2.bucket_id;
const BUCKET_NAME = functions.config().b2.bucket_name;

// Cache auth result (valid 24h)
let cachedAuth = null;

async function getAuth() {
  if (cachedAuth && Date.now() < cachedAuth.expiresAt) return cachedAuth;
  const credentials = Buffer.from(`${KEY_ID}:${APP_KEY}`).toString('base64');
  const res = await axios.get(
    'https://api.backblazeb2.com/b2api/v2/b2_authorize_account',
    { headers: { Authorization: `Basic ${credentials}` } }
  );
  cachedAuth = {
    token: res.data.authorizationToken,
    apiUrl: res.data.apiUrl,
    downloadUrl: res.data.downloadUrl,
    expiresAt: Date.now() + 23 * 60 * 60 * 1000, // 23h to be safe
  };
  return cachedAuth;
}

exports.b2GetUploadUrl = functions.https.onRequest(async (req, res) => {
  res.set('Access-Control-Allow-Origin', '*');
  if (req.method === 'OPTIONS') return res.status(204).send('');
  const auth = await getAuth();
  const upload = await axios.post(
    `${auth.apiUrl}/b2api/v2/b2_get_upload_url`,
    { bucketId: BUCKET_ID },
    { headers: { Authorization: auth.token } }
  );
  res.json({
    upload_url: upload.data.uploadUrl,
    upload_auth_token: upload.data.authorizationToken,
  });
});

exports.b2GetDownloadAuth = functions.https.onRequest(async (req, res) => {
  res.set('Access-Control-Allow-Origin', '*');
  if (req.method === 'OPTIONS') return res.status(204).send('');
  const { fileName, validSeconds } = req.body;
  const auth = await getAuth();
  const dlAuth = await axios.post(
    `${auth.apiUrl}/b2api/v2/b2_get_download_authorization`,
    { bucketName: BUCKET_NAME, fileNamePrefix: fileName, validDurationInSeconds: validSeconds || 900 },
    { headers: { Authorization: auth.token } }
  );
  const downloadToken = dlAuth.data.authorizationToken;
  const fileUrl = `${auth.downloadUrl}/file/${BUCKET_NAME}/${fileName}?Authorization=${downloadToken}`;
  res.json({ download_url: fileUrl });
});

exports.b2ListFiles = functions.https.onRequest(async (req, res) => {
  res.set('Access-Control-Allow-Origin', '*');
  if (req.method === 'OPTIONS') return res.status(204).send('');
  const auth = await getAuth();
  const list = await axios.post(
    `${auth.apiUrl}/b2api/v2/b2_list_file_names`,
    { bucketId: BUCKET_ID, maxFileCount: 100 },
    { headers: { Authorization: auth.token } }
  );
  res.json({ files: list.data.files.map(f => ({ name: f.fileName, size: f.contentLength, id: f.fileId })) });
});
```

**Expected result:** Three deployed Cloud Function URLs — b2GetUploadUrl, b2GetDownloadAuth, and b2ListFiles — that return usable B2 upload/download data to FlutterFlow without exposing B2 credentials.

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

In FlutterFlow, create two API Groups. The first talks to your proxy to get upload URLs and download tokens. The second performs the actual file upload directly to B2's one-time upload URL.

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 three API Calls:
- getUploadUrl: method GET, endpoint /b2GetUploadUrl. In the Response & Test tab, add JSON paths $.upload_url and $.upload_auth_token.
- getDownloadAuth: method POST, endpoint /b2GetDownloadAuth. Body: {"fileName":"{{fileName}}","validSeconds":900}. Add fileName variable (String). JSON path: $.download_url.
- listFiles: method GET, endpoint /b2ListFiles. JSON paths: $.files[*].name, $.files[*].size, $.files[*].id.

API Group 2 — name it B2Upload. This group has a variable base URL because B2's upload endpoint is returned dynamically. In practice, you will use the full upload_url returned by getUploadUrl as the endpoint for a single API Call named uploadFile. Set method to POST. Add these headers as variables (not static): Authorization bound to the upload_auth_token variable, Content-Type bound to the file MIME type, X-Bz-File-Name bound to the filename (URL-encoded), X-Bz-Content-Sha1 bound to the file content hash or the literal string do_not_verify_sha1 for simplicity during development. Body type: Binary.

```
// B2Proxy API Group — reference config
{
  "base_url": "https://us-central1-your-project.cloudfunctions.net",
  "calls": [
    {
      "name": "getUploadUrl",
      "method": "GET",
      "endpoint": "/b2GetUploadUrl",
      "response_paths": ["$.upload_url", "$.upload_auth_token"]
    },
    {
      "name": "getDownloadAuth",
      "method": "POST",
      "endpoint": "/b2GetDownloadAuth",
      "body": { "fileName": "{{fileName}}", "validSeconds": 900 },
      "response_paths": ["$.download_url"]
    },
    {
      "name": "listFiles",
      "method": "GET",
      "endpoint": "/b2ListFiles",
      "response_paths": ["$.files[*].name", "$.files[*].size", "$.files[*].id"]
    }
  ]
}

// B2Upload API Call — headers from variables
{
  "name": "uploadFile",
  "method": "POST",
  "endpoint": "{{uploadUrl}}",
  "headers": {
    "Authorization": "{{uploadAuthToken}}",
    "Content-Type": "{{contentType}}",
    "X-Bz-File-Name": "{{fileName}}",
    "X-Bz-Content-Sha1": "do_not_verify_sha1"
  },
  "body_type": "binary"
}
```

**Expected result:** Two API Groups appear in FlutterFlow: B2Proxy for proxy operations and B2Upload for direct file uploads to B2's one-time upload URL.

### 4. Wire the upload action flow and build the file list with download tokens

On your upload page, wire the following action sequence in the Action Flow Editor for an Upload button:

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

Step 2: API Call action → B2Proxy → getUploadUrl — this returns a one-time uploadUrl and an uploadAuthToken. Store both in Page State variables (uploadUrl, uploadAuthToken). The upload URL is single-use — request it immediately before the upload, not in advance.

Step 3: API Call action → B2Upload → uploadFile — set the endpoint variable uploadUrl to the Page State variable uploadUrl, uploadAuthToken to the stored token, contentType to the appropriate MIME type (image/jpeg, application/pdf, etc.), fileName to the Page State fileName (URL-encode any spaces as %20), and the binary body to fileBytes.

Step 4: On success (response code 200), add a Snackbar action (File uploaded successfully) and trigger a backend query refresh on your file list.

For the file list, add a ListView with a Backend Query → B2Proxy → listFiles. Display $.files[*].name as the file name. For each file's View button, add an API Call action → B2Proxy → getDownloadAuth binding fileName to the list item's $.files[*].name. After the call, add a Launch URL action bound to the returned $.download_url to open the file in the device browser, or load it into a WebView widget.

**Expected result:** Users can upload files to B2 via the two-step flow (get upload URL → PUT bytes). The file list shows B2 objects, and tapping View generates a private download token and opens the file.

### 5. Test on device and handle B2 error responses

Start testing in FlutterFlow's Run mode with a small image file (under 100 KB) to verify the proxy calls work. Because the upload goes to a dynamic URL returned by the proxy, you should see two API Call actions in sequence in the action flow — the first returning the uploadUrl and the second consuming it.

Check common failure points: open your Firebase console → Functions → Logs while running a test upload. If b2_authorize_account fails, check that the keyID and applicationKey environment variables are set correctly. If the upload returns a 401, confirm the uploadAuthToken is being passed in the Authorization header and that you are not trying to reuse a single-use upload URL for a second file.

For private bucket downloads, test the getDownloadAuth flow with a file you know exists. The returned download_url includes the Authorization token as a query parameter — this is how B2 authorizes private file access without requiring the app to send a Bearer header (which would mean putting credentials in the app).

For large files (over 100 MB), the single-call upload path may time out on slow connections. The native B2 API's large-file API (b2_start_large_file → b2_get_upload_part_url → b2_upload_part × N → b2_finish_large_file) handles this. Add a proxy endpoint for each step and implement the chunked upload in a Custom Action in Dart.

If the two-step auth proxy and dynamic URL wiring feels complex for your current stage, RapidDev's team builds FlutterFlow integrations like this regularly — get a free scoping call at rapidevelopers.com/contact.

**Expected result:** Small file uploads succeed end-to-end through the proxy. Private file downloads work with the generated token. Large files are flagged for the chunked upload path.

## Best practices

- Never store the B2 application key in FlutterFlow or Dart code — keep it exclusively in your backend function's environment variables.
- Cache the b2_authorize_account result in your proxy for up to 23 hours — the token is valid for 24 hours, and re-authorizing on every request wastes calls and risks rate limits.
- Always request a fresh upload URL immediately before each file upload — B2 upload URLs are single-use and cannot be shared across multiple upload calls.
- Use X-Bz-Content-Sha1: do_not_verify_sha1 during development to skip checksum computation; compute and send the real SHA1 in production to detect corruption during transfer.
- Generate download authorization tokens server-side with short validity periods (15 minutes) rather than making the bucket public — this prevents URL sharing from being permanent.
- Scope application keys to a single bucket rather than granting access to all buckets — limits exposure if the key is ever compromised.
- Use the native B2 API (this page) instead of the S3 layer specifically when you need download authorization tokens or the large-file part upload API — otherwise the S3 layer (backblaze-b2) is simpler.

## Use cases

### Private document vault app with per-download authorization

A FlutterFlow app where users store sensitive documents (contracts, tax returns, medical records) in a private B2 bucket. The native B2 API's b2_get_download_authorization endpoint generates time-limited download tokens per file — so even if a download URL is shared, it expires and cannot be reused. The proxy generates fresh download tokens before each viewing session.

Prompt example:

```
Build a secure document vault where users see a list of their private documents and can tap to view a document in a WebView using a time-limited B2 download token, with the token expiring after 15 minutes.
```

### Video upload app with large-file part uploads

A FlutterFlow app where creators upload long video files (over 100 MB) to B2. The native B2 API's b2_start_large_file, b2_get_upload_part_url, and b2_finish_large_file endpoints handle chunked uploads reliably. The proxy manages the large-file session and provides part upload URLs to FlutterFlow, which uploads each chunk sequentially.

Prompt example:

```
Create a video upload screen where users select a video from their camera roll, see an upload progress bar as the video is chunked and sent to Backblaze B2, and receive a confirmation when processing is complete.
```

### App backup service using B2 lifecycle rules

A FlutterFlow app for small businesses to back up local data files to B2 with automatic cleanup. B2 native lifecycle rules delete old versions after a set number of days — a feature not exposed through the S3 layer. The proxy configures lifecycle rules at setup time and provides upload URLs for the backup jobs that run from the app.

Prompt example:

```
Build a backup status dashboard that shows last backup time, total storage used in B2, and allows users to trigger a manual backup of their local data files to Backblaze B2.
```

## Troubleshooting

### b2GetUploadUrl returns 401 Unauthorized from the B2 API

Cause: The b2_authorize_account call failed because the keyID or applicationKey in the function environment variables is incorrect, or the application key expired.

Solution: In your Firebase console, check Functions → Logs for the full B2 error message. Verify the b2.key_id and b2.app_key config values match exactly what is shown in the Backblaze console. If the key has expired, create a new application key and update the function config.

### Upload returns 400 Bad Request with code bad_bucket_id or upload_url_is_wrong

Cause: The upload URL returned by b2_get_upload_url was reused for a second upload. B2 upload URLs are single-use — each individual upload requires its own fresh upload URL.

Solution: Ensure your action flow calls getUploadUrl immediately before every file upload, not once on page load. Move the getUploadUrl API Call action to be the first action in the upload button sequence, immediately followed by the uploadFile action using the freshly returned URL.

### Private file download returns 401 or unauthorized with the download URL

Cause: The download authorization token expired (default validity is 15 minutes), or the fileNamePrefix used to generate the token does not match the requested file path exactly.

Solution: Generate download tokens close to when the user will use them — not in advance. In the getDownloadAuth call, set the fileNamePrefix to the exact file name (or a prefix that covers all files in a folder). If the token expired, call getDownloadAuth again to get a fresh URL.

### b2_list_file_names returns an empty files array for a bucket that has files

Cause: The bucket ID in the proxy function is incorrect, or the application key does not have read access to that bucket.

Solution: Open the Backblaze console, navigate to the bucket, and copy the exact Bucket ID (not the bucket name — they are different). Verify in the Firebase function config that b2.bucket_id matches this ID. Also confirm the application key was created with access to this specific bucket.

## Frequently asked questions

### What is the difference between this page and the backblaze-b2 page?

The backblaze-b2 page covers the B2 S3-compatible API layer, where you use standard AWS S3 tools and presigned URLs. This page covers the native B2 API, which has its own endpoint format (b2_authorize_account, b2_get_upload_url, b2_get_download_authorization) and gives access to features the S3 layer does not expose, such as per-file download authorization tokens and the large-file part upload API. Both pages write to the same Backblaze B2 storage — the API surface is different.

### Why does the B2 apiUrl change with each authorization? Can I hardcode it?

The apiUrl is account-specific and determined by Backblaze's infrastructure routing. It is stable for a given account but is not guaranteed to remain the same forever, and it varies between accounts. You should not hardcode it — always use the apiUrl returned by b2_authorize_account and cache it for the token's lifetime (up to 24 hours). The proxy pattern in this tutorial handles this automatically.

### Can I use B2 download authorization tokens to serve private video streams in FlutterFlow?

Yes. A B2 download authorization token generates a URL that includes the authorization as a query parameter — for example https://f001.backblazeb2.com/file/mybucket/video.mp4?Authorization=TOKEN. Pass this URL to a Video Player widget in FlutterFlow to stream the private video. The token expires after the validDurationInSeconds you specified, after which the user would need a fresh URL.

### What happens if my backend function goes down? Will FlutterFlow be able to upload files?

No — the proxy is required for the native B2 API because FlutterFlow cannot call b2_authorize_account or b2_get_upload_url without exposing the application key. If the proxy goes down, uploads and downloads of private files will fail. Use Firebase's scaling and redundancy features, or set up health monitoring on your function endpoints.

### Is the native B2 API faster or slower than the S3 layer for uploads?

Performance should be similar — both API surfaces ultimately write to the same Backblaze storage infrastructure. The native API adds one extra round trip (b2_get_upload_url) before the actual upload, but this is a small overhead compared to network transit time for the file bytes themselves. Choose between native and S3 based on feature requirements, not performance.

---

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