How to Configure File Storage
Framework M supports pluggable file storage backends for handling document attachments and file uploads. This guide explains how to configure each backend.
Choosing a Backend
| Backend | Best for |
|---|---|
local | Local development, single-server "Indie" deployments |
memory | Automated tests |
s3 | Production deployments — works with AWS S3 and any S3-compatible API (MinIO, DigitalOcean Spaces, Backblaze B2, Cloudflare R2, etc.) |
Local Storage
Files are written to a date-organized directory tree on the local filesystem. No external services are required.
[files]
storage_backend = "local"
storage_path = "./uploads" # relative or absolute path
max_upload_size = "10MB"
allowed_extensions = ["pdf", "doc", "docx", "xls", "xlsx", "png", "jpg", "jpeg", "gif", "txt"]
The local adapter writes to <storage_path>/YYYY/MM/DD/<random-prefix>_<filename> using atomic temp-file renames to prevent corruption.
Local storage is not suitable for multi-instance deployments. Use s3 instead.
S3 / S3-Compatible Storage
Step 1 — Set the backend
[files]
storage_backend = "s3"
max_upload_size = "100MB"
allowed_extensions = ["pdf", "png", "jpg", "mp4"]
Step 2 — Provide S3 credentials
Credentials can be set in framework_config.toml or (preferably in production) via environment variables. Environment variables override TOML values.
Via framework_config.toml
[files]
storage_backend = "s3"
s3_bucket = "my-app-files"
s3_region = "us-east-1"
# Optional — omit for IAM role / instance profile auth
s3_access_key = "AKIA..."
s3_secret_key = "wJalrX..."
# Optional — only for S3-compatible services
s3_endpoint = "https://s3.example.com"
Via environment variables
FRAMEWORK_M_S3_BUCKET=my-app-files
FRAMEWORK_M_S3_REGION=us-east-1
FRAMEWORK_M_S3_ACCESS_KEY=AKIA...
FRAMEWORK_M_S3_SECRET_KEY=wJalrX...
FRAMEWORK_M_S3_ENDPOINT=https://s3.example.com # S3-compatible only
Common Deployment Patterns
AWS with IAM Role (recommended for EC2 / ECS / EKS)
IAM roles eliminate static credentials entirely. boto3 picks up the role automatically:
[files]
storage_backend = "s3"
s3_bucket = "my-app-files"
s3_region = "us-east-1"
# No access_key / secret_key needed
MinIO (self-hosted)
[files]
storage_backend = "s3"
s3_bucket = "my-app-files"
s3_region = "us-east-1" # arbitrary, required by boto3
s3_endpoint = "https://minio.internal:9000"
s3_access_key = "minioadmin"
s3_secret_key = "minioadmin"
Cloudflare R2
[files]
storage_backend = "s3"
s3_bucket = "my-app-files"
s3_region = "auto"
s3_endpoint = "https://<account-id>.r2.cloudflarestorage.com"
s3_access_key = "..."
s3_secret_key = "..."
Presigned Uploads (large files, S3 only)
For files that exceed memory-safe limits, Framework M supports presigned uploads where the browser sends bytes directly to S3, bypassing the server.
POST /api/v1/file/upload/presign → { upload_url, upload_id, part_urls, key }
PUT <presigned_url> (browser → S3 directly)
POST /api/v1/file/upload/presign/complete → { key, file_url }
The /presign/complete endpoint verifies the upload server-side using HeadObject before confirming success. See File Storage Architecture for the full design rationale.
/upload/presign returns HTTP 400 when storage_backend is not s3.
Testing
Use the in-memory backend in tests — no filesystem or S3 required:
[files]
storage_backend = "memory"
Or override via mock in pytest:
with patch("framework_m_standard.adapters.web.file_upload_routes.get_file_config") as mock:
mock.return_value = {
"storage_backend": "memory",
"max_upload_size": 10 * 1024 * 1024,
"allowed_extensions": ["txt", "pdf"],
"storage_path": "./uploads",
}
...