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

How to Integrate Bolt.new with Backblaze B2 Cloud Storage

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.

What you'll learn

  • How to create a Backblaze B2 bucket and generate application keys with the correct permissions
  • How to configure the AWS S3 SDK to work with B2's S3-compatible endpoint
  • How to build a Next.js API route in Bolt.new that generates pre-signed upload URLs for B2
  • How to implement a React file upload component that uploads directly to B2 using pre-signed URLs
  • How to deploy your Bolt.new app to Netlify and configure Backblaze B2 CORS for production uploads
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate21 min read20 minutesStorageApril 2026RapidDev Engineering Team
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.

Integration method

Bolt Chat + API Route

Backblaze B2 supports the S3-compatible API, which means the @aws-sdk/client-s3 library works with B2 by simply pointing it at B2's regional endpoint URL. In Bolt.new you build a Next.js API route that uses the AWS SDK configured with B2 credentials and endpoint to generate pre-signed upload URLs. The browser uploads files directly to B2 using the pre-signed URL, keeping secret credentials server-side. Public files are served directly from B2's CDN URL or through Cloudflare for free egress.

Prerequisites

  • 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
1# .env add to project root in Bolt.new
2B2_ENDPOINT=https://s3.us-west-004.backblazeb2.com
3B2_REGION=us-west-004
4B2_KEY_ID=your_b2_key_id_here
5B2_APP_KEY=your_b2_application_key_here
6B2_BUCKET_NAME=your-bucket-name
7# Optional: If using Cloudflare CDN in front of B2
8CLOUDFLARE_CDN_URL=https://cdn.yourdomain.com

Pro tip: For Cloudflare free egress, enable Cloudflare's proxy (orange cloud) for a CNAME record pointing to your B2 bucket's download URL. This activates the Bandwidth Alliance and all files served through Cloudflare will have zero B2 egress charges. B2 Download URL format is: f[number].backblazeb2.com/file/[bucket-name]/ — check your bucket details for the exact domain.

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.

Bolt.new Prompt

Install @aws-sdk/client-s3 and @aws-sdk/s3-request-presigner, then create a Backblaze B2 file upload integration in this Next.js project. Build an API route at app/api/storage/upload-url/route.ts that: creates an S3Client configured with endpoint process.env.B2_ENDPOINT, region process.env.B2_REGION, and credentials from B2_KEY_ID (accessKeyId) and B2_APP_KEY (secretAccessKey). Accepts a POST request with JSON body { filename: string, contentType: string, folder?: string }. Generates a unique file key using crypto.randomUUID() + the original file extension. Creates a PutObjectCommand for bucket process.env.B2_BUCKET_NAME with the generated key, ContentType, and ACL: 'public-read' for public buckets. Returns a presigned PUT URL valid for 600 seconds and the final public file URL.

Paste this in Bolt.new chat

app/api/storage/upload-url/route.ts
1// app/api/storage/upload-url/route.ts
2import { NextResponse } from 'next/server';
3import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
4import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
5import crypto from 'crypto';
6import path from 'path';
7
8const s3 = new S3Client({
9 endpoint: process.env.B2_ENDPOINT,
10 region: process.env.B2_REGION || 'us-west-004',
11 credentials: {
12 accessKeyId: process.env.B2_KEY_ID!,
13 secretAccessKey: process.env.B2_APP_KEY!,
14 },
15 // Required for Backblaze B2 S3-compatible API
16 forcePathStyle: true,
17});
18
19export async function POST(request: Request) {
20 const { B2_BUCKET_NAME, B2_ENDPOINT } = process.env;
21
22 if (!B2_BUCKET_NAME || !B2_ENDPOINT || !process.env.B2_KEY_ID || !process.env.B2_APP_KEY) {
23 return NextResponse.json(
24 { error: 'Backblaze B2 environment variables are not configured' },
25 { status: 500 }
26 );
27 }
28
29 try {
30 const body = await request.json();
31 const { filename, contentType, folder = 'uploads' } = body as {
32 filename: string;
33 contentType: string;
34 folder?: string;
35 };
36
37 if (!filename || !contentType) {
38 return NextResponse.json(
39 { error: 'filename and contentType are required' },
40 { status: 400 }
41 );
42 }
43
44 const ext = path.extname(filename);
45 const uniqueKey = `${folder}/${crypto.randomUUID()}${ext}`;
46
47 const command = new PutObjectCommand({
48 Bucket: B2_BUCKET_NAME,
49 Key: uniqueKey,
50 ContentType: contentType,
51 });
52
53 const uploadUrl = await getSignedUrl(s3, command, { expiresIn: 600 });
54
55 // Construct the public download URL
56 // B2 public URL format: ENDPOINT/BUCKET/KEY
57 const endpointHost = new URL(B2_ENDPOINT).host;
58 const publicUrl = `https://${endpointHost}/${B2_BUCKET_NAME}/${uniqueKey}`;
59
60 return NextResponse.json({ uploadUrl, publicUrl, key: uniqueKey });
61 } catch (error) {
62 const message = error instanceof Error ? error.message : 'Unknown error';
63 return NextResponse.json({ error: message }, { status: 500 });
64 }
65}

Pro tip: Backblaze B2 requires forcePathStyle: true in the S3Client configuration. Without this, the AWS SDK formats the URL as bucket.endpoint (virtual-hosted-style), which B2 does not support. Always set forcePathStyle: true when using the AWS SDK with non-AWS S3-compatible storage.

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.

Bolt.new Prompt

Build a file upload component for Backblaze B2 in this Next.js project. Create a component at components/B2FileUpload.tsx that: accepts 'accept' (MIME type string), 'folder' (storage folder, default 'uploads'), and 'onUploadComplete' (callback with publicUrl and key) props. Renders a styled file input with drag-and-drop support using Tailwind CSS. On file select: validates the file type against the accept prop, calls POST /api/storage/upload-url with filename and contentType to get the presigned URL, then uploads the file directly to B2 using fetch with method PUT and the file as the body. Shows an upload progress indicator during the upload using a progress state. Calls onUploadComplete with the publicUrl and key on success. Shows an error message on failure.

Paste this in Bolt.new chat

components/B2FileUpload.tsx
1// components/B2FileUpload.tsx
2'use client';
3
4import { useState, useRef } from 'react';
5
6interface Props {
7 accept?: string;
8 folder?: string;
9 onUploadComplete: (result: { publicUrl: string; key: string }) => void;
10 maxSizeMB?: number;
11}
12
13export function B2FileUpload({
14 accept = 'image/*',
15 folder = 'uploads',
16 onUploadComplete,
17 maxSizeMB = 10,
18}: Props) {
19 const [uploading, setUploading] = useState(false);
20 const [progress, setProgress] = useState(0);
21 const [error, setError] = useState<string | null>(null);
22 const [preview, setPreview] = useState<string | null>(null);
23 const inputRef = useRef<HTMLInputElement>(null);
24
25 async function handleFileChange(file: File) {
26 setError(null);
27 setProgress(0);
28
29 if (file.size > maxSizeMB * 1024 * 1024) {
30 setError(`File must be smaller than ${maxSizeMB}MB`);
31 return;
32 }
33
34 // Show local preview immediately for images
35 if (file.type.startsWith('image/')) {
36 setPreview(URL.createObjectURL(file));
37 }
38
39 setUploading(true);
40 try {
41 // Step 1: Get pre-signed upload URL from API route
42 const urlResponse = await fetch('/api/storage/upload-url', {
43 method: 'POST',
44 headers: { 'Content-Type': 'application/json' },
45 body: JSON.stringify({
46 filename: file.name,
47 contentType: file.type,
48 folder,
49 }),
50 });
51
52 if (!urlResponse.ok) {
53 const errData = await urlResponse.json();
54 throw new Error(errData.error || 'Failed to get upload URL');
55 }
56
57 const { uploadUrl, publicUrl, key } = await urlResponse.json();
58
59 // Step 2: Upload directly to Backblaze B2 using pre-signed URL
60 await new Promise<void>((resolve, reject) => {
61 const xhr = new XMLHttpRequest();
62 xhr.upload.onprogress = (e) => {
63 if (e.lengthComputable) {
64 setProgress(Math.round((e.loaded / e.total) * 100));
65 }
66 };
67 xhr.onload = () => {
68 if (xhr.status >= 200 && xhr.status < 300) resolve();
69 else reject(new Error(`B2 upload failed: ${xhr.status}`));
70 };
71 xhr.onerror = () => reject(new Error('Network error during upload'));
72 xhr.open('PUT', uploadUrl);
73 xhr.setRequestHeader('Content-Type', file.type);
74 xhr.send(file);
75 });
76
77 onUploadComplete({ publicUrl, key });
78 } catch (err) {
79 setError(err instanceof Error ? err.message : 'Upload failed');
80 setPreview(null);
81 } finally {
82 setUploading(false);
83 }
84 }
85
86 return (
87 <div className="space-y-4">
88 <div
89 className="border-2 border-dashed border-gray-300 rounded-xl p-8 text-center cursor-pointer hover:border-blue-400 transition-colors"
90 onClick={() => inputRef.current?.click()}
91 onDragOver={(e) => e.preventDefault()}
92 onDrop={(e) => {
93 e.preventDefault();
94 const file = e.dataTransfer.files[0];
95 if (file) handleFileChange(file);
96 }}
97 >
98 {preview ? (
99 <img src={preview} alt="Preview" className="max-h-48 mx-auto rounded-lg object-contain" />
100 ) : (
101 <div className="text-gray-500">
102 <p className="text-lg font-medium">Drop a file here</p>
103 <p className="text-sm mt-1">or click to browse</p>
104 </div>
105 )}
106 </div>
107
108 <input
109 ref={inputRef}
110 type="file"
111 accept={accept}
112 className="hidden"
113 onChange={(e) => {
114 const file = e.target.files?.[0];
115 if (file) handleFileChange(file);
116 }}
117 />
118
119 {uploading && (
120 <div className="space-y-2">
121 <div className="w-full bg-gray-200 rounded-full h-2">
122 <div
123 className="bg-blue-500 h-2 rounded-full transition-all duration-300"
124 style={{ width: `${progress}%` }}
125 />
126 </div>
127 <p className="text-sm text-gray-600 text-center">{progress}% uploaded</p>
128 </div>
129 )}
130
131 {error && (
132 <p className="text-sm text-red-600 bg-red-50 rounded-lg p-3">{error}</p>
133 )}
134 </div>
135 );
136}

Pro tip: The AWS SDK's getSignedUrl function for a PutObjectCommand produces a URL that expects the Content-Type header to match exactly what was specified when generating the URL. Always pass the actual file MIME type from file.type when requesting the pre-signed URL, and set the same Content-Type header when doing the PUT upload.

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.

Bolt.new Prompt

Add a Backblaze B2 CORS configuration route to this project that I can call once to set up bucket CORS rules. Create an API route at app/api/storage/setup-cors/route.ts that calls the Backblaze B2 native API (not S3 API) to configure CORS on the bucket. First authorize using B2_KEY_ID and B2_APP_KEY to get an auth token and API URL from https://api.backblazeb2.com/b2api/v3/b2_authorize_account. Then call b2_update_bucket with corsRules allowing origins ['*'], operations ['b2_download_file_by_name', 'b2_upload_file', 's3_head', 's3_get', 's3_put'], headers ['Content-Type', 'x-amz-content-sha256', 'x-amz-date', 'authorization', 'x-amz-security-token'], maxAgeSeconds 3600.

Paste this in Bolt.new chat

app/api/storage/setup-cors/route.ts
1// app/api/storage/setup-cors/route.ts
2// Run this ONCE to configure CORS on your B2 bucket.
3// After running, this route is no longer needed.
4import { NextResponse } from 'next/server';
5
6export async function POST() {
7 const keyId = process.env.B2_KEY_ID;
8 const appKey = process.env.B2_APP_KEY;
9 const bucketName = process.env.B2_BUCKET_NAME;
10
11 if (!keyId || !appKey || !bucketName) {
12 return NextResponse.json({ error: 'B2 credentials not configured' }, { status: 500 });
13 }
14
15 try {
16 // Step 1: Authorize with B2 native API
17 const authResponse = await fetch(
18 'https://api.backblazeb2.com/b2api/v3/b2_authorize_account',
19 {
20 headers: {
21 Authorization: `Basic ${Buffer.from(`${keyId}:${appKey}`).toString('base64')}`,
22 },
23 }
24 );
25
26 if (!authResponse.ok) {
27 const err = await authResponse.json();
28 return NextResponse.json({ error: `B2 auth failed: ${err.message}` }, { status: 401 });
29 }
30
31 const auth = await authResponse.json();
32 const { authorizationToken, apiInfo } = auth;
33 const apiUrl = apiInfo?.storageApi?.apiUrl || auth.apiUrl;
34
35 // Step 2: Get bucket ID
36 const bucketsResponse = await fetch(`${apiUrl}/b2api/v3/b2_list_buckets?bucketName=${bucketName}`, {
37 headers: { Authorization: authorizationToken },
38 });
39 const bucketsData = await bucketsResponse.json();
40 const bucket = bucketsData.buckets?.[0];
41
42 if (!bucket) {
43 return NextResponse.json({ error: `Bucket ${bucketName} not found` }, { status: 404 });
44 }
45
46 // Step 3: Update bucket with CORS rules
47 const corsResponse = await fetch(`${apiUrl}/b2api/v3/b2_update_bucket`, {
48 method: 'POST',
49 headers: {
50 Authorization: authorizationToken,
51 'Content-Type': 'application/json',
52 },
53 body: JSON.stringify({
54 accountId: auth.accountId,
55 bucketId: bucket.bucketId,
56 corsRules: [
57 {
58 corsRuleName: 'bolt-app-uploads',
59 allowedOrigins: ['*'],
60 allowedOperations: [
61 'b2_download_file_by_name',
62 'b2_upload_file',
63 's3_head',
64 's3_get',
65 's3_put',
66 ],
67 allowedHeaders: [
68 'Content-Type',
69 'x-amz-content-sha256',
70 'x-amz-date',
71 'authorization',
72 'x-amz-security-token',
73 'content-length',
74 ],
75 maxAgeSeconds: 3600,
76 },
77 ],
78 }),
79 });
80
81 const corsData = await corsResponse.json();
82 return NextResponse.json({ success: true, corsRules: corsData.corsRules });
83 } catch (error) {
84 const message = error instanceof Error ? error.message : 'Unknown error';
85 return NextResponse.json({ error: message }, { status: 500 });
86 }
87}

Pro tip: Call this setup-cors route once from the Bolt terminal or via a fetch in your browser after setting up the project. You only need to run it once per bucket — CORS rules persist on the bucket. After running it, you can delete this route from the project if you prefer not to leave a CORS management endpoint active in production.

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.

Bolt.new Prompt

Add a file listing API route to this Backblaze B2 Next.js project. Create an API route at app/api/storage/files/route.ts that uses @aws-sdk/client-s3 with ListObjectsV2Command to list objects in the B2 bucket. Accept optional 'prefix' and 'maxKeys' query params. Return an array of objects with key, size, lastModified, and publicUrl constructed from the B2 endpoint and bucket name. Handle pagination using the ContinuationToken if the bucket has more objects than maxKeys.

Paste this in Bolt.new chat

app/api/storage/files/route.ts
1// app/api/storage/files/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { S3Client, ListObjectsV2Command } from '@aws-sdk/client-s3';
4
5const s3 = new S3Client({
6 endpoint: process.env.B2_ENDPOINT,
7 region: process.env.B2_REGION || 'us-west-004',
8 credentials: {
9 accessKeyId: process.env.B2_KEY_ID!,
10 secretAccessKey: process.env.B2_APP_KEY!,
11 },
12 forcePathStyle: true,
13});
14
15export async function GET(request: NextRequest) {
16 const { searchParams } = new URL(request.url);
17 const prefix = searchParams.get('prefix') || '';
18 const maxKeys = parseInt(searchParams.get('maxKeys') || '50', 10);
19 const continuationToken = searchParams.get('continuationToken') || undefined;
20
21 const bucketName = process.env.B2_BUCKET_NAME;
22 const endpoint = process.env.B2_ENDPOINT;
23
24 if (!bucketName || !endpoint) {
25 return NextResponse.json({ error: 'B2 not configured' }, { status: 500 });
26 }
27
28 try {
29 const command = new ListObjectsV2Command({
30 Bucket: bucketName,
31 Prefix: prefix,
32 MaxKeys: maxKeys,
33 ContinuationToken: continuationToken,
34 });
35
36 const response = await s3.send(command);
37 const endpointHost = new URL(endpoint).host;
38
39 const files = (response.Contents || []).map((obj) => ({
40 key: obj.Key,
41 size: obj.Size,
42 lastModified: obj.LastModified,
43 publicUrl: `https://${endpointHost}/${bucketName}/${obj.Key}`,
44 }));
45
46 return NextResponse.json({
47 files,
48 nextContinuationToken: response.NextContinuationToken || null,
49 isTruncated: response.IsTruncated || false,
50 });
51 } catch (error) {
52 const message = error instanceof Error ? error.message : 'Unknown error';
53 return NextResponse.json({ error: message }, { status: 500 });
54 }
55}

Pro tip: Add B2_ENDPOINT, B2_REGION, B2_KEY_ID, B2_APP_KEY, and B2_BUCKET_NAME to Netlify's environment variables and trigger a redeploy. After deploying, update the bucket CORS rules to restrict allowedOrigins to your Netlify URL instead of the wildcard for production security.

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.

Common 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.

Bolt.new Prompt

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.

Copy this prompt to try it in Bolt.new

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.

Bolt.new Prompt

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.

Copy this prompt to try it in Bolt.new

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.

Bolt.new Prompt

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.

Copy this prompt to try it in Bolt.new

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.

typescript
1const s3 = new S3Client({
2 endpoint: process.env.B2_ENDPOINT,
3 region: process.env.B2_REGION,
4 credentials: {
5 accessKeyId: process.env.B2_KEY_ID!,
6 secretAccessKey: process.env.B2_APP_KEY!,
7 },
8 forcePathStyle: true, // REQUIRED for Backblaze B2
9});

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.

typescript
1// For private bucket: generate download URL
2import { GetObjectCommand } from '@aws-sdk/client-s3';
3import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
4
5const downloadCommand = new GetObjectCommand({
6 Bucket: process.env.B2_BUCKET_NAME!,
7 Key: fileKey,
8});
9const 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.

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

Alternatives

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.

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.