Retool file upload & storage: from File Input to a permanent CDN URL

A Retool File Input hands you a file's bytes (and, with Retool Storage, an opaque storage id) โ€” but a real app needs a durable link, private access, CDN delivery, and predictable cost. This guide shows how to store files from Retool on files.link and get a permanent CDN URL back, without hand-rolling an S3 bucket, IAM user, or CORS policy.

The whole thing is one JavaScript query: request a presigned URL, PUT the bytes, confirm โ€” then drop the resulting URL into a table cell, a form submission, or your own database. It's the same three-call file upload API used everywhere else on files.link, just driven from a Retool query.

The Retool file-storage problem

Retool is great at the UI. Where builders get stuck is what happens after a user picks a file:

  • Retool Storage keeps files behind Retool. Fine for internal-only assets, but you don't get a public, shareable, CDN-backed URL to use elsewhere.
  • Wiring an S3 resource means creating a bucket, an IAM user with the right policy, and a CORS configuration โ€” then maintaining all of it. That's a lot of AWS console for "save a file and give me a link."
  • Base64 in a database works until the files get big, then your queries and your DB bill both suffer.

files.link is the storage + CDN layer in between: your Retool query uploads the file and gets a stable https://cdn.files.link/<key> URL, served over a global CDN with edge locations. No IAM, no CORS, no bucket to babysit.

The workflow in three calls

  1. Create โ€” POST /v1/files/{folderId} with the file's metadata. You get back a short-lived presigned upload URL and a file id.
  2. Upload โ€” PUT the raw bytes straight to that presigned URL.
  3. Confirm โ€” POST /v1/files/confirm-upload with the id to finalize. Uploads you never confirm are cleaned up automatically.

Authentication is your API key in the Authorization header โ€” the raw key, with no Bearer prefix. Step 2 needs no auth header at all; the credentials are baked into the presigned URL.

The Retool query (JavaScript)

Add a File Input (or File Button) component, then create a JavaScript query with the code below. Replace <FOLDER_ID> and <YOUR_API_KEY> with values from your dashboard.

Retool ยท JavaScript query
// Retool JavaScript query โ€” uploads the selected file and returns a
// permanent files.link CDN URL. Wire it to a File Input / File Button
// component (here called fileInput1) and run it on upload.
const API = "https://api.files.link/v1";
const API_KEY = "<YOUR_API_KEY>"; // store as a Retool config variable, not inline
const FOLDER_ID = "<FOLDER_ID>";  // from your files.link dashboard

// Retool's File Input exposes parallel arrays: .files (metadata) and
// .value (base64-encoded contents). Take the first selected file.
const meta = fileInput1.files[0];   // { name, size, type }
const base64 = fileInput1.value[0]; // base64 string of the file contents

// 1. Ask files.link for a short-lived presigned upload URL.
const create = await fetch(`${API}/files/${FOLDER_ID}`, {
  method: "POST",
  headers: { Authorization: API_KEY, "Content-Type": "application/json" },
  body: JSON.stringify({
    filesMetadata: [{ name: meta.name, size: meta.size, type: meta.type }],
  }),
});
const { urls } = await create.json();
const { url, id, key } = urls[0];

// 2. PUT the raw bytes straight to storage โ€” decode Retool's base64 first.
//    No Authorization header here; the signature is baked into the URL.
const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
await fetch(url, {
  method: "PUT",
  headers: { "Content-Type": meta.type || "application/octet-stream" },
  body: bytes,
});

// 3. Confirm so the file is finalized (unconfirmed uploads are cleaned up).
await fetch(`${API}/files/confirm-upload`, {
  method: "POST",
  headers: { Authorization: API_KEY, "Content-Type": "application/json" },
  body: JSON.stringify({ ids: [id] }),
});

// Permanent CDN URL โ€” write it to a table cell, your DB, or a state variable.
return `https://cdn.files.link/${key}`;
CORS & key safety: a Retool JavaScript query runs in the browser, so the request must be allowed cross-origin and your API key is visible client-side. For internal tools that's often acceptable; to lock it down, run steps 1 and 3 through a Retool REST API resource (server-side, key stored in the resource) and keep only the presigned PUT in the browser.

Alternative: a Retool REST API resource

Prefer Retool's resource model over raw JS? Point a REST API resource at https://api.files.link/v1 and run the create + confirm calls through it โ€” they go out from Retool's backend, so there's no browser CORS to deal with.

Retool ยท REST API resource
# Prefer Retool's REST API resource? Point one at files.link and run
# steps 1 and 3 through it โ€” the calls go out from Retool's backend, so
# there is no browser CORS to worry about.

Resource โ†’ REST API
  Base URL:  https://api.files.link/v1
  Headers:   Authorization: <YOUR_API_KEY>   (raw key, no "Bearer" prefix)

# Query "createUpload"  ยท  POST  /files/{{ FOLDER_ID }}
{
  "filesMetadata": [
    {
      "name": {{ fileInput1.files[0].name }},
      "size": {{ fileInput1.files[0].size }},
      "type": {{ fileInput1.files[0].type }}
    }
  ]
}

# Query "confirmUpload"  ยท  POST  /files/confirm-upload
{ "ids": [ {{ createUpload.data.urls[0].id }} ] }

# The presigned PUT in step 2 targets a different host (upload.files.link),
# so keep that one as a small JavaScript query (see the recipe above).

Using the file in tables, forms & downloads

  • Store the URL. Write the returned cdn.files.link URL into your database (via your existing SQL/REST resource) or a Retool state variable, right next to the row it belongs to.
  • Show it. Public files render directly โ€” use the CDN URL as an Image src, a Link href, or an image/link column in a Table.
  • Keep some files private. A file's privacy follows its folder. Private files aren't publicly reachable โ€” you request a signed URL when you need to serve one, and it expires after 10 minutes, so generate it on demand rather than caching it.

Retool Storage / S3 resource vs files.link

Retool StorageRetool + S3 resourcefiles.link
SetupBuilt inBucket + IAM + CORSAPI key + folder id
Public shareable URLBehind RetoolYou configure itCDN URL out of the box
CDN deliveryNoOnly if you add CloudFrontGlobal CDN included
Private / signed linksLimitedYou build itSigned URLs (10-min expiry)
Cost modelBundled in RetoolAWS meteredPrepaid, pay for what you use

Honest limitations: files.link is a separate paid service โ€” there's no free tier; you add a payment method and pay for the storage and bandwidth you use. It isn't embedded in Retool the way Retool Storage is, so you manage an API key. If all you need is a throwaway internal asset that never leaves Retool, Retool Storage is simpler. The moment you need durable, fast, shareable, or signed links, files.link is the better fit.

FAQ

How do I handle file upload in Retool?

Add a File Input component and run a JavaScript query that reads its files/value, requests a presigned URL from files.link, PUTs the bytes, and confirms โ€” you get a permanent CDN URL back to store in your table. No S3 bucket or IAM policy required.

What's the best storage for a Retool app's files?

Retool Storage is fine for internal-only assets. For durable, shareable, CDN-backed links (or private signed URLs), files.link stores the file and gives you a stable cdn.files.link URL you can use inside Retool and out.

Can I use files.link as a Retool S3 uploader?

Yes โ€” it's the storage + CDN layer, so you skip the S3 resource setup (bucket policy, IAM user, CORS rules). Your Retool query calls the files.link API and gets a CDN URL back.

How do I upload a file to S3 from Retool without configuring IAM?

Point your Retool query at files.link instead of S3 directly. It mints a presigned upload URL per file and serves the result over a CDN, so you never create an IAM user, bucket policy, or CORS configuration yourself.

How do I download or display a stored file in Retool?

Public files: use the CDN URL directly as an Image src, Link href, or table column. Private files: request a short-lived signed URL (10-minute expiry) and use it as the download link.

Wire files.link into your Retool app

files.link is prepaid and pay-as-you-go: create an account, add a payment method, grab an API key, and paste the query above into Retool. You pay only for what you store and serve.

Related

More guides on the Developer Guides hub.

files.link
Copyright ยฉ 2026
All rights reserved
ContactGuidesGlossaryStatusLegal