# How to Integrate Bolt.new with Backblaze B2 Cloud Storage

- Tool: Bolt.new
- Difficulty: Intermediate
- Time required: 20 minutes
- Last updated: April 2026

## TL;DR

Integrate Backblaze B2 Cloud Storage with Bolt.new using the S3-compatible API and AWS SDK. B2 costs ~75% less than AWS S3 with free egress through the Cloudflare Bandwidth Alliance. Create a B2 bucket, generate application keys, configure the AWS S3 client with B2's endpoint URL, and upload files via pre-signed URLs generated in a Next.js API route. The S3 SDK code is nearly identical to standard AWS S3.

## Add Backblaze B2 File Storage to Bolt.new at 75% Less Cost

Backblaze B2 is a cloud object storage service that costs approximately $0.006 per GB per month, compared to AWS S3's $0.023 — about 75% less expensive for identical functionality. More significantly, Backblaze B2 offers completely free egress bandwidth when your domain or bucket is served through Cloudflare or Fastly via the Bandwidth Alliance program, eliminating the egress costs that often dominate S3 bills for media-heavy applications. For Bolt.new apps that store user uploads, profile images, documents, or generated media, B2 delivers the same reliability and S3-compatible API at a fraction of the cost.

The key technical advantage of B2 for Bolt.new developers is full S3 API compatibility. Rather than learning a new SDK, you use the @aws-sdk/client-s3 package — the only configuration change is setting the endpoint URL to B2's regional endpoint and providing B2 application key credentials. The same PutObjectCommand, GetObjectCommand, DeleteObjectCommand, and getSignedUrl utilities work without modification. If you have existing AWS S3 code, migrating to B2 is typically a two-line change in your SDK configuration.

For Bolt.new specifically, the pre-signed URL upload pattern is the recommended approach. A Next.js API route generates a short-lived signed URL using server-side credentials (keeping your B2 application key out of the browser), and the client uploads files directly to B2 using that URL. This pattern avoids routing large files through your serverless function, prevents timeout issues, and reduces bandwidth consumption. Public files in B2 can be served directly via B2's download URL or through a Cloudflare CDN layer for zero egress cost when your domain is proxied through Cloudflare.

## Before you start

- A Backblaze account with a B2 bucket created (select a region close to your users, note the bucket endpoint URL)
- A B2 application key generated with Read and Write permissions for your bucket (Backblaze dashboard → Application Keys → Add a New Application Key)
- Your B2 bucket name, endpoint URL (e.g., s3.us-west-004.backblazeb2.com), region code, Key ID, and Application Key
- Optional: Cloudflare account with your domain proxied through Cloudflare for free egress bandwidth via the Bandwidth Alliance
- A Bolt.new account with a new Next.js project open

## Step-by-step guide

### 1. Create a Backblaze B2 Bucket and Generate Application Keys

Log into your Backblaze account at backblaze.com and navigate to B2 Cloud Storage → Buckets. Click Create a Bucket. Choose a globally unique bucket name (B2 bucket names must be unique across all Backblaze accounts, similar to S3). For files that need to be publicly accessible (profile images, CDN assets), select Public for the file access setting. For private files (documents, user uploads that need authentication), select Private. Choose a region close to your users — options include US West (us-west-004), US East (us-east-005), EU Central (eu-central-003), and others. Note the region code because you need it for the S3 endpoint URL. After creating the bucket, find the endpoint URL by clicking the bucket name and looking at the bucket details — it will be in the format s3.[region].backblazeb2.com. Next, generate application keys for API access. Go to Application Keys in the B2 section and click Add a New Application Key. Give it a descriptive name (like 'bolt-app-key'), select Allow access to Bucket: [your bucket name] rather than all buckets for better security, and set the type of access to Read and Write. Click Create New Key. The Key ID and Application Key are displayed only once — copy both immediately and store them securely. The Application Key is equivalent to a secret key and should never be exposed in client-side code.

```
# .env — add to project root in Bolt.new
B2_ENDPOINT=https://s3.us-west-004.backblazeb2.com
B2_REGION=us-west-004
B2_KEY_ID=your_b2_key_id_here
B2_APP_KEY=your_b2_application_key_here
B2_BUCKET_NAME=your-bucket-name
# Optional: If using Cloudflare CDN in front of B2
CLOUDFLARE_CDN_URL=https://cdn.yourdomain.com
```

**Expected result:** Your B2 bucket is created with the correct access setting. You have the endpoint URL, region, Key ID, and Application Key saved in your .env file.

### 2. Install the AWS SDK and Prompt Bolt to Generate the Upload URL Route

Backblaze B2's S3-compatible API means you use the same @aws-sdk/client-s3 and @aws-sdk/s3-request-presigner packages you would use for AWS S3. The only configuration difference is the endpoint URL — instead of letting the SDK infer the AWS endpoint from the region, you provide the B2 endpoint explicitly. This works because B2 implements the S3 REST API at the path and query parameter level, including the SigV4 request signing protocol used by the SDK. In your Bolt project, open the chat and prompt Bolt to install the AWS SDK packages and generate the pre-signed upload URL route. The route accepts a filename and content type from the React component, generates a unique storage key to avoid filename collisions, creates a PutObjectCommand for the B2 bucket, and returns a pre-signed URL valid for a limited time (10 minutes is a reasonable default for direct browser uploads). The React component uses this URL to PUT the file directly to B2 — no server proxying of the file content required. After Bolt generates the route, verify the S3Client initialization uses process.env.B2_ENDPOINT as the endpoint, process.env.B2_REGION as the region, and process.env.B2_KEY_ID and process.env.B2_APP_KEY as the credentials. Confirm that none of these variables use the NEXT_PUBLIC_ prefix, keeping all B2 credentials server-side only. During development in Bolt's WebContainer, the API route generates the pre-signed URL correctly and outbound calls to B2 work for upload and download operations. WebContainers cannot receive incoming connections from B2, but B2 storage does not require incoming connections — all operations are initiated by your code.

```
// app/api/storage/upload-url/route.ts
import { NextResponse } from 'next/server';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import crypto from 'crypto';
import path from 'path';

const s3 = new S3Client({
  endpoint: process.env.B2_ENDPOINT,
  region: process.env.B2_REGION || 'us-west-004',
  credentials: {
    accessKeyId: process.env.B2_KEY_ID!,
    secretAccessKey: process.env.B2_APP_KEY!,
  },
  // Required for Backblaze B2 S3-compatible API
  forcePathStyle: true,
});

export async function POST(request: Request) {
  const { B2_BUCKET_NAME, B2_ENDPOINT } = process.env;

  if (!B2_BUCKET_NAME || !B2_ENDPOINT || !process.env.B2_KEY_ID || !process.env.B2_APP_KEY) {
    return NextResponse.json(
      { error: 'Backblaze B2 environment variables are not configured' },
      { status: 500 }
    );
  }

  try {
    const body = await request.json();
    const { filename, contentType, folder = 'uploads' } = body as {
      filename: string;
      contentType: string;
      folder?: string;
    };

    if (!filename || !contentType) {
      return NextResponse.json(
        { error: 'filename and contentType are required' },
        { status: 400 }
      );
    }

    const ext = path.extname(filename);
    const uniqueKey = `${folder}/${crypto.randomUUID()}${ext}`;

    const command = new PutObjectCommand({
      Bucket: B2_BUCKET_NAME,
      Key: uniqueKey,
      ContentType: contentType,
    });

    const uploadUrl = await getSignedUrl(s3, command, { expiresIn: 600 });

    // Construct the public download URL
    // B2 public URL format: ENDPOINT/BUCKET/KEY
    const endpointHost = new URL(B2_ENDPOINT).host;
    const publicUrl = `https://${endpointHost}/${B2_BUCKET_NAME}/${uniqueKey}`;

    return NextResponse.json({ uploadUrl, publicUrl, key: uniqueKey });
  } catch (error) {
    const message = error instanceof Error ? error.message : 'Unknown error';
    return NextResponse.json({ error: message }, { status: 500 });
  }
}
```

**Expected result:** Calling POST /api/storage/upload-url with a filename and contentType returns a pre-signed B2 upload URL and the final public file URL. The upload URL is a B2 S3-compatible endpoint URL.

### 3. Build the React File Upload Component

With the upload URL API route working, build the React component that handles file selection, requests a pre-signed URL, and uploads the file directly to B2. The two-step upload process — get URL then upload — is the most important pattern to implement correctly: the component first POSTs to your API route to get the pre-signed URL, then uses that URL to PUT the file directly to Backblaze B2. This approach keeps file data out of your serverless function entirely, which is important for large files because serverless functions have execution time limits and often have request body size limits around 4.5–50 MB depending on the hosting platform. The direct upload to B2 can handle files of any size limited only by B2's per-file maximum (5 TB). Track upload progress using XMLHttpRequest instead of fetch — the fetch API does not expose upload progress events, but XHR's upload.onprogress event provides real-time progress percentage. After the upload succeeds, the component receives the publicUrl returned by the API route and can display the uploaded file or save the URL to a database. For image uploads, display a preview using the publicUrl immediately after upload without needing a separate fetch. Include file type validation in the component (check that the selected file's MIME type matches what the application expects) to avoid uploading invalid files before even requesting a pre-signed URL.

```
// components/B2FileUpload.tsx
'use client';

import { useState, useRef } from 'react';

interface Props {
  accept?: string;
  folder?: string;
  onUploadComplete: (result: { publicUrl: string; key: string }) => void;
  maxSizeMB?: number;
}

export function B2FileUpload({
  accept = 'image/*',
  folder = 'uploads',
  onUploadComplete,
  maxSizeMB = 10,
}: Props) {
  const [uploading, setUploading] = useState(false);
  const [progress, setProgress] = useState(0);
  const [error, setError] = useState<string | null>(null);
  const [preview, setPreview] = useState<string | null>(null);
  const inputRef = useRef<HTMLInputElement>(null);

  async function handleFileChange(file: File) {
    setError(null);
    setProgress(0);

    if (file.size > maxSizeMB * 1024 * 1024) {
      setError(`File must be smaller than ${maxSizeMB}MB`);
      return;
    }

    // Show local preview immediately for images
    if (file.type.startsWith('image/')) {
      setPreview(URL.createObjectURL(file));
    }

    setUploading(true);
    try {
      // Step 1: Get pre-signed upload URL from API route
      const urlResponse = await fetch('/api/storage/upload-url', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          filename: file.name,
          contentType: file.type,
          folder,
        }),
      });

      if (!urlResponse.ok) {
        const errData = await urlResponse.json();
        throw new Error(errData.error || 'Failed to get upload URL');
      }

      const { uploadUrl, publicUrl, key } = await urlResponse.json();

      // Step 2: Upload directly to Backblaze B2 using pre-signed URL
      await new Promise<void>((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.upload.onprogress = (e) => {
          if (e.lengthComputable) {
            setProgress(Math.round((e.loaded / e.total) * 100));
          }
        };
        xhr.onload = () => {
          if (xhr.status >= 200 && xhr.status < 300) resolve();
          else reject(new Error(`B2 upload failed: ${xhr.status}`));
        };
        xhr.onerror = () => reject(new Error('Network error during upload'));
        xhr.open('PUT', uploadUrl);
        xhr.setRequestHeader('Content-Type', file.type);
        xhr.send(file);
      });

      onUploadComplete({ publicUrl, key });
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Upload failed');
      setPreview(null);
    } finally {
      setUploading(false);
    }
  }

  return (
    <div className="space-y-4">
      <div
        className="border-2 border-dashed border-gray-300 rounded-xl p-8 text-center cursor-pointer hover:border-blue-400 transition-colors"
        onClick={() => inputRef.current?.click()}
        onDragOver={(e) => e.preventDefault()}
        onDrop={(e) => {
          e.preventDefault();
          const file = e.dataTransfer.files[0];
          if (file) handleFileChange(file);
        }}
      >
        {preview ? (
          <img src={preview} alt="Preview" className="max-h-48 mx-auto rounded-lg object-contain" />
        ) : (
          <div className="text-gray-500">
            <p className="text-lg font-medium">Drop a file here</p>
            <p className="text-sm mt-1">or click to browse</p>
          </div>
        )}
      </div>

      <input
        ref={inputRef}
        type="file"
        accept={accept}
        className="hidden"
        onChange={(e) => {
          const file = e.target.files?.[0];
          if (file) handleFileChange(file);
        }}
      />

      {uploading && (
        <div className="space-y-2">
          <div className="w-full bg-gray-200 rounded-full h-2">
            <div
              className="bg-blue-500 h-2 rounded-full transition-all duration-300"
              style={{ width: `${progress}%` }}
            />
          </div>
          <p className="text-sm text-gray-600 text-center">{progress}% uploaded</p>
        </div>
      )}

      {error && (
        <p className="text-sm text-red-600 bg-red-50 rounded-lg p-3">{error}</p>
      )}
    </div>
  );
}
```

**Expected result:** The B2FileUpload component renders a drag-and-drop file picker. Selecting a file shows a local preview, then uploads directly to B2 with a live progress bar, and calls onUploadComplete with the B2 public URL after success.

### 4. Configure B2 CORS for Browser Uploads

Backblaze B2 requires explicit CORS configuration on the bucket before browsers can upload files directly using pre-signed URLs. Without CORS rules, the browser-side PUT request to the B2 endpoint will be blocked by the browser's same-origin policy, even though the server-side URL generation works correctly. CORS for B2 is configured using the Backblaze B2 API or the b2 command-line tool — it is not available through the web UI dashboard. The simplest way to configure CORS is using the B2 CLI. Install it with pip install b2 and authenticate with b2 authorize-account KEY_ID APP_KEY, then use b2 update-bucket to set the CORS rules. The CORS configuration should allow PUT and GET methods from all origins (or specifically from your Netlify deployment URL) with the Content-Type header permitted. For development in Bolt's WebContainer, allow the StackBlitz preview origins or simply use a wildcard for the allowed origins. For production, restrict origins to your specific deployment URLs. Once CORS is configured, upload requests from the browser using pre-signed URLs will succeed. Note that GET requests for public files do not require CORS because public files are served without authentication headers — CORS is only needed for authenticated operations like pre-signed PUT uploads. If you prefer not to use the CLI, you can also configure CORS by calling the B2 API directly using a fetch request with your application key credentials from your Bolt API route — the b2_update_bucket endpoint accepts a corsRules array.

```
// app/api/storage/setup-cors/route.ts
// Run this ONCE to configure CORS on your B2 bucket.
// After running, this route is no longer needed.
import { NextResponse } from 'next/server';

export async function POST() {
  const keyId = process.env.B2_KEY_ID;
  const appKey = process.env.B2_APP_KEY;
  const bucketName = process.env.B2_BUCKET_NAME;

  if (!keyId || !appKey || !bucketName) {
    return NextResponse.json({ error: 'B2 credentials not configured' }, { status: 500 });
  }

  try {
    // Step 1: Authorize with B2 native API
    const authResponse = await fetch(
      'https://api.backblazeb2.com/b2api/v3/b2_authorize_account',
      {
        headers: {
          Authorization: `Basic ${Buffer.from(`${keyId}:${appKey}`).toString('base64')}`,
        },
      }
    );

    if (!authResponse.ok) {
      const err = await authResponse.json();
      return NextResponse.json({ error: `B2 auth failed: ${err.message}` }, { status: 401 });
    }

    const auth = await authResponse.json();
    const { authorizationToken, apiInfo } = auth;
    const apiUrl = apiInfo?.storageApi?.apiUrl || auth.apiUrl;

    // Step 2: Get bucket ID
    const bucketsResponse = await fetch(`${apiUrl}/b2api/v3/b2_list_buckets?bucketName=${bucketName}`, {
      headers: { Authorization: authorizationToken },
    });
    const bucketsData = await bucketsResponse.json();
    const bucket = bucketsData.buckets?.[0];

    if (!bucket) {
      return NextResponse.json({ error: `Bucket ${bucketName} not found` }, { status: 404 });
    }

    // Step 3: Update bucket with CORS rules
    const corsResponse = await fetch(`${apiUrl}/b2api/v3/b2_update_bucket`, {
      method: 'POST',
      headers: {
        Authorization: authorizationToken,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        accountId: auth.accountId,
        bucketId: bucket.bucketId,
        corsRules: [
          {
            corsRuleName: 'bolt-app-uploads',
            allowedOrigins: ['*'],
            allowedOperations: [
              'b2_download_file_by_name',
              'b2_upload_file',
              's3_head',
              's3_get',
              's3_put',
            ],
            allowedHeaders: [
              'Content-Type',
              'x-amz-content-sha256',
              'x-amz-date',
              'authorization',
              'x-amz-security-token',
              'content-length',
            ],
            maxAgeSeconds: 3600,
          },
        ],
      }),
    });

    const corsData = await corsResponse.json();
    return NextResponse.json({ success: true, corsRules: corsData.corsRules });
  } catch (error) {
    const message = error instanceof Error ? error.message : 'Unknown error';
    return NextResponse.json({ error: message }, { status: 500 });
  }
}
```

**Expected result:** Calling POST /api/storage/setup-cors configures CORS rules on your B2 bucket. After this step, browser-side PUT uploads using pre-signed URLs succeed without CORS errors.

### 5. Deploy to Netlify and Add B2 Environment Variables

Your Backblaze B2 integration works in Bolt's WebContainer preview because all B2 API calls are outbound HTTP requests from the server-side Next.js routes — outbound calls to B2's S3-compatible API work correctly in the WebContainer development environment, so you can test URL generation and even complete uploads during development. When you are ready to deploy, connect your Bolt project to Netlify through Settings → Applications → Netlify → Connect. After the initial deployment, navigate to your Netlify dashboard → Site Configuration → Environment Variables and add the following variables from your .env file: B2_ENDPOINT, B2_REGION, B2_KEY_ID, B2_APP_KEY, and B2_BUCKET_NAME. If you are using Cloudflare CDN for zero-egress serving, also add CLOUDFLARE_CDN_URL. Trigger a redeploy from the Netlify dashboard after adding environment variables. One WebContainer limitation to note: Bolt's development preview cannot receive incoming connections from external services. Backblaze B2 does not require incoming connections to your app — all upload and download operations are initiated by your application code — so this limitation does not affect the B2 integration. However, if you implement B2's event notification webhooks (available for detecting when files are uploaded or deleted), those webhooks require a deployed Netlify URL. After deploying, update the CORS rules on your B2 bucket to restrict allowedOrigins from ['*'] to your specific Netlify deployment URL for better security. Re-run the setup-cors route with the updated environment or update the corsRules directly via the B2 dashboard.

```
// app/api/storage/files/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { S3Client, ListObjectsV2Command } from '@aws-sdk/client-s3';

const s3 = new S3Client({
  endpoint: process.env.B2_ENDPOINT,
  region: process.env.B2_REGION || 'us-west-004',
  credentials: {
    accessKeyId: process.env.B2_KEY_ID!,
    secretAccessKey: process.env.B2_APP_KEY!,
  },
  forcePathStyle: true,
});

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const prefix = searchParams.get('prefix') || '';
  const maxKeys = parseInt(searchParams.get('maxKeys') || '50', 10);
  const continuationToken = searchParams.get('continuationToken') || undefined;

  const bucketName = process.env.B2_BUCKET_NAME;
  const endpoint = process.env.B2_ENDPOINT;

  if (!bucketName || !endpoint) {
    return NextResponse.json({ error: 'B2 not configured' }, { status: 500 });
  }

  try {
    const command = new ListObjectsV2Command({
      Bucket: bucketName,
      Prefix: prefix,
      MaxKeys: maxKeys,
      ContinuationToken: continuationToken,
    });

    const response = await s3.send(command);
    const endpointHost = new URL(endpoint).host;

    const files = (response.Contents || []).map((obj) => ({
      key: obj.Key,
      size: obj.Size,
      lastModified: obj.LastModified,
      publicUrl: `https://${endpointHost}/${bucketName}/${obj.Key}`,
    }));

    return NextResponse.json({
      files,
      nextContinuationToken: response.NextContinuationToken || null,
      isTruncated: response.IsTruncated || false,
    });
  } catch (error) {
    const message = error instanceof Error ? error.message : 'Unknown error';
    return NextResponse.json({ error: message }, { status: 500 });
  }
}
```

**Expected result:** Your Bolt.new app is deployed on Netlify with B2 environment variables configured. File uploads work end-to-end in production. The file listing endpoint returns stored files with their B2 public URLs.

## Best practices

- Always set forcePathStyle: true in the AWS S3Client configuration when using Backblaze B2 — virtual-hosted-style URLs are not supported by B2's S3-compatible API
- Never expose B2_APP_KEY as a NEXT_PUBLIC_ variable — keep it server-side in API routes only, as it grants write access to your entire bucket
- Generate unique file keys using crypto.randomUUID() to prevent filename collisions and make it impossible for users to overwrite each other's files by guessing filenames
- Use pre-signed URL uploads (GET signed URL → PUT directly to B2) rather than proxying file content through your serverless function — this avoids function timeouts for large files and reduces bandwidth costs
- Configure B2 CORS rules with specific allowed origins matching your production domain rather than the wildcard '*' to prevent unauthorized uploads from other sites
- Pair Backblaze B2 with Cloudflare CDN (connected via a CNAME record) to take advantage of the Bandwidth Alliance for zero egress costs on all served files
- Use folder prefixes (e.g., uploads/, avatars/, documents/) when generating file keys to organize stored files and simplify listing operations with the prefix parameter
- For private files, always generate pre-signed download URLs with a short expiration (1 hour or less) rather than making buckets public — this prevents unauthorized access even if URLs leak

## Use cases

### User Profile Image Upload

Allow users to upload profile images from the browser directly to Backblaze B2 using pre-signed URLs. The Next.js API route generates a time-limited signed upload URL, the browser uploads the file directly to B2, and the resulting public B2 URL is saved to the user's profile in the database. This keeps large file transfers out of the serverless function and provides fast direct uploads.

Prompt example:

```
Build a profile image upload feature using Backblaze B2 storage. Create an API route at app/api/storage/upload-url/route.ts that generates a pre-signed PUT URL for B2 using @aws-sdk/client-s3 and @aws-sdk/s3-request-presigner configured with process.env.B2_ENDPOINT, process.env.B2_REGION, process.env.B2_KEY_ID, and process.env.B2_APP_KEY. Accept 'filename' and 'contentType' in the request body, generate a unique key using crypto.randomUUID(), create the presigned URL with PutObjectCommand for bucket process.env.B2_BUCKET_NAME, and return the upload URL and the final public file URL.
```

### Document Storage and Download

Store user-generated documents, PDFs, and files in Backblaze B2 and serve them to authenticated users via short-lived pre-signed download URLs. This pattern is ideal for invoice PDFs, contract documents, or any files that should not be publicly accessible without authentication.

Prompt example:

```
Create a document storage system using Backblaze B2 in this Next.js project. Build an API route at app/api/storage/download-url/route.ts that generates a pre-signed GET URL for a B2 object using @aws-sdk/client-s3 and @aws-sdk/s3-request-presigner. Accept a 'key' query parameter with the object key. Configure the S3 client with B2's endpoint from process.env.B2_ENDPOINT and credentials from B2_KEY_ID and B2_APP_KEY. Generate a GetObjectCommand presigned URL valid for 3600 seconds and return it. Create a Documents page at app/documents/page.tsx that lists stored documents and generates download links on demand.
```

### App Asset CDN with Cloudflare Free Egress

Use Backblaze B2 as the origin storage for images and assets served through Cloudflare, taking advantage of the Bandwidth Alliance to serve assets with zero egress costs. Configure Cloudflare as a CDN in front of B2, upload assets via the API route, and serve them through the Cloudflare CDN URL for maximum performance and no bandwidth charges.

Prompt example:

```
Build an asset upload and CDN serving system for this Next.js project using Backblaze B2 with Cloudflare CDN. Create an API route at app/api/storage/asset/route.ts that uploads image files to B2 using @aws-sdk/client-s3 with PutObjectCommand. Accept multipart form data with a 'file' field. Generate a unique filename with crypto.randomUUID() + the original extension. After upload, return the Cloudflare CDN URL constructed as process.env.CLOUDFLARE_CDN_URL + '/' + key (not the B2 direct URL). Create an image upload component with drag-and-drop support that displays the CDN URL after successful upload.
```

## Troubleshooting

### AWS SDK throws 'InvalidSignatureException' or 'SignatureDoesNotMatch' when calling B2

Cause: The S3Client is missing the forcePathStyle: true configuration option, causing the SDK to format the request as virtual-hosted-style (bucket.endpoint) which B2 does not support.

Solution: Add forcePathStyle: true to your S3Client configuration. This forces the SDK to use path-style URLs (endpoint/bucket/key) rather than virtual-hosted-style (bucket.endpoint/key). B2's S3-compatible API requires path-style URLs.

```
const s3 = new S3Client({
  endpoint: process.env.B2_ENDPOINT,
  region: process.env.B2_REGION,
  credentials: {
    accessKeyId: process.env.B2_KEY_ID!,
    secretAccessKey: process.env.B2_APP_KEY!,
  },
  forcePathStyle: true, // REQUIRED for Backblaze B2
});
```

### Browser upload to pre-signed URL fails with CORS error ('Access to XMLHttpRequest blocked by CORS policy')

Cause: The Backblaze B2 bucket does not have CORS rules configured, or the configured rules do not include the origin of the Bolt preview or deployed app.

Solution: Run the /api/storage/setup-cors route once to configure CORS on your B2 bucket. After deployment, update the CORS allowedOrigins from ['*'] to your specific Netlify domain for better security. CORS must be configured using the B2 API — it is not available in the B2 web dashboard.

### Pre-signed URL upload succeeds but file is not publicly accessible via the B2 URL

Cause: The B2 bucket is set to Private access, or the PutObjectCommand was generated without the correct ACL setting, so uploaded files default to private visibility.

Solution: For public files, ensure the B2 bucket is set to Public access in the Backblaze dashboard. If the bucket must remain private, generate pre-signed GET URLs for downloads using GetObjectCommand and getSignedUrl rather than serving the direct B2 URL.

```
// For private bucket: generate download URL
import { GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const downloadCommand = new GetObjectCommand({
  Bucket: process.env.B2_BUCKET_NAME!,
  Key: fileKey,
});
const downloadUrl = await getSignedUrl(s3, downloadCommand, { expiresIn: 3600 });
```

### B2 endpoint returns 403 Forbidden for file upload despite correct credentials

Cause: The application key was created with access restricted to a specific bucket or bucket prefix that does not match the key being written, or the key does not have Write permissions.

Solution: In the Backblaze dashboard, go to Application Keys and verify the key has Write access and is associated with the correct bucket. If the key was created with a keyNamePrefix restriction, ensure your upload keys start with that prefix. Generate a new application key with the correct permissions if needed.

## Frequently asked questions

### Does Bolt.new work with Backblaze B2?

Yes. Backblaze B2 works with Bolt.new through the S3-compatible API using the standard @aws-sdk/client-s3 package. Configure the S3Client with B2's endpoint URL, region, and application key credentials, and set forcePathStyle: true. Outbound API calls to B2 work correctly in Bolt's WebContainer development preview, so you can test the full upload flow before deploying.

### Can I use the AWS SDK with Backblaze B2 in Bolt.new?

Yes. Backblaze B2 implements the S3-compatible API, which means @aws-sdk/client-s3 and @aws-sdk/s3-request-presigner work with B2 with minimal configuration changes. The only requirements are: set the endpoint to B2's regional URL, provide B2 application key credentials as accessKeyId and secretAccessKey, and enable forcePathStyle: true. All standard S3 SDK operations including PutObject, GetObject, DeleteObject, ListObjectsV2, and getSignedUrl work identically.

### How does Backblaze B2 compare to AWS S3 for Bolt.new projects?

B2 costs approximately 75% less than AWS S3 ($0.006/GB/mo vs $0.023/GB/mo) and offers free egress when paired with Cloudflare through the Bandwidth Alliance. The S3-compatible API means migration from S3 to B2 requires only changing the endpoint URL and credentials. For Bolt.new projects that don't require S3-specific features like Lambda triggers, Glacier archiving, or tight AWS service integrations, B2 delivers identical functionality at lower cost.

### Can I get free egress bandwidth with Backblaze B2?

Yes. Backblaze B2 participates in the Bandwidth Alliance with Cloudflare and Fastly. Traffic served through Cloudflare or Fastly from B2 has zero egress charges. To enable this, connect a Cloudflare-proxied domain to your B2 bucket using a CNAME record pointing to your bucket's download URL, then serve files through your Cloudflare domain rather than directly from the B2 URL.

### Can B2 event notifications or webhooks reach the Bolt.new development preview?

No. Bolt's WebContainer runs inside a browser tab and cannot receive incoming HTTP connections. B2 event notifications (triggered when files are uploaded or deleted) require a deployed environment with a public URL. For the core upload and download integration, this does not matter — all operations are initiated by your application code. Only if you specifically need B2 event notifications do you need to deploy first.

### Why do I need to configure CORS on my B2 bucket?

CORS must be configured so browsers can make direct PUT requests to B2 using pre-signed URLs. Without CORS rules, the browser blocks the upload request as a cross-origin request to the B2 domain. CORS configuration for B2 must be done via the B2 API (not the web dashboard) — use the setup-cors API route provided in this guide to configure the rules once. Server-side upload via your API route does not require CORS because server-side code is not subject to browser CORS restrictions.

---

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