Skip to main content
RapidDev - Software Development Agency
bolt-ai-integrationsBolt Chat + API Route

How to Integrate Bolt.new with Backblaze B2

Backblaze B2 integrates with Bolt.new using the AWS S3 SDK pointed at B2's endpoint — the API is S3-compatible, so the code is nearly identical to AWS S3 integration. Generate pre-signed upload URLs in a Next.js API route to keep your B2 credentials server-side, upload files from the browser using the pre-signed URL, and serve files via Cloudflare CDN for free egress. B2 costs about 75% less than S3 with no egress fees through Cloudflare.

What you'll learn

  • How to configure the AWS S3 SDK to work with Backblaze B2's S3-compatible endpoint
  • How to generate pre-signed upload URLs in a Next.js API route to keep B2 credentials server-side
  • How to build a file upload component in React that uploads directly to B2 via pre-signed URL
  • How to connect a Cloudflare CDN in front of B2 for free egress and fast global delivery
  • Why outbound uploads work in Bolt's WebContainer preview but webhook callbacks require deployment
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate18 min read30 minutesStorageApril 2026RapidDev Engineering Team
TL;DR

Backblaze B2 integrates with Bolt.new using the AWS S3 SDK pointed at B2's endpoint — the API is S3-compatible, so the code is nearly identical to AWS S3 integration. Generate pre-signed upload URLs in a Next.js API route to keep your B2 credentials server-side, upload files from the browser using the pre-signed URL, and serve files via Cloudflare CDN for free egress. B2 costs about 75% less than S3 with no egress fees through Cloudflare.

Backblaze B2 in Bolt.new: S3-Compatible Storage at a Fraction of the Cost

Backblaze B2 is one of the best-kept secrets in cloud storage for web developers. At $0.006 per GB per month — compared to S3's $0.023/GB — B2 is about 75% cheaper. More importantly, B2 has a partnership with Cloudflare under the 'Bandwidth Alliance': any B2 bucket connected to a Cloudflare CDN zone transfers data for free. For image-heavy apps, SaaS products with user uploads, or any application that serves files to many users, this makes B2 dramatically more cost-effective than S3.

The technical reason B2 is easy to integrate in Bolt.new is its S3 API compatibility. Backblaze implemented the S3 API protocol on B2, meaning you use the exact same @aws-sdk/client-s3 package you would use for AWS. The only differences are: the endpoint URL (B2's regional endpoint instead of AWS's), different credential field names (applicationKeyId instead of accessKeyId, applicationKey instead of secretAccessKey), and the bucket name resolution method (path-style instead of virtual-hosted-style). Change these three configuration details and all S3 SDK calls work against B2.

In Bolt's WebContainer, outbound HTTPS requests work fine — the S3 SDK communicates over HTTPS to upload and download files, so it functions correctly in the Bolt preview. However, if your app needs to receive webhook callbacks (for example, if B2 triggers a notification when a file is processed), those callbacks cannot reach the Bolt WebContainer. Deploy to Netlify or Bolt Cloud first, then configure any callback URLs using your deployed domain.

Integration method

Bolt Chat + API Route

Backblaze B2 uses the AWS S3 SDK with B2's custom endpoint URL — since B2 is S3-compatible, you configure @aws-sdk/client-s3 with B2's regional endpoint instead of AWS's. File uploads use pre-signed URLs generated in a Next.js API route: the server generates a time-limited signed URL using your B2 credentials, the browser uploads directly to B2 using that URL, and B2 files are served via Cloudflare CDN for free bandwidth. Bolt generates this pattern reliably when prompted with the B2 endpoint details.

Prerequisites

  • A Backblaze account at backblaze.com with a B2 Cloud Storage bucket created
  • B2 Application Key ID and Application Key (created in B2 → App Keys → Create Application Key)
  • Your B2 bucket's regional endpoint URL (found in B2 bucket settings, format: s3.us-west-004.backblazeb2.com)
  • A Bolt.new project using Next.js for API routes (needed for server-side credential handling)
  • Optional: Cloudflare account with your B2 bucket connected for free egress (highly recommended for public file serving)

Step-by-step guide

1

Create a B2 Bucket and Generate Application Keys

Before writing any code, set up your Backblaze B2 storage bucket and generate the credentials your Bolt app will use. Log into backblaze.com, navigate to B2 Cloud Storage → Buckets, and click 'Create a Bucket'. Choose a globally unique bucket name (B2 bucket names are global across all Backblaze accounts), set access to 'Private' for secure applications (you will use pre-signed URLs) or 'Public' for assets you want universally accessible like profile images. Select your preferred region — us-west-004 (Western US) is Backblaze's primary region. After creating the bucket, note the bucket's Endpoint URL shown in the bucket settings — it follows the format s3.[region].backblazeb2.com. This is your S3-compatible endpoint and the most important configuration value. Next, create an Application Key: navigate to Account → App Keys → Add a New Application Key. Give it a descriptive name ('my-app-uploads'), optionally restrict it to your specific bucket (recommended for security — don't grant access to all buckets), and select the capabilities you need: readFiles, writeFiles, deleteFiles. After creating, immediately copy the keyID and applicationKey — Backblaze only shows the applicationKey once. Store these as environment variables in your Bolt project's .env file: B2_KEY_ID, B2_APP_KEY, B2_BUCKET_NAME, and B2_ENDPOINT. Never paste these credentials into your Bolt chat or code directly.

Bolt.new Prompt

Create a .env file with these Backblaze B2 environment variables (I'll fill in the values): B2_KEY_ID=your_application_key_id_here, B2_APP_KEY=your_application_key_here, B2_BUCKET_NAME=your_bucket_name_here, B2_ENDPOINT=https://s3.us-west-004.backblazeb2.com, B2_REGION=us-west-004, VITE_B2_CDN_URL=https://your-cdn-domain.com (optional Cloudflare CDN URL for serving files)

Paste this in Bolt.new chat

.env
1# .env
2B2_KEY_ID=your_application_key_id_here
3B2_APP_KEY=your_application_key_here
4B2_BUCKET_NAME=your-bucket-name
5B2_ENDPOINT=https://s3.us-west-004.backblazeb2.com
6B2_REGION=us-west-004
7
8# Optional: Cloudflare CDN domain connected to your B2 bucket
9# Enables free egress for public files
10VITE_B2_CDN_URL=https://cdn.yourdomain.com

Pro tip: Create two sets of B2 credentials: one application key for development (restricted to your dev bucket) and one for production (restricted to your production bucket). This prevents test uploads from mixing with production data and limits blast radius if a key is compromised.

Expected result: Your B2 bucket exists with your chosen access level, you have your Application Key ID and Application Key saved, and your .env file has all four B2 environment variables filled in.

2

Install S3 SDK and Configure B2 Client

Install the AWS S3 SDK packages and configure the S3 client to point at Backblaze B2. The @aws-sdk/client-s3 package is the official modular AWS SDK v3 for S3 operations. The @aws-sdk/s3-request-presigner package generates pre-signed URLs for browser-side uploads and downloads. Both are pure JavaScript packages with no native binaries — they install and run correctly in Bolt's WebContainer. The S3 client configuration for B2 has four key differences from standard AWS S3: the endpoint is set to B2's regional URL (https://s3.us-west-004.backblazeb2.com), credentials use B2's keyId/appKey values in the accessKeyId/secretAccessKey fields, the region is set to the B2 region code (us-west-004), and forcePathStyle must be set to true (B2 uses path-style bucket URLs like s3.endpoint.com/bucket-name instead of AWS's virtual-hosted-style bucket-name.s3.amazonaws.com). Create the S3 client in a lib/b2.ts file that is imported by your API routes. Because this file uses process.env credentials, it runs only in server-side contexts (Next.js API routes) — never in browser-side components. The Vite VITE_ prefix is for client-side public values only; B2 credentials must stay in server-side environment variables without the VITE_ prefix.

Bolt.new Prompt

Install @aws-sdk/client-s3 and @aws-sdk/s3-request-presigner. Create lib/b2.ts that exports a configured S3Client pointing at Backblaze B2. Configure it with: endpoint from B2_ENDPOINT env var, region from B2_REGION env var (default 'us-west-004'), credentials using B2_KEY_ID as accessKeyId and B2_APP_KEY as secretAccessKey, and forcePathStyle: true. Export B2_BUCKET_NAME from process.env.B2_BUCKET_NAME. Export the client as b2Client and the bucket name as b2Bucket.

Paste this in Bolt.new chat

lib/b2.ts
1// lib/b2.ts
2import { S3Client } from '@aws-sdk/client-s3';
3
4const endpoint = process.env.B2_ENDPOINT;
5const region = process.env.B2_REGION || 'us-west-004';
6const accessKeyId = process.env.B2_KEY_ID!;
7const secretAccessKey = process.env.B2_APP_KEY!;
8
9if (!endpoint || !accessKeyId || !secretAccessKey) {
10 throw new Error('Missing Backblaze B2 environment variables: B2_ENDPOINT, B2_KEY_ID, B2_APP_KEY');
11}
12
13export const b2Client = new S3Client({
14 endpoint,
15 region,
16 credentials: {
17 accessKeyId,
18 secretAccessKey,
19 },
20 forcePathStyle: true, // Required for B2 — path-style URLs
21});
22
23export const b2Bucket = process.env.B2_BUCKET_NAME!;
24
25if (!b2Bucket) {
26 throw new Error('Missing B2_BUCKET_NAME environment variable');
27}

Pro tip: The forcePathStyle: true setting is critical for B2 compatibility. Without it, the SDK tries to use virtual-hosted-style URLs (bucket.s3.endpoint.com) which B2 does not support, causing all requests to fail with a DNS error.

Expected result: The @aws-sdk/client-s3 and @aws-sdk/s3-request-presigner packages are installed. lib/b2.ts exports a configured S3 client pointed at B2 with forcePathStyle enabled.

3

Create Pre-Signed URL API Route

The pre-signed URL pattern is the recommended way to upload files to B2 from a browser. A pre-signed URL is a temporary, time-limited URL that grants permission to upload a specific file to B2 without exposing your B2 credentials to the browser. The flow is: browser asks your API route for a pre-signed URL, API route generates the URL using your server-side B2 credentials, browser uploads directly to B2 using that URL, B2 accepts the upload without needing to know your app's credentials. The API route for generating upload URLs uses PutObjectCommand from @aws-sdk/client-s3 and getSignedUrl from @aws-sdk/s3-request-presigner. You define the target S3 key (the object path within your bucket), set the expiry time (60–300 seconds is typical for interactive uploads), and optionally specify the content type. The API route returns the pre-signed URL to the browser. For the S3 object key naming strategy, include path prefixes to organize uploads: 'profile-photos/' for avatars, 'documents/' for PDFs, 'uploads/[userId]/' for user-specific files. Include a timestamp or UUID in the filename to prevent collisions. For example: profile-photos/2026-01-15-550e8400-e29b-41d4-a716-446655440000.jpg. This key becomes the file's address within your B2 bucket and, once served through Cloudflare CDN, the path portion of your CDN URL.

Bolt.new Prompt

Create a Next.js API route at app/api/upload-url/route.ts that generates a Backblaze B2 pre-signed upload URL. The POST route accepts JSON body: { filename: string, contentType: string, folder?: string }. Generate the S3 key as: {folder || 'uploads'}/{Date.now()}-{crypto.randomUUID()}-{sanitized_filename}. Use PutObjectCommand and getSignedUrl from @aws-sdk/s3-request-presigner with expiresIn: 120 seconds. Return: { uploadUrl: string, objectKey: string, publicUrl: string }. publicUrl should be the CDN URL if VITE_B2_CDN_URL is set, otherwise the B2 direct URL. Sanitize the filename to remove special characters.

Paste this in Bolt.new chat

app/api/upload-url/route.ts
1// app/api/upload-url/route.ts
2import { NextResponse } from 'next/server';
3import { PutObjectCommand } from '@aws-sdk/client-s3';
4import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
5import { b2Client, b2Bucket } from '@/lib/b2';
6import crypto from 'crypto';
7
8function sanitizeFilename(filename: string): string {
9 return filename
10 .replace(/[^a-zA-Z0-9._-]/g, '-')
11 .replace(/--+/g, '-')
12 .toLowerCase();
13}
14
15export async function POST(request: Request) {
16 try {
17 const { filename, contentType, folder = 'uploads' } = await request.json();
18
19 if (!filename || !contentType) {
20 return NextResponse.json(
21 { error: 'filename and contentType are required' },
22 { status: 400 }
23 );
24 }
25
26 const sanitized = sanitizeFilename(filename);
27 const objectKey = `${folder}/${Date.now()}-${crypto.randomUUID()}-${sanitized}`;
28
29 const command = new PutObjectCommand({
30 Bucket: b2Bucket,
31 Key: objectKey,
32 ContentType: contentType,
33 });
34
35 const uploadUrl = await getSignedUrl(b2Client, command, { expiresIn: 120 });
36
37 const cdnBase = process.env.VITE_B2_CDN_URL;
38 const b2Base = process.env.B2_ENDPOINT;
39 const publicUrl = cdnBase
40 ? `${cdnBase}/${objectKey}`
41 : `${b2Base}/file/${b2Bucket}/${objectKey}`;
42
43 return NextResponse.json({ uploadUrl, objectKey, publicUrl });
44 } catch (error: unknown) {
45 const e = error as { message: string };
46 return NextResponse.json({ error: e.message }, { status: 500 });
47 }
48}

Pro tip: Add authentication to the upload URL API route so only logged-in users can generate pre-signed URLs. Without authentication, anyone who discovers the endpoint can upload unlimited files to your B2 bucket at your expense.

Expected result: POST to /api/upload-url with a filename and contentType returns a pre-signed B2 upload URL valid for 120 seconds, the object key, and a public URL for accessing the file after upload.

4

Build the File Upload React Component

With the pre-signed URL API route in place, build the React component that handles the user-facing upload experience. The upload flow has three steps: (1) user selects a file, (2) component calls /api/upload-url to get a pre-signed URL, (3) component uploads the file directly to B2 using a PUT request to the pre-signed URL. This entire flow works in Bolt's WebContainer preview because it uses only outbound HTTPS requests — no incoming webhooks or TCP connections. For progress tracking, use XMLHttpRequest instead of fetch. XHR provides an upload.onprogress event that fires with loaded and total bytes, enabling a real-time progress bar. Fetch does not have a progress API for upload tracking. The XHR pattern: create an XMLHttpRequest, open a PUT request to the pre-signed URL, set the Content-Type header, listen for upload.onprogress, and call xhr.send(file) to start the upload. After a successful upload, the publicUrl returned by /api/upload-url is immediately usable if your B2 bucket is public. For private buckets, generate a separate pre-signed GetObject URL via a download API route. Store the objectKey (not the pre-signed URL, which expires) in your database — generate fresh download URLs on demand from the objectKey whenever a user requests access.

Bolt.new Prompt

Create a FileUploader React component that uploads files to Backblaze B2. The component has a drag-and-drop zone and a file input button. On file selection: (1) validate file type (images and PDFs only) and size (max 20MB), (2) call POST /api/upload-url with filename and contentType, (3) upload the file directly to the returned uploadUrl using XMLHttpRequest PUT with a Content-Type header and upload progress tracking, (4) show a progress bar with percentage, (5) on success call an onUploadComplete(publicUrl: string, objectKey: string) prop callback. Show error states for validation failures and upload failures. Style with Tailwind — dashed border drag zone, progress bar in blue.

Paste this in Bolt.new chat

components/FileUploader.tsx
1// components/FileUploader.tsx
2import { useState, useCallback } from 'react';
3
4interface FileUploaderProps {
5 onUploadComplete: (publicUrl: string, objectKey: string) => void;
6 accept?: string;
7 maxSizeMB?: number;
8 folder?: string;
9}
10
11export function FileUploader({
12 onUploadComplete,
13 accept = 'image/*,application/pdf',
14 maxSizeMB = 20,
15 folder = 'uploads',
16}: FileUploaderProps) {
17 const [progress, setProgress] = useState(0);
18 const [status, setStatus] = useState<'idle' | 'uploading' | 'done' | 'error'>('idle');
19 const [error, setError] = useState('');
20 const [isDragging, setIsDragging] = useState(false);
21
22 const uploadFile = useCallback(async (file: File) => {
23 if (file.size > maxSizeMB * 1024 * 1024) {
24 setError(`File exceeds ${maxSizeMB}MB limit`);
25 return;
26 }
27
28 setStatus('uploading');
29 setProgress(0);
30 setError('');
31
32 try {
33 // Step 1: Get pre-signed URL
34 const res = await fetch('/api/upload-url', {
35 method: 'POST',
36 headers: { 'Content-Type': 'application/json' },
37 body: JSON.stringify({ filename: file.name, contentType: file.type, folder }),
38 });
39 const { uploadUrl, objectKey, publicUrl } = await res.json();
40
41 // Step 2: Upload directly to B2 with progress tracking
42 await new Promise<void>((resolve, reject) => {
43 const xhr = new XMLHttpRequest();
44 xhr.open('PUT', uploadUrl);
45 xhr.setRequestHeader('Content-Type', file.type);
46 xhr.upload.onprogress = (e) => {
47 if (e.lengthComputable) setProgress(Math.round((e.loaded / e.total) * 100));
48 };
49 xhr.onload = () => (xhr.status === 200 ? resolve() : reject(new Error(`Upload failed: ${xhr.status}`)));
50 xhr.onerror = () => reject(new Error('Network error during upload'));
51 xhr.send(file);
52 });
53
54 setStatus('done');
55 onUploadComplete(publicUrl, objectKey);
56 } catch (err: unknown) {
57 const e = err as { message: string };
58 setError(e.message);
59 setStatus('error');
60 }
61 }, [onUploadComplete, folder, maxSizeMB]);
62
63 return (
64 <div
65 onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }}
66 onDragLeave={() => setIsDragging(false)}
67 onDrop={(e) => { e.preventDefault(); setIsDragging(false); const f = e.dataTransfer.files[0]; if (f) uploadFile(f); }}
68 className={`border-2 border-dashed rounded-xl p-8 text-center transition-colors ${
69 isDragging ? 'border-blue-500 bg-blue-50' : 'border-gray-300 bg-gray-50 hover:bg-gray-100'
70 }`}
71 >
72 {status === 'idle' && (
73 <>
74 <p className="text-gray-600 mb-3">Drop a file here or</p>
75 <label className="cursor-pointer bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700">
76 Browse Files
77 <input type="file" accept={accept} className="hidden" onChange={(e) => { const f = e.target.files?.[0]; if (f) uploadFile(f); }} />
78 </label>
79 </>
80 )}
81 {status === 'uploading' && (
82 <div>
83 <p className="text-gray-600 mb-2">Uploading... {progress}%</p>
84 <div className="w-full bg-gray-200 rounded-full h-2">
85 <div className="bg-blue-600 h-2 rounded-full transition-all" style={{ width: `${progress}%` }} />
86 </div>
87 </div>
88 )}
89 {status === 'done' && <p className="text-green-600 font-medium">Upload complete!</p>}
90 {status === 'error' && <p className="text-red-600">{error}</p>}
91 </div>
92 );
93}

Pro tip: Store objectKey in your database, not publicUrl or the pre-signed URL. The objectKey is permanent; publicUrl may change if you switch CDN providers, and pre-signed URLs expire. Generate fresh URLs from objectKey on demand.

Expected result: The FileUploader component uploads files to B2 via pre-signed URL with real-time progress tracking. Uploads work in Bolt's WebContainer preview since they use outbound HTTPS requests.

5

Deploy and Configure Cloudflare CDN for Free Egress

Deploy your Bolt project to Netlify or Bolt Cloud to move into a real Node.js environment. After deployment, update your environment variables in the hosting dashboard (Netlify: Site Settings → Environment Variables) with your B2 credentials. The B2 API route runs as a serverless function post-deployment, keeping credentials secure. To connect Cloudflare for free B2 egress — one of B2's most compelling advantages — follow these steps: add your domain to Cloudflare, create a CNAME DNS record pointing to your B2 bucket's download endpoint (for public buckets: f004.backblazeb2.com or the region-specific equivalent). Enable Cloudflare proxy (orange cloud icon). Your B2 files are now served through Cloudflare's global CDN with no B2 egress charges. Update the VITE_B2_CDN_URL environment variable in your deployment to your Cloudflare-proxied domain. For private buckets, you can use Cloudflare Workers to add authentication headers before forwarding requests to B2 — but this is an advanced pattern. For most Bolt apps, use a public B2 bucket for user-generated content like profile photos and use pre-signed download URLs for sensitive documents. The WebContainer TCP limitation note: during Bolt development, the file upload (outbound PUT request) works fine in the preview. Bolt's WebContainer cannot receive incoming requests from B2, so if B2 sends event notifications (available in B2 Event Notifications beta), register notification URLs using your deployed Netlify or Bolt Cloud domain, not a localhost or WebContainer preview URL.

Bolt.new Prompt

Help me prepare for deployment with Backblaze B2. Create a netlify.toml with build command 'npm run build', publish '.next', Node version 20. Make sure all B2-related API routes have proper error handling that returns JSON even when B2 credentials are missing or the upload fails, so the app doesn't show a blank error page. Add a GET /api/b2/health route that checks if B2 credentials are configured and returns {configured: true/false, bucket: string}.

Paste this in Bolt.new chat

netlify.toml
1# netlify.toml
2[build]
3 command = "npm run build"
4 publish = ".next"
5
6[build.environment]
7 NODE_VERSION = "20"
8
9[[plugins]]
10 package = "@netlify/plugin-nextjs"
11
12# Environment variables to add in Netlify Dashboard:
13# B2_KEY_ID = your_application_key_id
14# B2_APP_KEY = your_application_key
15# B2_BUCKET_NAME = your_bucket_name
16# B2_ENDPOINT = https://s3.us-west-004.backblazeb2.com
17# B2_REGION = us-west-004
18# VITE_B2_CDN_URL = https://cdn.yourdomain.com (if using Cloudflare)

Pro tip: B2's free egress to Cloudflare requires your Cloudflare zone to have the B2 bucket domain proxied (orange cloud icon on the DNS record). If you set Cloudflare to DNS-only (gray cloud), normal B2 egress pricing applies. The bandwidth alliance only works with proxied Cloudflare traffic.

Expected result: App is deployed to Netlify with B2 credentials configured as environment variables. Files upload to B2 and serve via Cloudflare CDN at no egress cost. The /api/b2/health endpoint confirms credentials are configured.

Common use cases

User Profile Photo Upload with Image Serving

Allow users to upload profile photos to B2, store the B2 object key in your database, and serve images through Cloudflare CDN. The API route generates a pre-signed URL for the upload, the browser uploads directly to B2, and a Cloudflare-proxied custom domain serves images at no egress cost. This pattern scales to thousands of users without S3 costs.

Bolt.new Prompt

Add a profile photo upload feature using Backblaze B2. Create a Next.js API route at app/api/upload-url/route.ts that generates a B2 pre-signed upload URL using @aws-sdk/client-s3 PutObjectCommand. Configure the S3 client with: endpoint 'https://s3.us-west-004.backblazeb2.com' (I'll update the region), accessKeyId from B2_KEY_ID env var, secretAccessKey from B2_APP_KEY env var, forcePathStyle: true. The API route accepts a filename and contentType in the request body and returns a pre-signed URL valid for 60 seconds. Create a ProfilePhotoUploader React component that accepts a file via drag-and-drop or file input, calls the API for a pre-signed URL, uploads directly to B2, then shows a preview of the uploaded image.

Copy this prompt to try it in Bolt.new

Document Storage and Download System

Build a document management feature where users upload PDFs and other files, receive a download link, and files are stored securely in a private B2 bucket. Pre-signed download URLs provide time-limited access without exposing permanent public links, suitable for sensitive documents like invoices, contracts, or reports.

Bolt.new Prompt

Build a document storage system with Backblaze B2. Private B2 bucket (not public). Two API routes: POST /api/documents/upload-url (generates pre-signed PutObject URL, accepts filename/contentType/userId, stores document metadata in Supabase with fields: id, user_id, filename, b2_key, size, created_at) and GET /api/documents/[id]/download (generates pre-signed GetObject URL valid for 15 minutes, verifies user owns the document before generating URL). A DocumentList component shows all user documents with filename, size, upload date, and a 'Download' button that calls the download URL endpoint. Use @aws-sdk/client-s3 and @aws-sdk/s3-request-presigner packages.

Copy this prompt to try it in Bolt.new

Multi-File Upload with Progress Tracking

Build a bulk file upload feature for a content management app where users upload multiple images at once with individual progress bars. B2's S3-compatible multipart upload supports large files; for smaller files, individual PutObject pre-signed URLs work well. Track upload progress using XMLHttpRequest's upload progress events, which provide per-file percentage.

Bolt.new Prompt

Build a multi-file upload component for Backblaze B2. The component accepts up to 10 files at once (max 50MB each). For each file, fetch a pre-signed upload URL from /api/upload-url. Upload files in parallel using XMLHttpRequest (not fetch) so we can track upload progress with xhr.upload.onprogress. Show each file in a list with a progress bar and status (uploading X%, complete, error). On success, emit an array of public CDN URLs. The CDN base URL is in the VITE_B2_CDN_URL environment variable (a Cloudflare custom domain). API route generates pre-signed URLs with the object key pattern: uploads/{timestamp}-{random}-{original-filename}.

Copy this prompt to try it in Bolt.new

Troubleshooting

S3 request fails with 'InvalidAccessKeyId' or 'SignatureDoesNotMatch'

Cause: B2 Application Key ID or Application Key is incorrect, or the credentials have been revoked in the Backblaze console.

Solution: Verify B2_KEY_ID is your Application Key ID (not your Backblaze account ID) and B2_APP_KEY is your Application Key (not your Backblaze account password). These are found in B2 Cloud Storage → App Keys. If the key was only shown once during creation and you didn't save it, create a new application key — existing keys cannot be retrieved.

typescript
1// Verify the client configuration in lib/b2.ts:
2console.log('B2 config:', {
3 endpoint: process.env.B2_ENDPOINT,
4 region: process.env.B2_REGION,
5 keyId: process.env.B2_KEY_ID?.substring(0, 8) + '...', // Log only prefix for security
6 bucketName: process.env.B2_BUCKET_NAME,
7});

Upload fails with 'Access Denied' or 403 when using the pre-signed URL

Cause: The pre-signed URL has expired (default 120s), the Content-Type in the PUT request doesn't match the Content-Type used when generating the pre-signed URL, or the application key doesn't have writeFiles permission for this bucket.

Solution: Ensure the Content-Type header in the PUT request exactly matches the contentType sent to /api/upload-url when generating the pre-signed URL. Increase the expiresIn value if users are taking too long to start uploads. Check B2 App Key capabilities include 'writeFiles' and the key is not restricted to a different bucket.

typescript
1// In the XHR upload, ensure Content-Type matches exactly:
2xhr.setRequestHeader('Content-Type', file.type);
3// The PutObjectCommand must use the same contentType:
4const command = new PutObjectCommand({
5 Bucket: b2Bucket,
6 Key: objectKey,
7 ContentType: contentType, // Must match what the browser sends
8});

Files upload successfully but are not accessible via CDN or B2 public URL

Cause: B2 bucket is set to 'Private' but you are trying to access files via direct URL without a pre-signed download URL. Or Cloudflare CDN is configured incorrectly — DNS-only (gray cloud) instead of proxied (orange cloud).

Solution: For private buckets: generate pre-signed GetObject URLs via a download API route. For public buckets: verify the bucket is set to 'Public' in B2 settings and files are accessible via the B2 file URL format. For Cloudflare CDN issues: verify the DNS record has the orange cloud (proxied) status and that your Cloudflare zone includes the B2 endpoint domain.

typescript
1// Download URL API route for private buckets:
2import { GetObjectCommand } from '@aws-sdk/client-s3';
3import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
4
5export async function GET(request: Request, { params }: { params: { key: string } }) {
6 const command = new GetObjectCommand({ Bucket: b2Bucket, Key: params.key });
7 const downloadUrl = await getSignedUrl(b2Client, command, { expiresIn: 900 }); // 15 min
8 return NextResponse.json({ downloadUrl });
9}

CORS error when uploading to B2 pre-signed URL from the browser

Cause: B2 bucket CORS rules do not allow requests from your app's origin. B2 buckets have a default CORS policy that may not permit your development domain.

Solution: Configure CORS on your B2 bucket via the B2 API (there is no GUI for CORS in the Backblaze dashboard). Use the b2_update_bucket API endpoint or the Backblaze B2 CLI to set a CORS rule allowing your origin. In development, your Bolt preview URL or localhost:3000 must be included. In production, allow your deployed domain.

typescript
1// CORS rule for B2 bucket (applied via B2 API or CLI):
2// corsRules: [{
3// corsRuleName: 'allowWebUploads',
4// allowedOrigins: ['https://yourdomain.com', 'http://localhost:3000'],
5// allowedOperations: ['b2_upload_file', 'b2_download_file_by_name'],
6// allowedHeaders: ['Content-Type', 'Authorization', 'X-Bz-*'],
7// maxAgeSeconds: 3600
8// }]

Best practices

  • Always generate pre-signed upload URLs from a server-side API route — never expose B2_KEY_ID or B2_APP_KEY in client-side code or VITE_ prefixed variables
  • Store the objectKey in your database, not the full URL or pre-signed URL — keys are permanent, URLs change when you switch CDNs or when pre-signed URLs expire
  • Set forcePathStyle: true on the S3 client configuration — B2 does not support virtual-hosted-style URLs and requests will fail without this setting
  • Connect a Cloudflare CDN in front of public B2 buckets — the Bandwidth Alliance eliminates all egress costs for Cloudflare-proxied traffic, which is B2's biggest cost advantage
  • Create separate B2 application keys for development and production environments with bucket-specific access — do not use your master application key for app credentials
  • Use object key prefixes to organize files by type and user: 'uploads/{userId}/profile/' for profile photos, 'uploads/{userId}/documents/' for user documents — this makes access control and cleanup easier
  • Set appropriate CORS rules on your B2 bucket to allow uploads from your specific domains only — overly permissive CORS (allowedOrigins: ['*']) allows any site to trigger uploads at your expense

Alternatives

Frequently asked questions

Does Backblaze B2 work in Bolt's WebContainer preview?

File uploads work in the Bolt preview because they use outbound HTTPS requests (PUT requests to pre-signed URLs). The B2 SDK communicates over HTTPS, which WebContainers support fully. However, incoming webhook callbacks — if B2 sends event notifications to your app — cannot reach the WebContainer. Deploy to Netlify or Bolt Cloud to test webhook-based features like file processing notifications.

Why use the AWS S3 SDK for Backblaze B2?

Backblaze B2 implements the S3 API protocol, making it compatible with any S3 SDK. The AWS S3 SDK (@aws-sdk/client-s3) is the most mature, well-documented S3 client available. By pointing it at B2's endpoint with forcePathStyle: true, you get all the SDK's features — pre-signed URLs, multipart upload, object tagging — against B2's cheaper storage infrastructure. There is no official Backblaze SDK to install.

How much does Backblaze B2 cost compared to AWS S3?

B2 storage costs $0.006/GB/month versus S3's $0.023/GB/month — about 75% cheaper. Egress (downloading files) costs $0.01/GB on B2 versus $0.09/GB on S3. With the Cloudflare Bandwidth Alliance, egress from B2 through Cloudflare CDN is completely free. For a typical web app serving 100GB of images per month, B2 + Cloudflare costs approximately $0.60 in storage only, versus S3's $9 in storage plus $9 in egress.

Can I migrate from AWS S3 to Backblaze B2 in a Bolt project?

Yes, migration is straightforward because both use the S3 SDK. Change the endpoint from AWS's URL to B2's endpoint, update accessKeyId to your B2_KEY_ID, secretAccessKey to your B2_APP_KEY, add forcePathStyle: true, and update the bucket name. Your existing PutObjectCommand, GetObjectCommand, and getSignedUrl calls work without any other changes. Test thoroughly in development before switching production credentials.

Do I need Cloudflare to use Backblaze B2?

No, Cloudflare is optional. B2 works without Cloudflare — files are served directly from B2's CDN at standard B2 egress rates ($0.01/GB). Cloudflare is strongly recommended for public file serving because the Bandwidth Alliance eliminates egress costs entirely. If you are building a private app where files are accessed infrequently (admin dashboards, document archives), B2 without Cloudflare is perfectly adequate.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.