# Upload and serve private files

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

Source: https://docs.fortly.com/files/upload-and-serve

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.

> **Note:**
>
> 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

| Task | Request path | Where authorization happens |
|---|---|---|
| Inspect or upload a file yourself | Storage → Buckets → your bucket → Files | Your Fortly account role |
| Store an app-generated export | Your server → Fortly S3 endpoint | Your app checks the user, then uses its bucket key |
| Let a user download a private document | Your server issues a temporary GET URL; browser downloads from S3 | Your app checks access before signing; S3 validates the signed request |
| Let a browser upload directly | Your server issues a temporary PUT URL; browser sends bytes to S3 | Your app checks the upload request; S3 validates the signature, key permission and shared quota |
| Publish a deliberately public asset | Project 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:

```bash
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.

```js title="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](https://docs.fortly.com/files/private-buckets#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](https://docs.fortly.com/files/access-and-keys).

## 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](https://docs.fortly.com/files/usage-and-capacity#resolve-a-storage-refusal) for a specific next action.

## Public delivery and recovery

Use [CDN delivery](https://docs.fortly.com/files/cdn) 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](https://docs.fortly.com/files/usage-and-capacity) before a large import.
