# How to Integrate Bolt.new with AWS S3

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

## TL;DR

To integrate AWS S3 with Bolt.new, install @aws-sdk/client-s3 (pure JavaScript, works in WebContainers) and generate pre-signed URLs through a Next.js API route or Supabase Edge Function. Store your AWS credentials in the .env file. The S3 client communicates over HTTPS, bypassing WebContainer's TCP limitation. Configure bucket CORS to allow StackBlitz origins for development.

## Solving Bolt's Ephemeral Storage Problem with AWS S3

Bolt.new runs entirely inside a browser tab using StackBlitz's WebContainer technology. The in-memory file system means any file uploaded to your app — profile photos, documents, generated assets — disappears the moment the user refreshes the page. AWS S3 solves this by providing persistent, durable object storage accessible over HTTPS, which is the only external protocol WebContainers support. Unlike raw TCP-based storage systems, S3's HTTP API works seamlessly from both the WebContainer during development and your deployed server.

The @aws-sdk/client-s3 package is written in pure JavaScript without any native C++ bindings, making it one of the few AWS services that works directly inside Bolt's runtime. The recommended integration pattern uses pre-signed URLs: your API route generates a time-limited signed URL that authorizes the browser to upload directly to S3, without routing the file data through your server. This reduces latency, saves bandwidth costs, and eliminates server-side file buffering — an especially important consideration when your API routes are serverless functions with limited memory.

For developers who also need a NoSQL database alongside file storage, the @aws-sdk/client-dynamodb package uses the same HTTP-based architecture and works in WebContainers as well. You can use DynamoDB to store metadata about S3 objects (file names, user associations, upload timestamps) while S3 handles the binary data itself. Both services share the same AWS credentials, simplifying your environment variable configuration.

## Before you start

- An AWS account with an IAM user that has S3 permissions (AmazonS3FullAccess for development, scoped policies for production)
- An S3 bucket created in your desired AWS region with a name that matches your app
- Your AWS Access Key ID and Secret Access Key from the IAM console
- A Bolt.new project using Next.js (for API routes) or Vite with Supabase (for Edge Functions)
- Basic familiarity with environment variables and API routes

## Step-by-step guide

### 1. Create an S3 Bucket and Configure CORS

Before writing any code, you need an S3 bucket configured to accept uploads from Bolt's WebContainer URLs. Log into the AWS Console, navigate to S3, and create a new bucket. Choose a region close to your users and uncheck 'Block all public access' if you need publicly readable files (for images), or leave it checked for private documents. After creating the bucket, click on it, go to the Permissions tab, and scroll to Cross-origin resource sharing (CORS). You must add a CORS configuration that allows Bolt's WebContainer origins. StackBlitz uses URLs like `https://[hash].local.credentialless.webcontainer-api.io` during development and your deployed domain in production. The CORS rule should allow PUT and GET methods from all origins during development (`*`), then be tightened to your specific domain after deployment. Without this CORS configuration, browser-based uploads to S3 will be blocked by the browser's same-origin policy, even though the AWS credentials are valid.

```
[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],
    "AllowedOrigins": ["*"],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3000
  }
]
```

**Expected result:** Your S3 bucket shows 'CORS: Configured' in the Permissions tab, and you've saved your bucket name, region, Access Key ID, and Secret Access Key for the next step.

### 2. Install the AWS SDK and Add Environment Variables

The @aws-sdk/client-s3 and @aws-sdk/s3-request-presigner packages are pure JavaScript with no native dependencies, making them fully compatible with Bolt's WebContainer. Prompt Bolt to install these packages and set up the environment variable structure. You'll store your AWS credentials in the .env file at the project root. In a Next.js project, server-side environment variables have no prefix (they're never sent to the browser), while client-safe variables use NEXT_PUBLIC_. Your AWS Secret Access Key must never have a NEXT_PUBLIC_ prefix — it must only be read by your API routes. The Access Key ID and bucket region can be exposed if needed, but it's cleaner to keep all AWS config server-side. After prompting Bolt, manually edit the .env file to replace the placeholder values with your real credentials.

```
# .env file — never commit this to git
AWS_ACCESS_KEY_ID=your_access_key_here
AWS_SECRET_ACCESS_KEY=your_secret_key_here
AWS_REGION=us-east-1
AWS_S3_BUCKET_NAME=your-bucket-name
```

**Expected result:** The .env file exists in your project root with your real AWS credentials. The terminal shows no errors when running npm run dev.

### 3. Create the Pre-Signed URL API Route

The core of the S3 integration is a server-side API route that generates pre-signed URLs. A pre-signed URL is a time-limited URL that grants permission to perform a specific S3 operation (like uploading a file) without exposing your AWS credentials. The client sends the file's content type and desired filename to your API route, the route generates a signed URL using your secret credentials, and returns that URL to the client. The client then uploads directly to S3 using a simple PUT request. This approach has two major advantages: your AWS Secret Access Key never reaches the browser, and file data doesn't pass through your server (reducing costs and latency). The pre-signed URL expires after a configurable duration (typically 15 minutes), preventing abuse even if a URL is intercepted.

```
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { NextResponse } from 'next/server';

const s3Client = new S3Client({
  region: process.env.AWS_REGION!,
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
  },
});

export async function POST(request: Request) {
  try {
    const { fileName, fileType } = await request.json();

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

    // Create a unique key to avoid filename collisions
    const key = `uploads/${Date.now()}-${fileName.replace(/[^a-zA-Z0-9.-]/g, '_')}`;

    const command = new PutObjectCommand({
      Bucket: process.env.AWS_S3_BUCKET_NAME!,
      Key: key,
      ContentType: fileType,
    });

    const signedUrl = await getSignedUrl(s3Client, command, {
      expiresIn: 900, // 15 minutes
    });

    const objectUrl = `https://${process.env.AWS_S3_BUCKET_NAME}.s3.${process.env.AWS_REGION}.amazonaws.com/${key}`;

    return NextResponse.json({ signedUrl, objectUrl, key });
  } catch (error) {
    console.error('Error generating signed URL:', error);
    return NextResponse.json(
      { error: 'Failed to generate upload URL' },
      { status: 500 }
    );
  }
}
```

**Expected result:** A POST request to /api/upload-url with {fileName: 'test.jpg', fileType: 'image/jpeg'} returns a JSON object containing signedUrl and objectUrl.

### 4. Build the File Upload React Component

Now prompt Bolt to create the upload UI component that uses the pre-signed URL. The upload flow has two HTTP requests: first to your API route to get the signed URL, then directly to S3 to upload the file. The second request goes browser-to-S3 using the signed URL — your server is not involved in the file transfer itself. The component should show upload progress using the XMLHttpRequest API (which supports progress events, unlike fetch), handle file size limits, display success and error states, and return the permanent S3 URL for saving to your database. This component works during Bolt development and in production without any changes.

```
import { useState, useRef } from 'react';

interface FileUploadProps {
  accept?: string;
  maxSizeMB?: number;
  onUploadComplete?: (url: string) => void;
}

export function FileUpload({
  accept = '*/*',
  maxSizeMB = 10,
  onUploadComplete,
}: FileUploadProps) {
  const [uploading, setUploading] = useState(false);
  const [progress, setProgress] = useState(0);
  const [uploadedUrl, setUploadedUrl] = useState<string | null>(null);
  const [error, setError] = useState<string | null>(null);
  const fileInputRef = useRef<HTMLInputElement>(null);

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

    setUploading(true);
    setProgress(0);
    setError(null);

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

      if (!urlResponse.ok) throw new Error('Failed to get upload URL');
      const { signedUrl, objectUrl } = await urlResponse.json();

      // Step 2: Upload directly to S3 with progress tracking
      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) resolve();
          else reject(new Error(`Upload failed: ${xhr.status}`));
        };
        xhr.onerror = () => reject(new Error('Upload failed'));
        xhr.open('PUT', signedUrl);
        xhr.setRequestHeader('Content-Type', file.type);
        xhr.send(file);
      });

      setUploadedUrl(objectUrl);
      onUploadComplete?.(objectUrl);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Upload failed');
    } finally {
      setUploading(false);
    }
  };

  return (
    <div className="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center">
      <input
        ref={fileInputRef}
        type="file"
        accept={accept}
        className="hidden"
        onChange={(e) => e.target.files?.[0] && uploadFile(e.target.files[0])}
      />
      {!uploading && !uploadedUrl && (
        <button
          onClick={() => fileInputRef.current?.click()}
          className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
        >
          Choose File (max {maxSizeMB}MB)
        </button>
      )}
      {uploading && (
        <div>
          <div className="bg-gray-200 rounded-full h-2 mt-2">
            <div
              className="bg-blue-600 h-2 rounded-full transition-all"
              style={{ width: `${progress}%` }}
            />
          </div>
          <p className="mt-1 text-sm text-gray-500">{progress}% uploaded</p>
        </div>
      )}
      {uploadedUrl && (
        <p className="text-green-600 text-sm">Upload complete!</p>
      )}
      {error && <p className="text-red-600 text-sm mt-2">{error}</p>}
    </div>
  );
}
```

**Expected result:** A styled upload dropzone renders on the page. Selecting a file triggers the two-step upload process and shows a progress bar. After completion, the component displays a success message and fires the onUploadComplete callback with the S3 URL.

### 5. Deploy and Update Environment Variables

During development in Bolt's WebContainer, the API route at /api/upload-url runs server-side within the WebContainer runtime and reads credentials from your .env file. This works correctly for testing the upload flow. However, incoming webhooks from S3 (like S3 Event Notifications) cannot reach the WebContainer — Bolt's browser-based runtime has no public URL that S3 can call. If you need S3 event notifications (e.g., to trigger processing when a file is uploaded), you must deploy first. For Netlify deployment, push your code through Bolt's GitHub integration, then add your four AWS environment variables in Netlify's dashboard under Site Settings → Environment Variables. For Vercel, add them in Project Settings → Environment Variables. The API routes become serverless functions that have full access to the environment variables you configure. Remember that the CORS configuration on your S3 bucket should be updated to allow only your deployed domain rather than the wildcard (*) you used during development.

**Expected result:** Your deployed app on Netlify or Vercel successfully uploads files to S3. The environment variables panel in your hosting dashboard shows all four AWS variables. S3 event notifications (if needed) are registered with your deployed domain URL.

## Best practices

- Always generate pre-signed URLs server-side in an API route — never expose your AWS Secret Access Key to the browser or include it in client-side code
- Set short pre-signed URL expiry times (5-15 minutes) to limit the window during which an intercepted URL could be misused
- Sanitize file names before using them as S3 keys — replace spaces and special characters to avoid URL encoding issues and S3 key validation errors
- Use unique key prefixes (e.g., include a timestamp or UUID) to prevent filename collisions when multiple users upload files with the same name
- Restrict IAM permissions to the minimum necessary: the IAM user for your app only needs s3:PutObject, s3:GetObject, and s3:DeleteObject on your specific bucket
- Configure S3 lifecycle rules to automatically delete temporary or unfinished upload files after a set period, reducing storage costs
- After deployment, tighten your S3 bucket CORS configuration to only allow your specific production domain instead of the wildcard (*) used during development
- Store only the S3 object key (not the full URL) in your database — construct URLs programmatically so you can change bucket regions without a database migration

## Use cases

### User Profile Photo Uploads

Allow users to upload profile photos that persist across sessions and devices. The photo uploads directly from the browser to S3 using a pre-signed URL, then the public S3 URL is saved to your database alongside the user record. Profile photos appear immediately and remain available indefinitely.

Prompt example:

```
Add a profile photo upload feature. When a user clicks 'Change Photo', show a file picker that accepts JPG and PNG under 5MB. Generate a pre-signed S3 upload URL from an API route at /api/upload-url, upload the file directly to S3 from the browser, then save the resulting S3 URL to the user's profile in the database. Show a loading spinner during upload and display the new photo immediately after success.
```

### Document Management System

Build a document storage feature where users can upload PDFs, Word docs, and spreadsheets. S3 stores the actual files while your database tracks metadata like file name, size, upload date, and which user owns it. Generate signed download URLs on demand so only authorized users can access files.

Prompt example:

```
Create a document upload and management page. Users can upload files up to 50MB. Store files in S3 using pre-signed upload URLs generated by a /api/s3-upload-url API route. Save file metadata (name, size, S3 key, uploadedAt, userId) to the database. Show a file list with download buttons that generate fresh pre-signed download URLs from /api/s3-download-url. Include a delete button that removes the file from both S3 and the database.
```

### Image Gallery with CDN Delivery

Create a public image gallery where uploaded images are served directly from S3 or through CloudFront CDN. This is ideal for portfolio sites, product catalogs, or any app needing fast global image delivery. Images upload via pre-signed URLs and display using their permanent S3 public URL.

Prompt example:

```
Build an image gallery where users can upload images that appear in a responsive grid. Use S3 for storage with pre-signed URLs for uploads. Make the S3 bucket serve images publicly so they load directly from S3 URLs without authentication. The gallery should show upload progress, support drag-and-drop, and let users delete images (which removes them from both S3 and the database).
```

## Troubleshooting

### CORS error when uploading: 'Access to XMLHttpRequest at S3 URL from origin has been blocked'

Cause: The S3 bucket's CORS configuration doesn't include the origin where your app is running. During development, Bolt uses WebContainer URLs like *.webcontainer-api.io that need to be allowed.

Solution: Go to your S3 bucket → Permissions → CORS configuration and add a rule with AllowedOrigins: ['*'] for development. After deployment, update it to your specific domain. Make sure AllowedMethods includes 'PUT' since pre-signed uploads use the PUT method.

```
[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],
    "AllowedOrigins": ["*"],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3000
  }
]
```

### API route returns 500 with 'InvalidClientTokenId' or 'The security token included in the request is invalid'

Cause: The AWS Access Key ID or Secret Access Key in your .env file is incorrect, has been deactivated, or the environment variables aren't being read properly.

Solution: Verify your credentials in the AWS IAM console under Security credentials. Make sure your .env variable names exactly match what the code reads (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY). Restart the dev server after editing .env since Next.js caches environment variables at startup.

### Pre-signed URL works but upload fails with '403 Forbidden' from S3

Cause: The pre-signed URL was generated for a specific ContentType, but the PUT request is sending a different or missing Content-Type header. S3 validates that the actual upload matches what was signed.

Solution: Ensure the xhr.setRequestHeader('Content-Type', file.type) line in your upload code sends exactly the same MIME type that was passed to the API route. If file.type is empty (some browsers don't detect file types), pass a fallback: file.type || 'application/octet-stream'.

```
xhr.setRequestHeader('Content-Type', file.type || 'application/octet-stream');
```

### S3 Event Notifications or webhooks never arrive during Bolt development

Cause: Bolt's WebContainer runtime runs inside a browser tab and has no public IP address. S3 cannot make outbound HTTP calls to your WebContainer — it has no externally reachable URL to send events to.

Solution: This is a fundamental WebContainer limitation. Deploy your app to Netlify or Bolt Cloud first, then register S3 Event Notification endpoints using your deployed URL (e.g., https://your-app.netlify.app/api/s3-webhook). Use the deployed environment for all webhook testing.

## Frequently asked questions

### Does @aws-sdk/client-s3 work inside Bolt's WebContainer?

Yes. The @aws-sdk/client-s3 package is written in pure JavaScript and communicates exclusively over HTTPS, making it fully compatible with Bolt's WebContainer runtime. It does not use any native Node.js modules that require TCP sockets or C++ compilation. Both the S3 client and the pre-signer package install and run without issues.

### Can I use the full AWS SDK v3 or just the S3 client?

Most AWS SDK v3 modular packages work in WebContainers because they're written in pure JavaScript and use HTTPS. The @aws-sdk/client-dynamodb package for DynamoDB also works, making it a viable NoSQL option alongside S3 storage. However, services requiring TCP connections or native binaries (like certain data streaming services) won't function in the WebContainer during development.

### Why use pre-signed URLs instead of uploading through the API route?

Pre-signed URLs allow the browser to upload directly to S3, bypassing your server entirely. This means large files don't consume your serverless function's memory or execution time, uploads can be faster (direct connection to S3), and your server costs are lower. The signed URL proves the upload is authorized without exposing your AWS credentials to the client.

### How do I receive S3 event notifications in my Bolt app?

S3 event notifications (triggered when files are uploaded, deleted, etc.) require a publicly accessible webhook URL that S3 can call. Bolt's WebContainer has no public URL during development, so you must deploy your app first. After deploying to Netlify or Bolt Cloud, register your /api/s3-webhook endpoint URL in the S3 bucket's Event Notifications settings under Properties.

### Should I make my S3 bucket public or private?

It depends on your use case. For user-generated content like profile photos that display in your app, a public bucket with proper key naming is the simplest approach. For private documents, keep the bucket private and generate pre-signed download URLs (using GetObjectCommand) whenever a user needs to access a file. Never make a bucket containing sensitive documents publicly accessible.

---

Source: https://www.rapidevelopers.com/bolt-ai-integrations/aws-s3
© RapidDev — https://www.rapidevelopers.com/bolt-ai-integrations/aws-s3
