Connect Bubble to Backblaze B2 using the S3-compatible API layer — the simplest path. A presign proxy (Cloudflare Worker or Supabase Edge Function) holds your B2 credentials and returns short-lived PUT URLs. Your Bubble app never handles raw credentials. The key gotcha: set `s3ForcePathStyle: true` in the AWS SDK and use the exact regional endpoint from your bucket details page.
| Fact | Value |
|---|---|
| Tool | Backblaze B2 |
| Category | Storage & Files |
| Method | Bubble API Connector |
| Difficulty | Intermediate |
| Time required | 2–3 hours |
| Last updated | July 2026 |
Two paths to Backblaze B2 — choose S3-compatible for Bubble
Backblaze B2 supports two distinct API surfaces: its native B2 API and an S3-compatible layer at `s3.{region}.backblazeb2.com`. For Bubble developers, the S3-compatible layer is strongly recommended. The native B2 API requires handling a dynamic `apiUrl` returned per authentication session — a URL that changes and cannot be hardcoded in Bubble's API Connector. The S3-compatible layer works exactly like Amazon S3 and Wasabi: point the AWS SDK at the B2 endpoint, generate presigned PUT/GET URLs in a lightweight proxy function, and your Bubble app calls that proxy to fetch URLs before uploading directly from the browser.
The result is clean architecture: B2 credentials live only in the proxy function, Bubble workflows fetch short-lived presigned URLs via the API Connector (with any proxy auth marked 'Private'), and file data flows browser → B2 without touching Bubble's servers. Workload Units are consumed only by the Backend Workflow that calls the proxy — not by the file transfer itself.
Backblaze B2's pricing makes it the entry-level winner among S3-compatible alternatives: 10 GB always free, then approximately $6/TB/month for storage, with egress free up to three times the daily stored data amount. Unlike Wasabi, B2 has no per-month minimum charge — you pay only for what you use.
Integration method
Bubble's API Connector calls a presign proxy function that holds your B2 credentials server-side and returns short-lived presigned PUT/GET URLs. File bytes travel directly from the browser to Backblaze B2 — never through Bubble.
Prerequisites
- A Backblaze account (backblaze.com) — free tier includes 10 GB storage
- A Bubble app on a paid plan (Starter or above) — Backend Workflows required for the presign call are not available on the Free plan
- A presign proxy function deployed somewhere: Cloudflare Worker, Supabase Edge Function, or AWS Lambda all work
- The Bubble API Connector plugin installed in your app (Plugins tab → Add plugins → search 'API Connector' by Bubble)
Step-by-step guide
Create a Backblaze B2 bucket and note your exact regional endpoint
Log in to the Backblaze console at secure.backblaze.com. Navigate to B2 Cloud Storage → Buckets → Create a Bucket. Give it a unique name and choose Private (recommended — public buckets allow unauthenticated downloads of any file). After the bucket is created, click on its name to open bucket details. You will see a field labeled 'Endpoint' — this is your S3-compatible endpoint URL. It looks like `s3.us-west-004.backblazeb2.com` and includes a region number (004, 002, etc.). Copy this exact URL — you will need it in your presign proxy. Do not guess or construct this URL manually; the region number format is specific to each bucket and mismatches cause persistent `SignatureDoesNotMatch` errors that look like credential problems but are actually endpoint problems. Also decide on your CORS policy now. For browser-based uploads, Backblaze requires CORS to be configured on the bucket. In the bucket details page, click 'CORS Rules'. Add a rule with: AllowedOrigins set to your Bubble app domain (e.g., `https://myapp.bubbleapps.io`), AllowedOperations including `b2_upload` and `b2_download_file`, and set AllowedHeaders to `*`. Save the CORS rule before attempting any browser-side uploads.
1{2 "bucket_name": "my-bubble-app-uploads",3 "bucket_type": "allPrivate",4 "s3_compatible_endpoint": "s3.us-west-004.backblazeb2.com",5 "cors_rule": {6 "corsRuleName": "allowBubbleUploads",7 "allowedOrigins": ["https://yourapp.bubbleapps.io"],8 "allowedHeaders": ["*"],9 "allowedOperations": ["b2_upload", "b2_download_file"],10 "maxAgeSeconds": 360011 }12}Pro tip: If your Bubble app uses a custom domain (e.g., myapp.com), add that domain to AllowedOrigins too. You can list multiple origins in the CORS rule.
Expected result: Your B2 bucket is created with private access. You have the exact S3-compatible endpoint URL (including region number) copied somewhere safe, and CORS rules are saved on the bucket.
Create a Backblaze B2 application key
In the Backblaze console, go to Account → App Keys → Add a New Application Key. Give the key a descriptive name (e.g., 'bubble-app-presign'). Under 'Allow access to Bucket', select the specific bucket you just created rather than 'All Buckets' — this follows the principle of least privilege and limits the blast radius if the key is ever compromised. Enable read and write permissions. Click 'Create New Key'. Backblaze will display your `keyID` and `applicationKey` exactly once. Copy both values immediately — the `applicationKey` (equivalent to AWS Secret Access Key) is never shown again and cannot be recovered. If you lose it, you must create a new key. In the context of the AWS SDK: your `keyID` maps to `accessKeyId` and your `applicationKey` maps to `secretAccessKey`. These two values will live only in your presign proxy function's environment variables — they should never appear in Bubble's API Connector configuration or any client-facing code.
1# Environment variables for your presign proxy2B2_KEY_ID=your_keyID_here3B2_APPLICATION_KEY=your_applicationKey_here4B2_BUCKET_NAME=my-bubble-app-uploads5B2_ENDPOINT=https://s3.us-west-004.backblazeb2.com6B2_REGION=us-west-004Pro tip: The region value for the AWS SDK should match the region in your endpoint URL. For `s3.us-west-004.backblazeb2.com`, use `us-west-004` as the region.
Expected result: You have a scoped application key with both keyID and applicationKey saved securely. These credentials are ready to be added to your presign proxy's environment variables.
Deploy a presign proxy function with s3ForcePathStyle enabled
Your presign proxy is a small server-side function that uses the AWS SDK pointed at Backblaze B2 instead of Amazon S3. The single most important configuration difference: you MUST set `s3ForcePathStyle: true` (or `forcePathStyle: true` in AWS SDK v3) when creating the S3 client. Without this, the AWS SDK constructs the request URL as `bucket.s3.endpoint.com/key` (virtual-hosted style), which Backblaze B2 does not recognize. With path style, the URL becomes `s3.endpoint.com/bucket/key`, which B2 supports. Deploy this as a Cloudflare Worker, Supabase Edge Function, or AWS Lambda. The function should accept a POST request with a `filename` and `contentType` in the body, and return a presigned PUT URL that is valid for a short time window (15–60 minutes is typical). Set your B2 credentials as environment variables in the hosting platform's dashboard — never hardcode them in the function source. Once deployed, note the function's public URL (e.g., `https://my-worker.myname.workers.dev/presign`). This URL — not the B2 endpoint — is what you will configure in Bubble's API Connector.
1// Cloudflare Worker — presign proxy for Backblaze B22import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';3import { getSignedUrl } from '@aws-sdk/s3-request-presigner';45export default {6 async fetch(request, env) {7 if (request.method !== 'POST') {8 return new Response('Method not allowed', { status: 405 });9 }1011 const { filename, contentType } = await request.json();1213 const s3 = new S3Client({14 region: env.B2_REGION, // e.g., 'us-west-004'15 endpoint: env.B2_ENDPOINT, // e.g., 'https://s3.us-west-004.backblazeb2.com'16 forcePathStyle: true, // REQUIRED for Backblaze B217 credentials: {18 accessKeyId: env.B2_KEY_ID,19 secretAccessKey: env.B2_APPLICATION_KEY,20 },21 });2223 const key = `uploads/${Date.now()}-${filename}`;2425 const command = new PutObjectCommand({26 Bucket: env.B2_BUCKET_NAME,27 Key: key,28 ContentType: contentType,29 });3031 const presignedUrl = await getSignedUrl(s3, command, { expiresIn: 900 });3233 return new Response(34 JSON.stringify({ presignedUrl, objectKey: key }),35 { headers: { 'Content-Type': 'application/json' } }36 );37 },38};Pro tip: Test your proxy before connecting Bubble: send a POST request with a tool like Postman or curl to verify it returns a valid presignedUrl. Then try a PUT to that URL with a small test file — if you get a 200, CORS and credentials are working correctly.
Expected result: Your presign proxy is deployed and returning valid presigned PUT URLs when called with a filename and contentType. You have the proxy's public URL ready.
Add the API Connector in Bubble and configure the presign call
In your Bubble app editor, go to the Plugins tab in the left sidebar. Click 'Add plugins', search for 'API Connector' (the official plugin by Bubble), and install it. Once installed, click on API Connector to open its configuration panel. Click 'Add another API'. Name it 'B2 Presign Proxy'. In the Authentication field, you can leave it as 'None' if your proxy is publicly accessible (relying on the obscurity of the URL) or add a custom header if your proxy requires an auth token. If you add an auth header (e.g., `X-Proxy-Key`), check the 'Private' checkbox next to it — this ensures the header value is never sent to the browser. Click 'Add a new call' inside the B2 Presign Proxy API. Configure it as follows: - Name: Get Presigned Upload URL - Method: POST - URL: your proxy URL (e.g., `https://my-worker.workers.dev/presign`) - Body Type: JSON - Body parameters: `filename` (type: text, dynamic value: `<filename>`) and `contentType` (type: text, dynamic value: `<contentType>`) Click 'Initialize call'. Bubble will make an actual request to your proxy — provide real values: filename = `test.jpg`, contentType = `image/jpeg`. Bubble will parse the JSON response and detect the `presignedUrl` and `objectKey` fields automatically. These become bindable values you can use in workflows.
1{2 "api_name": "B2 Presign Proxy",3 "call_name": "Get Presigned Upload URL",4 "method": "POST",5 "url": "https://my-worker.myname.workers.dev/presign",6 "headers": {7 "Content-Type": "application/json"8 },9 "body": {10 "filename": "<dynamic: filename from workflow>",11 "contentType": "<dynamic: file mime type>"12 },13 "response_fields_auto_detected": [14 "presignedUrl",15 "objectKey"16 ]17}Pro tip: If the Initialize call returns 'There was an issue setting up your call', check that your proxy URL is correct and publicly reachable. Try opening the proxy URL in a browser first to confirm it responds (even if it says 'Method not allowed' for GET requests — that confirms it's live).
Expected result: The API Connector shows the B2 Presign Proxy API with a 'Get Presigned Upload URL' call. After initialization, Bubble has detected `presignedUrl` and `objectKey` in the response and you can see them in the response fields list.
Build a Backend Workflow to fetch the presigned URL and trigger the upload
Now wire everything together in Bubble's workflow editor. You will need a two-part flow: a Backend Workflow that fetches the presigned URL from your proxy (this runs server-side), and a client-side action that executes the actual browser-to-B2 upload using the presigned URL. First, create the upload trigger. Add a Button element to your page labeled 'Upload File'. In its workflow, add a step: Plugins → API Connector → GET PRESIGNED UPLOAD URL. Set the `filename` to the file element's filename and `contentType` to the file element's MIME type. This call returns `presignedUrl` and `objectKey` from your proxy. Next, add a second workflow step: 'Run JavaScript'. Use the Toolbox plugin (free, by Bubble) if it isn't installed — it provides the 'Run JavaScript' action. Write JavaScript that reads the file from a Bubble file uploader element and PUTs it to the presigned URL. Pass the presigned URL as an input parameter from the previous step's result. Finally, on JavaScript completion success, add a 'Make changes to a Thing' step that saves the `objectKey` to the relevant database record. Store ONLY the object key (e.g., `uploads/1710000000-document.pdf`), not the full presigned URL. Presigned URLs expire; object keys do not. When you need to display the file, call your proxy again to get a fresh presigned GET URL.
1// Bubble 'Run JavaScript' action — upload file to B2 presigned URL2// Input parameters: presignedUrl (text), objectKey (text)3// Access via: bubble_fn_param1, or pass as named inputs in Toolbox45(async function() {6 const presignedUrl = properties.presignedUrl; // from API Connector result7 const fileElement = document.querySelector('#fileUploader input[type="file"]');8 const file = fileElement.files[0];910 if (!file) {11 bubble_fn_result(false);12 return;13 }1415 try {16 const response = await fetch(presignedUrl, {17 method: 'PUT',18 body: file,19 headers: {20 'Content-Type': file.type,21 },22 });2324 if (response.ok) {25 bubble_fn_result(true); // triggers 'result is returned' event26 } else {27 console.error('Upload failed:', response.status, await response.text());28 bubble_fn_result(false);29 }30 } catch (err) {31 console.error('Upload error:', err);32 bubble_fn_result(false);33 }34})();Pro tip: Do NOT add an Authorization header to the fetch PUT request. The signature is already embedded in the presigned URL's query parameters. Adding any extra headers changes the signed payload and causes a 403 SignatureDoesNotMatch error from Backblaze.
Expected result: Clicking the upload button calls the proxy, gets a presigned URL, uploads the file directly to Backblaze B2 from the browser, and saves the object key to the Bubble database record. You can verify the file appeared in the Backblaze console bucket.
Retrieve files using fresh presigned GET URLs
Since you stored only the object key in your Bubble database (not a full URL), you need to generate a presigned GET URL whenever you want to display or link to a file. Add a second call to your B2 Presign Proxy API Connector for this. In the API Connector, inside the 'B2 Presign Proxy' API, click 'Add a new call'. Name it 'Get Presigned Download URL'. Configure it as a POST to your proxy at `/presign-get` (you will need to add this route to your proxy function as well). Send the `objectKey` in the request body. The proxy returns a presigned GET URL valid for a time window you control (e.g., 3600 seconds). In your Bubble pages, wherever you display uploaded files — Image elements, link elements, repeating groups — use a dynamic expression that calls this API call with the stored object key and binds the result's `presignedGetUrl` to the element's URL or link destination. For images you want to display in a repeating group, consider using a 'Get data from external API' source with the object key as the parameter. For files in a public bucket (less common, only if security isn't a concern), the direct download URL is simpler: `https://f{clusterNumber}.backblazeb2.com/file/{bucketName}/{objectKey}` — find the clusterNumber in your bucket details. No presigning needed for public objects. RapidDev's team has built dozens of Bubble file management workflows — if you need help designing the retrieval pattern for your specific use case, reach out at rapidevelopers.com/contact.
1// Add to your Cloudflare Worker — GET presigned URL route2if (request.url.includes('/presign-get')) {3 const { objectKey } = await request.json();45 const s3 = new S3Client({6 region: env.B2_REGION,7 endpoint: env.B2_ENDPOINT,8 forcePathStyle: true,9 credentials: {10 accessKeyId: env.B2_KEY_ID,11 secretAccessKey: env.B2_APPLICATION_KEY,12 },13 });1415 const command = new GetObjectCommand({16 Bucket: env.B2_BUCKET_NAME,17 Key: objectKey,18 });1920 const presignedGetUrl = await getSignedUrl(s3, command, { expiresIn: 3600 });2122 return new Response(23 JSON.stringify({ presignedGetUrl }),24 { headers: { 'Content-Type': 'application/json' } }25 );26}Pro tip: For repeating groups displaying many files, batch the presigned URL generation where possible. Each proxy call consumes a Bubble WU — if you have 50 files in a repeating group, that's 50 WU per page load. Consider whether public bucket access or Cloudflare CDN caching can reduce this.
Expected result: Files stored in Backblaze B2 can be displayed or downloaded in your Bubble app using fresh presigned GET URLs. Private files are not accessible without a valid time-limited URL.
Common use cases
User file uploads and document storage
Allow Bubble app users to upload files — PDFs, images, documents — that are stored durably in Backblaze B2 at a fraction of the cost of premium cloud storage. The presigned URL pattern means each user's upload goes directly to B2, with only the object key saved in the Bubble database for later retrieval.
Copy this prompt to try it in Bubble
Product image hosting and media library
Store product photos, portfolio images, or media assets in B2 and serve them directly from B2's download URL. Because B2 integrates with Cloudflare's CDN network (free egress between Backblaze and Cloudflare data centers), you can serve images globally with near-zero egress costs.
Copy this prompt to try it in Bubble
Backup and archival storage for generated content
Automatically archive generated reports, user-uploaded records, or exported data to Backblaze B2 via a Backend Workflow. Store the B2 object key in a Bubble data type alongside metadata, and generate a fresh presigned GET URL only when a user requests access — keeping older files out of your main database storage.
Copy this prompt to try it in Bubble
Troubleshooting
Upload fails with 403 SignatureDoesNotMatch even though credentials look correct
Cause: Almost always caused by either a wrong endpoint URL or missing s3ForcePathStyle. The B2 S3-compatible endpoint must match the exact bucket region (including region number like us-west-004). If the endpoint is wrong or the SDK constructs virtual-hosted URLs instead of path-style, the signature will not match.
Solution: 1. Copy the endpoint URL directly from the Backblaze console bucket details page — do not construct it manually. 2. Confirm your proxy includes `forcePathStyle: true` in the S3Client configuration. 3. Confirm the region string in the S3Client matches the region in the endpoint URL (e.g., `us-west-004`). 4. Re-deploy the proxy after changes and re-test.
CORS error in the browser console when uploading directly to Backblaze B2
Cause: Bucket CORS rules are either not set, not yet propagated, or your Bubble app domain is not in the AllowedOrigins list.
Solution: Go to Backblaze console → your bucket → CORS Rules. Add or update the rule to include your Bubble domain in AllowedOrigins. If you are on a custom domain, add both `https://yourapp.bubbleapps.io` and `https://yourcustomdomain.com`. CORS changes take a few minutes to propagate. Wait a few minutes and retry before further debugging.
Bubble API Connector shows 'There was an issue setting up your call' when initializing
Cause: The Initialize call in Bubble's API Connector makes a real HTTP request to your proxy. If the proxy URL is wrong, the proxy is not deployed yet, or the proxy requires a header that isn't configured, initialization fails.
Solution: 1. Confirm the proxy URL is correct and the function is deployed. 2. Open the URL in a browser (even a GET 'Method Not Allowed' response confirms the function is live). 3. If the proxy requires auth headers, ensure you've added them to the API Connector and the values are correct. 4. Re-run Initialize call with valid test values for filename and contentType.
PUT request to presigned URL returns 403 with error about extra headers
Cause: An Authorization header is being added to the PUT request in the JavaScript fetch call. Presigned URLs already embed the signature in query parameters — any extra headers alter the signed payload and invalidate the signature.
Solution: Remove any Authorization header from the fetch PUT request. The only header you should include is `Content-Type` matching the type used when generating the presigned URL. The PUT should have no auth headers.
1// WRONG — causes 4032await fetch(presignedUrl, {3 method: 'PUT',4 body: file,5 headers: { 'Authorization': 'Bearer ...', 'Content-Type': file.type }6});78// CORRECT9await fetch(presignedUrl, {10 method: 'PUT',11 body: file,12 headers: { 'Content-Type': file.type }13});Backend Workflow for fetching presigned URL is unavailable or throws 'Workflow API is not enabled'
Cause: Backend Workflows (API Workflows) are a paid Bubble feature. They are not available on the Free plan. The scheduled or server-side calls required for the presign proxy call pattern require at minimum the Starter plan.
Solution: Upgrade your Bubble app to Starter ($32/mo) or above to enable API Workflows and Backend Workflows. This is required for any server-side API call pattern in Bubble, not just Backblaze B2.
Best practices
- Store only the B2 object key in your Bubble database — never store presigned URLs. Presigned URLs expire (typically within 15–60 minutes). Generate a fresh presigned GET URL each time a user needs to access a file.
- Set `s3ForcePathStyle: true` in the AWS SDK S3Client when pointing at Backblaze B2. Without it, the SDK constructs virtual-hosted-style URLs that B2 does not support, causing signature errors.
- Use scoped application keys: create a key that grants access to only the specific bucket your app uses, with only the read and write permissions actually needed. If the key is compromised, the blast radius is contained.
- Configure bucket CORS before building your upload UI. Adding CORS after you've built the UI makes it harder to distinguish CORS errors from other upload failures. Set it up in the Backblaze console during initial configuration.
- Keep presign proxy logic minimal and stateless. The proxy should only sign requests and return URLs — it should not store files, log file contents, or add business logic. Keeping it thin makes it easier to secure and replace.
- Enable privacy rules on any Bubble data type that stores B2 object keys. If your app's Things are readable by users who shouldn't access certain files, they can call your proxy to generate presigned URLs for other users' object keys. Bubble's Data tab → Privacy rules block unauthorized access to the keys themselves.
- Consider Cloudflare's Bandwidth Alliance with Backblaze: if you deploy a Cloudflare Worker as your presign proxy and serve files through Cloudflare, egress from Backblaze to Cloudflare is free. This eliminates nearly all download costs for media-heavy applications.
Alternatives
Wasabi is also S3-compatible with the same presigned URL proxy pattern, but charges a 1 TB per month minimum ($6.99/month minimum spend even if you store only 1 GB). Backblaze B2 has no minimum — you pay only for what you use, making B2 better for small apps. Wasabi has no egress fees on all plans; B2 offers free egress up to 3× daily stored data. For apps storing more than 1 TB, compare both pricing pages carefully.
Amazon S3 uses the same presigned URL proxy pattern (same AWS SDK, same architecture) but at higher prices (~$0.023/GB/month vs B2's ~$6/TB). S3 benefits: native AWS ecosystem integration (CloudFront CDN, Lambda triggers, IAM), the largest set of regions, and the widest tool compatibility. Choose S3 when you need tight AWS service integration; choose B2 for pure object storage at lowest cost.
Filestack is not raw object storage — it's an upload pipeline with a hosted Picker widget, CDN, and on-the-fly image transformation URLs. No presign proxy is needed for basic uploads. Choose Filestack if you need a drag-and-drop file picker with built-in processing; choose B2 if you need direct control over object storage at the lowest possible price.
Frequently asked questions
Do I need a paid Bubble plan to integrate with Backblaze B2?
Yes. The presign proxy call happens in a Bubble Backend Workflow (a server-side action), which requires at minimum the Starter plan ($32/month). The Bubble Free plan cannot run Backend Workflows or API Workflows. Your Backblaze B2 account can remain on the free tier (10 GB storage) during development.
Why can't I just put my B2 credentials directly in the Bubble API Connector instead of using a proxy?
You technically could, but it's not safe for file uploads. AWS Signature V4 signing requires your secret key to be computed into the signature at request time — if your credentials are used in any client-side Bubble action or workflow, they can be extracted from browser network requests. The proxy pattern keeps credentials server-side at all times. For admin-only operations where only you (the developer) trigger the workflow and it runs server-side in Bubble, the API Connector's 'Private' header provides adequate protection — but the proxy gives you more control and portability.
What is s3ForcePathStyle and why is it required for Backblaze B2?
By default, the AWS SDK constructs S3 request URLs with the bucket name as a subdomain: `bucketname.s3.endpoint.com/objectkey` (virtual-hosted style). Backblaze B2 does not support this URL format — it requires path-style URLs: `s3.endpoint.com/bucketname/objectkey`. Setting `forcePathStyle: true` in the S3Client forces the SDK to use the path-style format that B2 expects. Without it, every request fails with a DNS resolution error or connection refused.
How do I find the correct regional endpoint for my B2 bucket?
Go to the Backblaze console (secure.backblaze.com) → B2 Cloud Storage → Buckets → click your bucket name → look for the 'Endpoint' field in bucket details. It shows the exact URL including the region number, such as `s3.us-west-004.backblazeb2.com`. Do not try to construct this URL from the region name alone — the number suffix is specific to each bucket's physical location.
Can I use Backblaze B2 to serve images directly in my Bubble app's Image elements?
Yes — for private buckets, generate a presigned GET URL from your proxy and bind it to the Image element's dynamic URL. For public buckets (bucket type: allPublic), you can construct the download URL directly as `https://f{clusterNumber}.backblazeb2.com/file/{bucketName}/{objectKey}` and bind that to the Image element without any presigning. Find your cluster number in the bucket details. Public bucket images are accessible to anyone with the URL.
How does Backblaze B2 compare to Wasabi on price for small apps?
For apps storing less than 1 TB, Backblaze B2 is significantly cheaper. Wasabi has a 1 TB per month minimum charge (~$6.99/month) regardless of actual usage. Backblaze B2 has no minimum — 10 GB is free, and beyond that you pay only for what you store (~$6/TB/month). For a small app storing 50 GB, B2 costs roughly $0.24/month while Wasabi costs $6.99/month minimum.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation