Docs

Upload and serve private files

Connect an app to a private bucket, upload with an S3 client, and authorize temporary downloads or browser uploads.

Updated October 25, 2026
On this page
  1. Choose the request path
  2. Connect your application
  3. Upload from server code
  4. Return a temporary download
  5. Upload directly from a browser
  6. Verify the full path
  7. Public delivery and recovery

Keep the object private, then let your application decide who can upload or download it. Console uploads, server-side S3 clients and temporary signed requests all operate on the same objects.

Private buckets require an enabled installation. If Storage does not offer bucket creation, use the available Project files workflow only for public content. This guide does not change an existing file's visibility.

Choose the request path

TaskRequest pathWhere authorization happens
Inspect or upload a file yourselfStorage → Buckets → your bucket → FilesYour Fortly account role
Store an app-generated exportYour server → Fortly S3 endpointYour app checks the user, then uses its bucket key
Let a user download a private documentYour server issues a temporary GET URL; browser downloads from S3Your app checks access before signing; S3 validates the signed request
Let a browser upload directlyYour server issues a temporary PUT URL; browser sends bytes to S3Your app checks the upload request; S3 validates the signature, key permission and shared quota
Publish a deliberately public assetProject files at /_files/…Anyone with the public URL can read it

A bucket key identifies an application or tool, not an individual end user. Fortly does not infer a user's right to a document from its object key. Check ownership in your application before signing a link or using server credentials on the user's behalf.

Connect your application

Create the resource

Open Storage → Buckets → Create bucket and enter a name. Open Files to upload a small test object. The upload stays private.

Configure the connection

Open Connect, select the required access, and choose Connect app. Read only permits listing and downloads. Read and write also permits uploading, replacing and deleting files. A connection saves encrypted runtime variables; it does not hand the bucket's secret to your browser.

Apply configuration to your app

Follow the connection result. Fortly refreshes runtime configuration; redeploy when your app reads variables at startup or embeds configuration during a build. Bucket connection secrets are runtime-only and are not sent to builds. View code example uses the selected connection's variable prefix.

Upload from server code

Use Node.js 20 or later. Install the verified client versions in your server-side application:

Terminal
npm install @aws-sdk/client-s3@3.1152.0 @aws-sdk/s3-request-presigner@3.1152.0

With the default connection names, construct a path-style client. Set a different prefix in the example if your connection uses one.

storage.mjs
import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

export const storage = new S3Client({
  endpoint: process.env.S3_ENDPOINT,
  region: process.env.S3_REGION,
  forcePathStyle: true,
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
  },
  requestChecksumCalculation: "WHEN_REQUIRED",
  responseChecksumValidation: "WHEN_REQUIRED",
});

export async function saveExport(objectKey, csvText) {
  await storage.send(new PutObjectCommand({
    Bucket: process.env.S3_BUCKET,
    Key: objectKey,
    Body: new TextEncoder().encode(csvText),
    ContentType: "text/csv",
  }));
}

export async function temporaryDownload(objectKey) {
  return getSignedUrl(storage, new GetObjectCommand({
    Bucket: process.env.S3_BUCKET,
    Key: objectKey,
  }), { expiresIn: 300 });
}

Call saveExport or temporaryDownload only after your route has authenticated the user and authorized the exact object. Use server-generated object keys rather than accepting arbitrary keys from a browser. Never expose these environment variables through a NEXT_PUBLIC_ variable, client bundle, logs or an agent conversation.

Single uploads are limited to 64 MiB. Larger objects require supported multipart uploads, up to 5 GiB. Check compatibility, particularly checksum aggregation, before using a transfer helper that automatically chooses multipart.

Return a temporary download

Your app returns the signed URL to the authorized user, who downloads directly from the S3 endpoint. Anyone holding it can use it until expiry, subject to the signing key and bucket still being accessible. Do not print signed URLs in request logs or analytics.

The console's Temporary link action is separate: it uses a hidden read-only signer, defaults to 15 minutes and allows up to 24 hours. Settings → Revoke download links invalidates those console links together. Revoking an app key does not revoke links issued with a different key. See keys and access.

Upload directly from a browser

After validating the user, object key, file type and expected size, your server can sign a PutObjectCommand with a short expiry. A read-only key cannot authorize a PUT. Configure the bucket's browser-access rules for the exact browser origin, PUT method and headers the request actually sends. CORS does not grant S3 access.

Send the file as the request body, not as FormData; this release does not support S3's HTML POST-policy upload form. Preserve signed headers and use the exact URL returned by the server. Do not append a different key or decode and rebuild the signed path. Fortly still enforces the supported request size and account capacity when the browser sends the upload. Your application remains responsible for validating the resulting object before treating it as trusted content.

Verify the full path

Open Files → Refresh files after an SDK upload. Confirm the object key and size, then download through an authorized app route or temporary link. Test a user who should be denied as well as one who should succeed. In Activity this month, inspect read/write attempts and transferred bytes; the figures are separate from stored capacity.

If an upload fails, identify the stage: user authorization, signing, CORS, S3 permission, request compatibility or quota. A successful application login does not prove S3 access. Use storage troubleshooting for a specific next action.

Public delivery and recovery

Use CDN delivery for edge caching with private signed links or explicitly published asset folders. It is configured separately in the bucket's Delivery section and does not make S3 anonymous. S3 presigned links continue to bypass caching. Public Project files have a separate access model; do not move a protected document there just to obtain a shareable URL.

PostgreSQL restores do not rewind objects. Permanent bucket deletion has no recovery window. Keep separate copies when your retention policy requires them, and plan shared storage capacity before a large import.

Something missing or unclear? Tell us and we will fix the page.© 2026 Fortly