> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/pocketbase/pocketbase/llms.txt
> Use this file to discover all available pages before exploring further.

# S3 storage configuration

> Configure PocketBase to use S3-compatible storage for files

PocketBase supports S3-compatible storage backends as an alternative to local filesystem storage. This includes AWS S3, MinIO, DigitalOcean Spaces, Cloudflare R2, and other S3-compatible services.

## Why use S3 storage

S3-compatible storage provides several advantages:

* **Scalability** - Handle unlimited file storage without disk space concerns
* **Reliability** - Built-in redundancy and durability (99.999999999% for AWS S3)
* **Performance** - Geographic distribution and CDN integration
* **Cost efficiency** - Pay only for what you use, with automatic scaling
* **Backups** - Easy replication across regions

## Storage backend selection

PocketBase automatically selects the storage backend based on your settings:

```go theme={null}
// From core/base.go:714
func (app *BaseApp) NewFilesystem() (*filesystem.System, error) {
    if app.settings != nil && app.settings.S3.Enabled {
        return filesystem.NewS3(
            app.settings.S3.Bucket,
            app.settings.S3.Region,
            app.settings.S3.Endpoint,
            app.settings.S3.AccessKey,
            app.settings.S3.Secret,
            app.settings.S3.ForcePathStyle,
        )
    }
    
    // Fallback to local filesystem
    return filesystem.NewLocal(filepath.Join(app.DataDir(), LocalStorageDirName))
}
```

## Configuration via Admin UI

The easiest way to configure S3 storage is through the PocketBase Admin UI:

<Steps>
  <Step title="Navigate to Settings">
    Go to Settings > Files storage in the Admin UI
  </Step>

  <Step title="Enable S3 storage">
    Toggle "Use S3 storage" to enable it
  </Step>

  <Step title="Enter S3 credentials">
    Fill in your S3 bucket configuration:

    * Bucket name
    * Region
    * Endpoint
    * Access key
    * Secret key
    * Force path style (if required)
  </Step>

  <Step title="Test connection">
    Use the "Test" button to verify your configuration
  </Step>

  <Step title="Save settings">
    Click "Save" to apply the configuration
  </Step>
</Steps>

<Warning>
  Changing from local to S3 storage (or vice versa) does not automatically migrate existing files. You'll need to manually copy files between storage backends.
</Warning>

## Configuration via code

You can also configure S3 storage programmatically:

```go theme={null}
package main

import (
    "github.com/pocketbase/pocketbase"
    "github.com/pocketbase/pocketbase/core"
)

func main() {
    app := pocketbase.New()
    
    app.OnServe().BindFunc(func(e *core.ServeEvent) error {
        // Update S3 settings
        settings, err := e.App.Settings().Clone()
        if err != nil {
            return err
        }
        
        settings.S3.Enabled = true
        settings.S3.Bucket = "my-bucket"
        settings.S3.Region = "us-east-1"
        settings.S3.Endpoint = "https://s3.amazonaws.com"
        settings.S3.AccessKey = "YOUR_ACCESS_KEY"
        settings.S3.Secret = "YOUR_SECRET_KEY"
        settings.S3.ForcePathStyle = false
        
        if err := e.App.Settings().Merge(settings); err != nil {
            return err
        }
        
        return e.Next()
    })
    
    if err := app.Start(); err != nil {
        panic(err)
    }
}
```

## Environment variables

For production deployments, use environment variables to avoid hardcoding credentials:

```bash theme={null}
export S3_ENABLED=true
export S3_BUCKET=my-bucket
export S3_REGION=us-east-1
export S3_ENDPOINT=https://s3.amazonaws.com
export S3_ACCESS_KEY=YOUR_ACCESS_KEY
export S3_SECRET_KEY=YOUR_SECRET_KEY
export S3_FORCE_PATH_STYLE=false
```

Then read them in your code:

```go theme={null}
import "os"

settings.S3.Enabled = os.Getenv("S3_ENABLED") == "true"
settings.S3.Bucket = os.Getenv("S3_BUCKET")
settings.S3.Region = os.Getenv("S3_REGION")
settings.S3.Endpoint = os.Getenv("S3_ENDPOINT")
settings.S3.AccessKey = os.Getenv("S3_ACCESS_KEY")
settings.S3.Secret = os.Getenv("S3_SECRET_KEY")
settings.S3.ForcePathStyle = os.Getenv("S3_FORCE_PATH_STYLE") == "true"
```

## Provider-specific configurations

### AWS S3

```go theme={null}
settings.S3.Enabled = true
settings.S3.Bucket = "my-app-files"
settings.S3.Region = "us-east-1"
settings.S3.Endpoint = "https://s3.amazonaws.com"
settings.S3.AccessKey = "AKIA..."
settings.S3.Secret = "your-secret-key"
settings.S3.ForcePathStyle = false
```

<Info>
  For AWS S3, you can omit the endpoint - it will be constructed automatically from the region: `https://s3.{region}.amazonaws.com`
</Info>

### MinIO

```go theme={null}
settings.S3.Enabled = true
settings.S3.Bucket = "pocketbase"
settings.S3.Region = "us-east-1" // MinIO region (can be any value)
settings.S3.Endpoint = "https://minio.example.com"
settings.S3.AccessKey = "minioadmin"
settings.S3.Secret = "minioadmin"
settings.S3.ForcePathStyle = true // MinIO requires path-style URLs
```

### DigitalOcean Spaces

```go theme={null}
settings.S3.Enabled = true
settings.S3.Bucket = "my-space"
settings.S3.Region = "nyc3"
settings.S3.Endpoint = "https://nyc3.digitaloceanspaces.com"
settings.S3.AccessKey = "DO00..."
settings.S3.Secret = "your-secret-key"
settings.S3.ForcePathStyle = false
```

### Cloudflare R2

```go theme={null}
settings.S3.Enabled = true
settings.S3.Bucket = "my-bucket"
settings.S3.Region = "auto"
settings.S3.Endpoint = "https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com"
settings.S3.AccessKey = "your-access-key"
settings.S3.Secret = "your-secret-key"
settings.S3.ForcePathStyle = false
```

### Backblaze B2

```go theme={null}
settings.S3.Enabled = true
settings.S3.Bucket = "my-bucket"
settings.S3.Region = "us-west-002"
settings.S3.Endpoint = "https://s3.us-west-002.backblazeb2.com"
settings.S3.AccessKey = "your-key-id"
settings.S3.Secret = "your-application-key"
settings.S3.ForcePathStyle = false
```

## Path-style vs. virtual-hosted-style URLs

S3 supports two URL styles:

### Virtual-hosted-style (default)

```
https://bucket-name.s3.amazonaws.com/path/to/file.txt
```

Used by AWS S3, DigitalOcean Spaces, and most modern providers.

Set `ForcePathStyle: false` (or omit, as it's the default).

### Path-style

```
https://s3.amazonaws.com/bucket-name/path/to/file.txt
```

Required by MinIO and some self-hosted S3 implementations.

Set `ForcePathStyle: true`.

<Note>
  AWS S3 deprecated path-style URLs for buckets created after September 30, 2020. Use virtual-hosted-style for AWS S3.
</Note>

## Direct S3 filesystem creation

For advanced use cases, you can create S3 filesystem instances directly:

```go theme={null}
import (
    "github.com/pocketbase/pocketbase/tools/filesystem"
)

func createS3Filesystem() (*filesystem.System, error) {
    fsys, err := filesystem.NewS3(
        "my-bucket",              // bucket
        "us-east-1",              // region
        "https://s3.amazonaws.com", // endpoint
        "AKIA...",                // access key
        "your-secret-key",        // secret key
        false,                    // force path style
    )
    if err != nil {
        return nil, err
    }
    
    return fsys, nil
}
```

### Using the S3 filesystem

```go theme={null}
fsys, err := createS3Filesystem()
if err != nil {
    return err
}
defer fsys.Close()

// Upload file
file, _ := filesystem.NewFileFromPath("/path/to/file.txt")
err = fsys.UploadFile(file, "uploads/file.txt")

// Download file
reader, err := fsys.GetReader("uploads/file.txt")
if err != nil {
    return err
}
defer reader.Close()

// List files
files, err := fsys.List("uploads/")
for _, file := range files {
    fmt.Println(file.Key, file.Size, file.ModTime)
}

// Delete file
err = fsys.Delete("uploads/file.txt")

// Copy file
err = fsys.Copy("uploads/file.txt", "backups/file.txt")
```

## S3 implementation details

PocketBase uses a custom S3 implementation (not AWS SDK) for minimal dependencies:

```go theme={null}
// From tools/filesystem/filesystem.go:44
func NewS3(
    bucketName string,
    region string,
    endpoint string,
    accessKey string,
    secretKey string,
    s3ForcePathStyle bool,
) (*System, error) {
    ctx := context.Background()
    
    client := &s3.S3{
        Bucket:       bucketName,
        Region:       region,
        Endpoint:     endpoint,
        AccessKey:    accessKey,
        SecretKey:    secretKey,
        UsePathStyle: s3ForcePathStyle,
    }
    
    drv, err := s3blob.New(client)
    if err != nil {
        return nil, err
    }
    
    return &System{ctx: ctx, bucket: blob.NewBucket(drv)}, nil
}
```

The internal S3 client supports:

* ✅ GetObject - Download files
* ✅ HeadObject - Get file metadata
* ✅ PutObject - Upload files
* ✅ DeleteObject - Delete files
* ✅ CopyObject - Copy files
* ✅ ListObjects - List files with pagination
* ✅ Multipart uploads - For large files
* ✅ AWS Signature V4 - Authentication

## Bucket permissions

Your S3 bucket needs the following permissions:

```json theme={null}
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject",
        "s3:ListBucket",
        "s3:GetObjectAttributes"
      ],
      "Resource": [
        "arn:aws:s3:::my-bucket",
        "arn:aws:s3:::my-bucket/*"
      ]
    }
  ]
}
```

<Warning>
  Never make your bucket publicly readable! PocketBase handles all file access through its API, respecting collection rules and file field protection settings.
</Warning>

## CORS configuration

If you're accessing files directly from the browser, configure CORS on your bucket:

```json theme={null}
[
  {
    "AllowedOrigins": ["https://your-app.com"],
    "AllowedMethods": ["GET", "HEAD"],
    "AllowedHeaders": ["*"],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3600
  }
]
```

<Info>
  CORS is only needed if you're serving files directly from S3. When using PocketBase's API endpoint, CORS is handled by PocketBase.
</Info>

## Migration between storage backends

To migrate files from local to S3 (or vice versa):

```go theme={null}
func migrateStorage(app core.App) error {
    // Create source filesystem (local)
    src, err := filesystem.NewLocal("./pb_data/storage")
    if err != nil {
        return err
    }
    defer src.Close()
    
    // Create destination filesystem (S3)
    dst, err := filesystem.NewS3(
        "my-bucket",
        "us-east-1",
        "https://s3.amazonaws.com",
        "access-key",
        "secret-key",
        false,
    )
    if err != nil {
        return err
    }
    defer dst.Close()
    
    // List all files
    files, err := src.List("")
    if err != nil {
        return err
    }
    
    // Copy each file
    for _, file := range files {
        if file.IsDir {
            continue
        }
        
        // Read from source
        reader, err := src.GetReader(file.Key)
        if err != nil {
            return err
        }
        
        // Read all data
        data, err := io.ReadAll(reader)
        reader.Close()
        if err != nil {
            return err
        }
        
        // Upload to destination
        err = dst.Upload(data, file.Key)
        if err != nil {
            return err
        }
        
        fmt.Printf("Migrated: %s\n", file.Key)
    }
    
    return nil
}
```

## Performance considerations

### Multipart uploads

For files larger than 5MB, PocketBase automatically uses multipart uploads:

```go theme={null}
// Configure multipart upload settings
writerOpts := &blob.WriterOptions{
    BufferSize:     10 << 20,  // 10MB part size
    MaxConcurrency: 5,          // 5 concurrent uploads
}
```

### Concurrent operations

The S3 driver supports concurrent operations:

```go theme={null}
// Concurrent file uploads
var wg sync.WaitGroup
for _, file := range files {
    wg.Add(1)
    go func(f *filesystem.File) {
        defer wg.Done()
        fsys.UploadFile(f, f.Name)
    }(file)
}
wg.Wait()
```

### Caching

PocketBase sets aggressive cache headers for S3 files:

```http theme={null}
Cache-Control: max-age=2592000, stale-while-revalidate=86400
```

This caches files for 30 days while allowing background revalidation.

## Troubleshooting

### Connection errors

```
failed to initialize S3: endpoint must be set
```

Ensure the endpoint URL is set correctly and includes the protocol (`https://`).

### Authentication errors

```
status code 403: Access Denied
```

Verify your access key and secret key. Check that your IAM user/role has the required permissions.

### Path-style errors

```
status code 404: NoSuchBucket
```

Try toggling `ForcePathStyle`. MinIO requires `true`, AWS S3 requires `false`.

### Region errors

```
AuthorizationHeaderMalformed: The authorization header is malformed
```

Ensure the region matches your bucket's region exactly.

## Next steps

<CardGroup cols={2}>
  <Card title="File upload" icon="upload" href="/files/upload">
    Learn how to upload files to S3 storage
  </Card>

  <Card title="File download" icon="download" href="/files/download">
    Understand how files are served from S3
  </Card>
</CardGroup>
