Media Storage
Sitepins requires an S3-compatible object storage bucket for application-level assets. Before configuring your bucket, it is essential to understand the architectural distinction between application media and website content media.
Two Different Kinds of Media
| Kind | Concrete Examples | Storage Destination | Why? |
|---|---|---|---|
| Website Content Media | Blog post images, illustrations, PDF downloads, hero banners | Your Git Repository | Versioned alongside your Markdown and MDX documents. When you clone or build your static site generator, your images are right there. |
| Application UI Assets | User profile pictures (avatars), organization thumbnails, project cover images | The S3 Bucket | These belong to the CMS interface, not to any individual website repository. |
Because website media is stored directly in your Git repository, a misconfigured S3 bucket will never break your website content or images. Only user avatars and project cover cards rely on the bucket.
Supported S3 Providers
Any storage service implementing the standard S3 API is supported:
- AWS S3
- Cloudflare R2 (Zero egress fees)
- MinIO (Self-hosted on your own hardware)
- DigitalOcean Spaces
- Backblaze B2
Provider Setup Examples
1. Amazon Web Services (AWS S3)
In api/.env:
S3_ENDPOINT="https://s3.us-east-1.amazonaws.com"
S3_REGION="us-east-1"
S3_ACCESS_KEY="AKIAIOSFODNN7EXAMPLE"
S3_SECRET_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
S3_BUCKET_NAME="my-sitepins-assets"
S3_FORCE_PATH_STYLE=falseIn app/.env:
NEXT_PUBLIC_BUCKET_URL="https://my-sitepins-assets.s3.us-east-1.amazonaws.com"2. Cloudflare R2
Cloudflare R2 requires setting S3_REGION="auto" and using your unique account endpoint:
In api/.env:
S3_ENDPOINT="https://<ACCOUNT_ID>.r2.cloudflarestorage.com"
S3_REGION="auto"
S3_ACCESS_KEY="<R2_ACCESS_KEY_ID>"
S3_SECRET_KEY="<R2_SECRET_ACCESS_KEY>"
S3_BUCKET_NAME="sitepins-assets"
S3_FORCE_PATH_STYLE=falseIn app/.env (use your R2 public bucket URL or custom domain):
NEXT_PUBLIC_BUCKET_URL="https://pub-<hash>.r2.dev"
# or with a custom domain:
# NEXT_PUBLIC_BUCKET_URL="https://assets.example.com"3. MinIO (Self-Hosted)
MinIO gateways require path-style access (http://endpoint/bucket instead of http://bucket.endpoint):
In api/.env:
S3_ENDPOINT="http://minio:9000"
S3_REGION="us-east-1"
S3_ACCESS_KEY="minioadmin"
S3_SECRET_KEY="minioadmin"
S3_BUCKET_NAME="sitepins-assets"
S3_FORCE_PATH_STYLE=trueIn app/.env (publicly accessible MinIO URL):
NEXT_PUBLIC_BUCKET_URL="http://localhost:9000/sitepins-assets"4. DigitalOcean Spaces
In api/.env:
S3_ENDPOINT="https://sgp1.digitaloceanspaces.com"
S3_REGION="sgp1"
S3_ACCESS_KEY="<DO_SPACES_KEY>"
S3_SECRET_KEY="<DO_SPACES_SECRET>"
S3_BUCKET_NAME="my-spaces-bucket"
S3_FORCE_PATH_STYLE=falseIn app/.env:
NEXT_PUBLIC_BUCKET_URL="https://my-spaces-bucket.sgp1.digitaloceanspaces.com"Existing setups configured with legacy variables (DOS_PUBLIC_ACCESS_KEY, DOS_PUBLIC_SECRET_KEY, DOS_BUCKET_NAME, and DOS_REGION) remain backward-compatible and auto-derive endpoints automatically.
Bucket CORS Configuration (Mandatory)
Browsers upload assets directly to your storage bucket or display them in image components. You must configure CORS on your bucket to allow requests from your web app origin.
AWS S3 / MinIO CORS JSON:
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "PUT", "POST", "HEAD"],
"AllowedOrigins": [
"http://localhost:3000",
"https://cms.example.com"
],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3000
}
]DigitalOcean Spaces / S3 XML format:
<CORSConfiguration>
<CORSRule>
<AllowedOrigin>http://localhost:3000</AllowedOrigin>
<AllowedOrigin>https://cms.example.com</AllowedOrigin>
<AllowedMethod>GET</AllowedMethod>
<AllowedMethod>PUT</AllowedMethod>
<AllowedMethod>POST</AllowedMethod>
<AllowedMethod>HEAD</AllowedMethod>
<AllowedHeader>*</AllowedHeader>
<ExposeHeader>ETag</ExposeHeader>
<MaxAgeSeconds>3000</MaxAgeSeconds>
</CORSRule>
</CORSConfiguration>Verifying Your Configuration
- Log in to Sitepins and navigate to Account β Profile & Security.
- Click Upload Display Picture and select an image.
- If the upload succeeds and your avatar displays immediately:
- Your
S3_*credentials on the API are working. - Your
NEXT_PUBLIC_BUCKET_URLon the Web app is correctly deriving the image host.
- Your
- Troubleshooting:
- Upload succeeds, but image is broken:
NEXT_PUBLIC_BUCKET_URLdoes not point to the correct public bucket origin, or the bucket permissions are not publicly readable. - Upload fails with a browser CORS error: Update your bucketβs CORS rules to include your web app origin.
- MinIO signature failure: Ensure
S3_FORCE_PATH_STYLE=true.
- Upload succeeds, but image is broken: