Cloud storage and access · from our own builds

AWS S3 uploads: presigned URLs, multipart uploads and private media pipelines

An S3 upload integration comes down to two decisions: whether files go straight from the browser or phone to S3 on a presigned URL or through your server, and how private files are read back. We use both: Storafleet presigns single PUTs and multipart parts straight to the customer's bucket, while Pakka, LeafRanger and BogarInfra take photos through the server with their metadata stripped and hand back signed URLs. The failures we hit were rarely in the upload itself, but in a bucket policy that made private card photos public, a key check a crafted path slipped past and a region default that broke every bucket outside us-east-1.

Updated 20 October 2026 · checked against our code and the vendors' documentation

Where have we built this?

  1. StorafleetUploads from the browser straight into the customer's own bucket through Uppy: a presigned PUT that signs the file size for files up to 100 MiB, presigned multipart above that, and a HeadObject check after every upload.
  2. BogarInfraPresigned PUTs valid for 5 minutes for public project, gallery and progress photos; visiting-card photos sent through the server into a private cards/ prefix and read with 10-minute signed URLs.
  3. LeafRangerUploads received through the server with busboy; photos re-encoded to a 1,600 px JPEG with all metadata dropped, stored in a private bucket and shown through presigned reads.
  4. PakkaProof-of-delivery photos and driver documents uploaded through the backend into a bucket with all public access blocked, read back with 15-minute signed URLs because the documents are personal data.

What are the numbers and limits to know?

Single PUT limit
Up to 5 GB in one PUT with the SDKs, CLI or REST APIsource, checked 20 October 2026
Multipart limits
Maximum object 48.8 TiB; up to 10,000 parts; parts of 5 MiB to 5 GiB, last part any size; consider multipart from 100 MBsource, checked 20 October 2026
Presigned URL lifetime
1 minute to 12 hours from the console; up to 7 days from the SDKs or CLI with IAM user credentials; a URL signed with temporary credentials dies when they expiresource, checked 20 October 2026
Presigned URLs are bearer tokens
Anyone holding one can use it until it expires; AWS suggests limits such as the s3:signatureAge condition in a bucket policysource, checked 20 October 2026
Incomplete multipart uploads
Billed for all storage, bandwidth and requests until completed or stopped; AWS recommends a lifecycle rule with AbortIncompleteMultipartUploadsource, checked 20 October 2026
PutBucketCors
Sets the bucket's CORS configuration; if one exists, S3 replaces itsource, checked 20 October 2026
S3 Standard storage, first 50 TB
$0.023 per GB-month in US East (N. Virginia); $0.025 in Asia Pacific (Mumbai)source, checked 20 October 2026
S3 Standard GET requests
$0.0004 per 1,000; the first 100 GB a month of data transfer out to the internet is free, aggregated across AWS servicessource, checked 20 October 2026

How do you integrate AWS S3 uploads, step by step?

  1. 1

    Choose the path per kind of file

    Large files, or files going into a customer's own bucket, go straight to S3 on a presigned URL. Small photos you must inspect, resize or keep private go through your server. Pakka chose server uploads for central validation; BogarInfra uses presigned PUTs for public photos and the server for private card photos.

  2. 2

    Keep the bucket private and list the public parts

    Keep the bucket private by default. If some folders must be public, as BogarInfra's project galleries are, grant public read on those prefixes by name, so any folder added later starts private.

  3. 3

    Presign narrowly

    The server builds the key and signs a short-lived URL: 5 minutes in BogarInfra, 15 in Storafleet. Storafleet signs the Content-Length header, so the browser cannot send a larger file than it declared, and, when the user turns overwriting off, adds If-None-Match: * where the provider supports it, so the upload cannot replace an existing file.

  4. 4

    Switch to multipart for large files

    Above 100 MiB, Storafleet creates a multipart upload, presigns each part, lists parts to resume, completes with every part's ETag in order, and aborts on cancel.

  5. 5

    Set CORS for browser uploads and expose ETag

    A browser upload needs a CORS rule on the bucket for your origin, and multipart needs ExposeHeaders: ETag or the browser cannot read the part ETags it must send back. When CORS is missing, Storafleet falls back to uploading through its server.

  6. 6

    Verify after upload

    Storafleet runs HeadObject on every finished upload to check what landed and deletes the object if the check fails.

  7. 7

    Process photos on the server

    LeafRanger and BogarInfra rotate each photo from its EXIF orientation, resize it to 1,600 px on the long edge and re-encode it as JPEG, which drops all metadata, GPS included. Photos taken inside homes or offices should never keep their location.

  8. 8

    Read private files through short-lived signed URLs

    Pakka signs reads for 15 minutes and BogarInfra for 10. LeafRanger signs display URLs for 7 days, the SigV4 maximum, and reuses them for 6, storing the canonical object URL rather than the signed one.

  9. 9

    Clean up incomplete multipart uploads

    Storafleet's cleaner lists uploads stuck for more than 24 hours, prices their parts, aborts them on request, and can add a lifecycle rule that aborts anything older than 7 days, merged with the bucket's existing rules.

  10. 10

    Decide what happens when storage is down

    Pakka's storage health check degrades after two outage-class failures. While degraded, the server, never the driver, waives the delivery photo and makes the delivery code mandatory, so deliveries keep moving.

What went wrong, and how was it fixed?

A public-read rule that covered private photos

BogarInfra's first live check found visiting-card photos readable without signing in: an anonymous GET returned 200, because the bucket policy allowed s3:GetObject to everyone on every key.

Fix: Fixed on 12 September 2026: the policy now names the three genuinely public prefixes instead, so any private prefix added later is private by default. Anonymous reads of card photos return 403, and signed reads still work.

A key check a crafted path slipped past

Pakka's support attachments were checked with a prefix test, which accepted support/../drivers/<id>/documents/license/1.jpg. That check was the only thing between an attachment and a signed URL for someone's licence.

Fix: The key must now match an exact shape inside the caller's own support folder. The probe that found it had the prediction written down first.

Every bucket outside us-east-1 failed to connect

Storafleet's connect form defaulted to us-east-1, so buckets in any other region landed in an error state, and the user had to know the right region to fix it.

Fix: S3's redirect already names the bucket's region. Storafleet now reads it from the redirect, or from GetBucketLocation, saves it and retests, so existing broken connections heal on the next test.

Parts nobody can see, billed every month

Multipart uploads that never complete leave parts that do not show in a bucket browser but are billed until aborted. Buckets customers brought to Storafleet had no rule to clean them up.

Fix: Shipped in June 2026: a per-bucket scan that prices stuck uploads, aborts one or all, and offers a 7-day abort rule merged with existing lifecycle rules. The click-through check in the app is still pending.

A check that read the wrong setting

Three Pakka uploads reported as blocked by S3 had never been blocked: the check read env.aws.bucket instead of env.aws.s3Bucket, so it was testing a setting the upload code never uses.

Fix: Found in the 27 August 2026 sweep of all 216 API routes. Point every check at the setting the code actually reads, and make the check able to fail.

Image processing that fails open

If LeafRanger's image step throws, the original bytes are uploaded, metadata included. BogarInfra's card reader refuses an image it cannot decode instead.

Fix: Open in LeafRanger: for photos that may carry a location, fail closed as BogarInfra does.

What should you check before going live?

  • Bucket private by default; public prefixes named one by one
  • Server builds every key; reads check the exact key shape and owner
  • Presigned PUTs sign Content-Length and expire in minutes
  • Browser multipart: CORS rule exposes ETag
  • Existing CORS and lifecycle rules merged, not replaced
  • Lifecycle rule aborts incomplete multipart uploads
  • Each finished upload checked with HeadObject
  • Photos re-encoded with metadata stripped before storage
  • Signed URL lifetime fits the credentials that signed it
  • Bucket region read from S3, not assumed

Questions about AWS S3 uploads

Should files upload straight to S3 or through our server?

It depends on the file. We send large files, and files going into a customer's own bucket, straight to S3 on presigned URLs, as Storafleet does. Photos we must resize, strip of location data or keep private go through the server, as in Pakka, LeafRanger and BogarInfra.

How long should a presigned S3 URL last?

Minutes for uploads: ours are 5 to 15. Reads can run longer: LeafRanger signs display URLs for 7 days, the maximum with IAM user credentials. A URL signed with temporary role credentials stops working when the role session ends, whatever expiry it was given.

Do presigned uploads need CORS on the bucket?

Uploads from a browser do: the bucket must allow your origin, and multipart uploads also need the ETag header exposed. Storafleet offers to set this in one click, though that click still replaces any CORS rules already on the bucket, an open fix in our own product; when CORS is missing it falls back to uploading through its server.

How do you keep private uploads private?

Keep the bucket private by default, name any public prefixes one by one, build every key on the server, check the exact key shape before signing a read, and keep signed URLs short. BogarInfra's card photos were readable until the policy stopped granting public read on every key.

What does S3 charge for an app's photo uploads?

S3 Standard storage is $0.023 per GB-month in US East and $0.025 in Mumbai for the first 50 TB, and GET requests are $0.0004 per 1,000. Unfinished multipart uploads are billed too until they are aborted, and data transfer out beyond the free 100 GB a month is charged separately.

How big a file can a presigned upload take?

A single PUT takes up to 5 GB. Beyond that you need multipart, up to 10,000 parts of 5 MiB to 5 GiB each. Storafleet switches to multipart above 100 MiB and lets Uppy size the parts, but its multipart path still applies the 5 GiB single-PUT size check by default, an open fix.

Need AWS S3 uploads in your app?

We have built it in production code, including the parts that went wrong. Tell us what your app needs and we'll say what it takes.

Ask about this integration