Store uploads and results without setting up a bucket
Keeping processed files often means setting up storage separately: creating an S3 bucket, writing an IAM policy, and adding credentials to a Template. That is extra work before you get to the problem you came to solve.
Transloadit Storage offers an alternative for admitted Workspaces. The /transloadit/store Robot
keeps files in your Workspace's storage, and a signed Smart CDN URL serves the original or an
on-the-fly derivative. The Console File Library and an S3-compatible endpoint provide ways to
manage those files when the File Library is enabled for your Workspace.
Transloadit Storage is in private preview. If you want to try it before general availability, ask us about access for your Workspace. This tutorial also needs File Library access, which our team enables separately. Enabling Storage alone does not grant that access.
This tutorial takes a file from your disk into Transloadit Storage and back out through the Smart CDN in four short steps, with Node.js and the official SDK. No bucket is created at any point.
What you need
- Node.js 22 or newer.
- A Workspace admitted to the Storage preview with File Library access enabled.
- Two Auth Keys from Credentials in the Console: an API key with
assemblies:write,dam:read,templates:read, andtemplates:writefor the examples below, and a separate Smart CDN signing key. For that second key, choose New Auth Key and Private image delivery (Smart CDN signing), which enables Smart CDN and selects onlysmart_cdn:sign. Uploading directly through S3 also needsdam:write. - The AWS CLI for the optional S3 commands in Step 2.
- The slug of your Workspace — the part after
/c/in the Console URL, for examplemytransloadit-toystory-app.
Assemblies authorized by assemblies:write can store and import files through Storage Robots.
The dam:read and dam:write scopes control direct S3 access, not those Assembly operations.
Enabling Smart CDN on a key does not limit what else it can do; its permissions do. With only
smart_cdn:sign, the key cannot create standalone Assemblies or manage Storage. Anyone with its
Auth Secret can still sign URLs that run transformations on your account and serve your stored files,
so keep it on your server like any other Auth Secret. If it leaks,
delete the key and create a replacement.
Create a project and a .env file:
mkdir zero-config-storage && cd zero-config-storage
npm init -y && npm pkg set type=module
npm install @transloadit/node @transloadit/utils
curl --fail --location --output photo.jpg https://demos.transloadit.com/inputs/chameleon.jpg
TRANSLOADIT_KEY=your-api-auth-key
TRANSLOADIT_SECRET=your-api-auth-secret
TRANSLOADIT_CDN_KEY=your-smart-cdn-auth-key
TRANSLOADIT_CDN_SECRET=your-smart-cdn-auth-secret
TRANSLOADIT_WORKSPACE=your-workspace-slug
Node reads it with --env-file, so there is no dotenv dependency. For the shell commands in Step 2,
load it with set -a; . ./.env; set +a and use that same terminal. During the preview you may also
have received endpoint overrides from us (TRANSLOADIT_ENDPOINT, TRANSLOADIT_STORAGE_S3_ENDPOINT,
TRANSLOADIT_SMART_CDN_BASE_URL, TRANSLOADIT_SMART_CDN_EXTRA_PARAMS); the scripts below pick
those up when present and fall back to the production defaults otherwise.
Quote values containing spaces or shell characters such as & in .env, so the file is also
safe to load with the shell command.
Step 1: Store a file with /transloadit/store
/transloadit/store is a storing Robot like /s3/store, except that it needs no credentials and no
bucket: it writes into your Workspace's storage. path is where the file ends up; folders are
created on demand, and ${file.name} is substituted per file.
import { Transloadit } from '@transloadit/node'
const client = new Transloadit({
authKey: process.env.TRANSLOADIT_KEY,
authSecret: process.env.TRANSLOADIT_SECRET,
endpoint: process.env.TRANSLOADIT_ENDPOINT,
})
const assembly = await client.createAssembly({
files: { photo: './photo.jpg' },
params: {
steps: {
stored: {
robot: '/transloadit/store',
use: ':original',
path: 'uploads/${file.name}',
// Uploading the same name again creates a new version instead of failing.
conflict_strategy: 'overwrite',
},
},
},
waitForCompletion: true,
})
// Storing Robots do not add a result key of their own: they annotate the file they stored,
// which is still listed under the step that produced it — here the upload, `:original`.
const [stored] = assembly.results[':original']
console.log(`Stored ${stored.path} (${stored.size} bytes, asset ${stored.asset_id})`)
node --env-file=.env store.js
# Stored uploads/photo.jpg (9309370 bytes, asset 8kLg…)
That is the whole "set up storage" part of this tutorial. The file now lives at
uploads/photo.jpg inside your Workspace. Store the same path again and you get a new version
(conflict_strategy: 'overwrite'). You can retrieve an earlier version while it is retained;
versioning is not a promise of permanent retention or a substitute for your backup policy.
Step 2: See it — in the Console or with the S3 tools you already have
The File Library must be enabled for your Workspace by our team. A Workspace owner, admin, or regular member can then open it; a Billing Admin cannot. Enabling Storage alone does not unlock the page or its S3 endpoint.
Once access is enabled, open the Console, pick your Workspace, and go to File Library
(https://transloadit.com/c/<workspace-slug>/file-library/). The uploads folder is there with
photo.jpg in it. You can browse and manage your stored files here.

Prefer a terminal? Transloadit Storage is S3-compatible, and your Auth Key and Secret are its credentials. Your Workspace slug is the bucket:
set -a; . ./.env; set +a
export AWS_ACCESS_KEY_ID="$TRANSLOADIT_KEY" AWS_SECRET_ACCESS_KEY="$TRANSLOADIT_SECRET"
export AWS_DEFAULT_REGION=us-east-1
aws --endpoint-url "${TRANSLOADIT_STORAGE_S3_ENDPOINT:-https://api2.transloadit.com/storage}" \
s3 ls "s3://$TRANSLOADIT_WORKSPACE/uploads/"
# 2026-08-31 10:12:04 9309370 photo.jpg
The following examples use S3 listing and versioned downloads. Check the operations and permissions
your client needs rather than assuming every AWS S3 feature is available. After running store.js
twice, you can list both versions through the S3 API:
aws --endpoint-url "${TRANSLOADIT_STORAGE_S3_ENDPOINT:-https://api2.transloadit.com/storage}" \
s3api list-object-versions --bucket "$TRANSLOADIT_WORKSPACE" --prefix uploads/photo.jpg \
--query 'Versions[?Key==`uploads/photo.jpg`].[VersionId,IsLatest,LastModified]' --output text
# 9G2… True 2026-08-31T10:12:04+00:00
# 7Kq… False 2026-08-31T10:05:41+00:00
After uploading to the same path twice, fetch the earlier version by its version ID. The exact-key
filter avoids selecting a different file whose name starts with uploads/photo.jpg:
if PREVIOUS=$(aws --endpoint-url "${TRANSLOADIT_STORAGE_S3_ENDPOINT:-https://api2.transloadit.com/storage}" \
s3api list-object-versions --bucket "$TRANSLOADIT_WORKSPACE" --prefix uploads/photo.jpg \
--query 'Versions[?Key==`uploads/photo.jpg` && IsLatest==`false`] | [0].VersionId' --output text); then
if [ -n "$PREVIOUS" ] && [ "$PREVIOUS" != None ]; then
aws --endpoint-url "${TRANSLOADIT_STORAGE_S3_ENDPOINT:-https://api2.transloadit.com/storage}" \
s3api get-object --bucket "$TRANSLOADIT_WORKSPACE" --key uploads/photo.jpg \
--version-id "$PREVIOUS" photo-previous.jpg
else
printf '%s\n' 'No previous version exists. Upload the same path again first.'
fi
else
printf '%s\n' 'Could not list object versions; no download was attempted.' >&2
fi
Step 3: Serve it through the Smart CDN
Keep the tutorial's uploads/ directory private. To hand a file to a browser, sign a Smart CDN URL for the built-in
builtin/storage-serve@0.0.1 Template, which reads the file from your storage and serves it. The
signature carries an expiry that limits how long Transloadit accepts that signature. Expiry
cannot recall bytes that a recipient has already downloaded or cached.
import { getSignedSmartCdnUrl } from '@transloadit/utils/node'
const url = getSignedSmartCdnUrl({
workspace: process.env.TRANSLOADIT_WORKSPACE,
template: 'builtin/storage-serve@0.0.1',
input: 'uploads/photo.jpg',
authKey: process.env.TRANSLOADIT_CDN_KEY,
authSecret: process.env.TRANSLOADIT_CDN_SECRET,
expiresAt: Date.now() + 60 * 60 * 1000, // valid for one hour
// Preview environments only; omit in production.
baseUrl: process.env.TRANSLOADIT_SMART_CDN_BASE_URL,
urlParams: Object.fromEntries(
new URLSearchParams(process.env.TRANSLOADIT_SMART_CDN_EXTRA_PARAMS ?? ''),
),
})
console.log(url)
curl --fail --silent --show-error --output photo-served.jpg "$(node --env-file=.env serve.js)"
The URL looks like
https://your-workspace-slug.tlcdn.com/builtin%2Fstorage-serve%400.0.1/uploads%2Fphoto.jpg?auth_key=…&exp=…&sig=sha256%3A….
Treat it like a credential: anyone holding it can use its authorization, so pipe it
into curl or hand it to a browser rather than logging it. The same URL without its auth_key,
exp and sig parameters is refused for this private directory, regardless of your Workspace's
Signature Authentication setting. Storage also supports explicitly published prefixes and separate
public-delivery Built-ins. This tutorial does not publish a prefix; do not publish private files
just to avoid signing URLs.
This serves the original — all 9.3 MB of it, also to a phone. Step 4 shows how to serve a resized version instead; in a real page you would use that for anything larger than a thumbnail.
Step 4 (optional): Derivatives on the fly
builtin/storage-serve serves the original. For thumbnails, watermarks, or format conversions you
create your own Template with /transloadit/import, a processing Robot, and /file/serve.
This one uses /image/resize to resize to a width passed in the URL:
import { Transloadit } from '@transloadit/node'
const client = new Transloadit({
authKey: process.env.TRANSLOADIT_KEY,
authSecret: process.env.TRANSLOADIT_SECRET,
endpoint: process.env.TRANSLOADIT_ENDPOINT,
})
const name = 'storage-thumb'
const template = {
steps: {
imported: { robot: '/transloadit/import', path: '${fields.input}' },
resized: { robot: '/image/resize', use: 'imported', width: '${fields.w}', imagemagick_stack: 'v3.0.0' },
served: { robot: '/file/serve', use: 'resized' },
},
}
// Search every page so rerunning the script also works in Workspaces with many Templates.
let existing
for await (const item of client.streamTemplates()) {
if (item.name === name) {
existing = item
break
}
}
if (existing) {
await client.editTemplate(existing.id, { name, template })
console.log(`Template ${name} updated`)
} else {
await client.createTemplate({ name, template })
console.log(`Template ${name} created`)
}
Signing a URL for it is the same call with your own Template name and the width as a URL parameter:
import { getSignedSmartCdnUrl } from '@transloadit/utils/node'
const url = getSignedSmartCdnUrl({
workspace: process.env.TRANSLOADIT_WORKSPACE,
template: 'storage-thumb',
input: 'uploads/photo.jpg',
authKey: process.env.TRANSLOADIT_CDN_KEY,
authSecret: process.env.TRANSLOADIT_CDN_SECRET,
expiresAt: Date.now() + 60 * 60 * 1000,
baseUrl: process.env.TRANSLOADIT_SMART_CDN_BASE_URL, // preview environments only
urlParams: {
w: 300,
...Object.fromEntries(new URLSearchParams(process.env.TRANSLOADIT_SMART_CDN_EXTRA_PARAMS ?? '')),
},
})
console.log(url)
node --env-file=.env create-template.js && \
curl --fail --silent --show-error --output photo-thumb.jpg "$(node --env-file=.env serve-thumb.js)"
# Template storage-thumb created
The Smart CDN returns a 300-pixel-wide version of uploads/photo.jpg.

The custom Template's /transloadit/import Step also requires authenticated access. Disabling
Workspace Signature Authentication does not make this private import public. For other Templates,
consult the authentication documentation before allowing unsigned use.
Reference: the two Robots
/transloadit/storeaccepts an optionalpath, which defaults to${file.url_name}. Existing destinations fail unless you chooseoverwriteorrename. Save the returnedworkspace,asset_id,version_id, and finalpathfrom the producing Step's result. See the Storage Robot reference for the complete options./transloadit/importaccepts a file or folderpath, or anasset_idwith an optionalversion_idto select retained bytes. Folder imports include only direct children unlessrecursive: trueis set. Its output is a normal file that another Robot canuse. See the import Robot reference for authentication and selectors.
What just happened
- One Robot,
/transloadit/store, kept the file in your Workspace's storage. No bucket, no IAM, no credentials in Templates. - An enabled Console File Library and the supported S3 operations expose the same stored files.
- A signed Smart CDN URL serves the original, and a small Template of your own serves derivatives.
Where to go next
- For browser upload interfaces, explore Uppy. Keep Auth Secrets and the signing examples from this article on your server, not in browser code.
- Use
/transloadit/importwith a folder path andrecursive: trueto include subfolders when re-processing a collection, such as making new thumbnails for a whole campaign.
