Heroku wipes uploaded files every time the app restarts or you deploy.
Turn on S3 so your product photos live in a cheap, permanent Amazon bucket
instead of on the Heroku dyno.
First-time setup — what to do in AWS
You'll create three things in AWS: a bucket (the folder for photos),
a bucket policy (so shoppers can view the photos), and an
IAM user with an access key (so this app can upload, read, and delete photos).
Pasting the access key into the form below and clicking Test Connection finishes the loop.
Step 1 — Create the S3 bucket
Sign in at console.aws.amazon.com (create a free account if needed).
Open the S3 service and click Create bucket .
Pick a globally unique Bucket name (e.g. my-vintage-shop-photos). Lowercase letters, digits, and dashes only.
Pick an AWS Region close to your shoppers (e.g. us-east-2). Write it down — you'll paste the exact code into Region below.
Under Object Ownership , leave the default ACLs disabled (Bucket owner enforced) . Do not enable ACLs — this app uses a bucket policy instead, and AWS will reject uploads if ACLs are enabled inconsistently.
Under Block Public Access settings for this bucket : keep
"Block public access to buckets and objects granted through new access control lists (ACLs)" and the matching "…through any access control lists (ACLs)" checked,
but uncheck the two policy lines:
"Block public access to buckets and objects granted through new public bucket or access point policies"
and "Block public and cross-account access to buckets and objects through any public bucket or access point policies" .
Acknowledge the warning. This lets shoppers load product photos through your bucket policy without granting any other public access.
Leave the rest of the bucket settings at their defaults and click Create bucket .
Step 2 — Add the bucket policy (public read for /uploads/*)
Open the bucket → Permissions tab → scroll to Bucket policy → click Edit .
Paste the policy below, replacing YOUR-BUCKET-NAME with your actual bucket name:
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "PublicReadForPhotos",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::YOUR-BUCKET-NAME/uploads/*"
}]
}
Save changes. Only objects under uploads/ become public — anything else in the bucket stays private.
Step 3 — Create the IAM user the app will sign in as
Open the IAM service → Users → Create user .
Give it a name like vintage-shop-uploader. Do NOT tick "Provide user access to the AWS Management Console" — this user only needs API access.
On the permissions step, choose Attach policies directly → Create policy → switch to the JSON tab and paste the policy below (replace YOUR-BUCKET-NAME in both ARNs):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListTheBucket",
"Effect": "Allow",
"Action": ["s3:ListBucket"],
"Resource": "arn:aws:s3:::YOUR-BUCKET-NAME"
},
{
"Sid": "ReadWritePhotos",
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:GetObject", "s3:DeleteObject"],
"Resource": "arn:aws:s3:::YOUR-BUCKET-NAME/*"
}
]
}
Note: s3:ListBucket applies to the bucket ARN itself (no /*); the read/write actions apply to the objects inside (/*). A common mistake is using "Resource": "*" — that grants access to every bucket in your account, which you don't want.
Name the policy (e.g. VintageShopUploaderPolicy), save it, then back on the user screen tick that policy and finish creating the user.
Open the new user → Security credentials tab → Create access key → choose Application running outside AWS . AWS will show an Access key ID (starts with AKIA…) and a Secret access key (40 random characters). Copy both — the secret is shown once and cannot be retrieved later.
Step 4 — Fill in the form below and Test Connection
Paste the bucket name, region code, access key ID, and secret access key into the fields below.
Region must be the bare AWS code like us-east-1, us-east-2, us-west-2, eu-west-1. Don't append anything to it — typos like us-east-2-an cause "Could not connect to the endpoint URL" errors.
Leave Custom Endpoint blank for Amazon S3. Only fill it in for S3-compatible services (DigitalOcean Spaces, Cloudflare R2, MinIO).
Click Test Connection . The app does three things behind the scenes:
HeadBucket — proves the credentials work and the region is correct.
PutObject on a tiny test file at uploads/.vintage-connection-test — proves write access.
DeleteObject on that same test file — proves delete access and cleans up.
If the test passes, click Save S3 Settings . New uploads will go straight to your bucket and survive Heroku restarts.
If something goes wrong
"Could not connect to the endpoint URL" — Region is misspelled or has extra characters.
"Bucket not found in region" — Region doesn't match where the bucket was created. Check the bucket's Properties tab in S3.
"Access denied" on HeadBucket / ListBucket — IAM policy is missing s3:ListBucket, or the bucket ARN in the policy is wrong.
"Write test failed (AccessDenied)" — IAM policy is missing s3:PutObject, or the bucket has ACLs enabled (it shouldn't — Object Ownership should stay Bucket owner enforced ).
Every failure is logged under Error Log below with the full AWS error code and stack trace — open the Details on the latest entry to see exactly what AWS said.
How much will this cost? Typically pennies per month for a small shop — AWS charges about $0.023 per GB stored and $0.09 per GB downloaded. A bucket with a few thousand product photos usually runs well under $1/month.
Bucket
The S3 bucket your photos will live in.
Bucket Name
Must match the name you chose in the AWS console exactly.
IAM Credentials
From the IAM user you created. Treat these like passwords.
Access Key ID