Ship files as a feature,
not a storage project.
Every product ends up handling uploads, attachments, documents and exports. Uplint gives your team a complete file layer behind one API — so the feature ships this sprint, and the cloud decisions never end up in product code.
Every feature has files.
Handle them once.
Each of these used to mean another upload path, another bucket convention, another cleanup job. With Uplint every surface is the same call — only the target changes.
await uplint.files.upload({file,storage: "avatars",metadata: { user: "usr_91k" }});The tickets that never
reach your board.
A file layer is a dozen small pieces of infrastructure that every product rebuilds. Take them out of the sprint and what is left is the work your users actually see.
POST /v1/files/v1/files/:id/urlfile recordrouting policy/v1/files/:id/movestorage connectioneventsOne call at launch.
The same call at scale.
Start with one bucket. Add regions, providers and customer-owned storage when a customer asks — in configuration, without touching the code that ships features.
- 01Launch
One bucket, one region.
Connect the S3 bucket you already have. Every upload lands there.
S3production → S3 · ap-south-1product code changed: none - 02First EU customer
Add a routing policy.
EU accounts go to Azure in Frankfurt. Everyone else stays where they are.
AZeu-customers → Azure · westeuropeproduct code changed: none - 03Enterprise deal
Connect their bucket.
The customer issues scoped credentials. Their files never leave their account.
S3acme-corp → S3 · customer-ownedproduct code changed: none - 04Cost review
Move cold files to R2.
Archive files move provider. IDs stay the same, so every link keeps working.
R2archive → R2 · moveproduct code changed: none
A few lines server-side.
Nothing client-side.
Upload from the route you already have, store the file ID next to the record, and hand the client a signed URL when it needs to render or download.
- Node SDK, or plain HTTPS from any language
- The file ID is one string column next to the record
- Buckets stay private; the client only ever sees a signed URL
const file = await uplint.files.upload({file: req.body,storage: "attachments",metadata: { ticket: ticket.id, uploadedBy: user.id }});await db.attachments.insert({ ticketId: ticket.id, fileId: file.id });const { url } = await uplint.files.url(attachment.fileId, { expires: 300 });// signed against whichever provider holds it — today S3, next year anythingStraight answers.
No. Store the file ID as a string on the record it belongs to — the ticket, the user, the invoice. Everything else about the file — size, type, metadata, current location — lives in Uplint’s record and is one GET away.
In your backend route. That keeps authorisation and validation in code you own, and the SDK call is a single line. The client never talks to storage directly and never learns which provider is behind the file.
Connect it as a storage target with credentials they issue, add a policy for that customer, and their files land there from the next upload. Product code does not change.
Yes. Buckets stay private and every download goes through a signed URL that expires. There is no public object path to leak or to guess.
Nothing you own. The buckets are still yours and readable with any tool. What you give up is the provider-specific code — and the migration project when that provider has to change.
Your next feature has files.
Ship it with Uplint.
Connect a bucket, create a key, and make the upload call. The storage decisions can wait until a customer asks.