> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sevalla.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> These are the docs for Sevalla, a platform for deploying applications, databases, static sites, object storage and load balancers. The dashboard is at https://app.sevalla.com.
> Bold text in steps is an exact dashboard label.
> To act on a Sevalla account, prefer the Sevalla MCP server (https://mcp.sevalla.com/mcp, see https://docs.sevalla.com/ai-agents/overview.md), then the Sevalla CLI (https://docs.sevalla.com/developer-tools/cli.md), then the REST API (https://api-docs.sevalla.com, OpenAPI spec at https://api.sevalla.com/v3/openapi.json).
> Every page index is at https://docs.sevalla.com/llms.txt.

# Use object storage from Node.js

> Store uploads from a Node.js application on Sevalla in an object storage bucket with the AWS SDK for JavaScript, and serve them with presigned URLs.

A Node.js application on Sevalla stores files in an [object storage](/object-storage/overview) bucket through the bucket's S3 API: the AWS SDK for JavaScript reads the bucket's endpoint, name and keys from environment variables, uploads files as they arrive, and hands out presigned URLs to download them. Files in a bucket survive deployments and are shared by every instance, unlike files on an instance's disk.

This guide adds file routes to an Express application. The client code works the same in any Node.js framework.

## Before you begin

* An Express application deployed on Sevalla, as in [Deploy Express](/guides/javascript/express).
* Node.js 20 or later in the application, which the AWS SDK for JavaScript (`@aws-sdk/client-s3`) needs. The Express guide pins Node.js 24.

## Create a bucket

In the sidebar, click **Object storage**, then click **Create object storage**. Enter `storefront-media` as the **Name**, choose the **Location** closest to the application, and click **Create object storage**. See [Create a bucket](/object-storage/create) for every field.

## Add the bucket details to the application

<Steps>
  <Step title="Copy the values">
    On the bucket's **Settings** tab, copy the **Endpoint** and **Bucket name** under **Details**, and the **Access key** and **Secret key** under **Access keys**.
  </Step>

  <Step title="Add them as environment variables">
    On the application's **Environment variables** tab, click **Add environment variables**. Add these variables, keep **Available during runtime** selected, and click **Add environment variables**:

    | Variable | Value |
    | - | - |
    | `S3_ENDPOINT` | The **Endpoint**, such as `https://<account-id>.r2.cloudflarestorage.com` |
    | `S3_BUCKET` | The **Bucket name**, such as `storefront-media-q8w2e`. The display name doesn't work in S3 requests. |
    | `S3_ACCESS_KEY_ID` | The **Access key** |
    | `S3_SECRET_ACCESS_KEY` | The **Secret key** |

    They apply at the next deployment.
  </Step>
</Steps>

See [Connect with the S3 API](/object-storage/connect#connection-values) for what each value is.

## Install the AWS SDK

```bash theme={null}
npm install @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request-presigner
```

* `@aws-sdk/client-s3` sends the S3 requests.
* `@aws-sdk/lib-storage` streams uploads and splits large files into parts.
* `@aws-sdk/s3-request-presigner` creates presigned URLs.

## Create the client

Create one client for the whole application:

```js storage.js theme={null}
import { S3Client } from "@aws-sdk/client-s3";

export const s3 = new S3Client({
  region: "auto",
  endpoint: process.env.S3_ENDPOINT,
  credentials: {
    accessKeyId: process.env.S3_ACCESS_KEY_ID,
    secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
  },
  requestChecksumCalculation: "WHEN_REQUIRED",
  responseChecksumValidation: "WHEN_REQUIRED",
});

export const Bucket = process.env.S3_BUCKET;
```

Sevalla buckets run on Cloudflare R2, which ignores the region, but the SDK requires one: use `auto`. The two `WHEN_REQUIRED` options stop the SDK from adding checksums that only some operations need: without them, it signs the checksum of an empty body into presigned upload URLs, and R2 rejects the real upload.

## Add routes for files

Add three routes to `server.js`: one that streams an upload into the bucket, one that lists the files, and one that redirects to a presigned URL for a file:

```js server.js theme={null}
import express from "express";
import { GetObjectCommand, ListObjectsV2Command } from "@aws-sdk/client-s3";
import { Upload } from "@aws-sdk/lib-storage";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { s3, Bucket } from "./storage.js";

const app = express();
const port = process.env.PORT || 3000;

// Stream the request body into the bucket as uploads/<name>.
app.put("/files/:name", async (req, res) => {
  await new Upload({
    client: s3,
    params: {
      Bucket,
      Key: `uploads/${req.params.name}`,
      Body: req,
      ContentType: req.headers["content-type"],
    },
  }).done();
  res.status(201).json({ key: `uploads/${req.params.name}` });
});

// List the uploaded files.
app.get("/files", async (req, res) => {
  const { Contents = [] } = await s3.send(
    new ListObjectsV2Command({ Bucket, Prefix: "uploads/" })
  );
  res.json(Contents.map(({ Key, Size }) => ({ key: Key, size: Size })));
});

// Redirect to a link that works for one hour.
app.get("/files/:name", async (req, res) => {
  const url = await getSignedUrl(
    s3,
    new GetObjectCommand({ Bucket, Key: `uploads/${req.params.name}` }),
    { expiresIn: 3600 }
  );
  res.redirect(url);
});

app.listen(port, () => {
  console.log(`Listening on port ${port}`);
});
```

<Warning>
  These routes have no authentication: anyone who knows the address can upload files and read them. Check who's signed in before you upload or sign URLs in a real application.
</Warning>

To try it locally, put the four variables in your shell (for example with `export S3_ENDPOINT=...`), run `npm start`, and upload a file with the `curl` command in [Deploy and verify](#deploy-and-verify), using `http://localhost:3000`.

## Deploy and verify

Commit and push the changes. With auto-deploy on, the push starts a deployment; otherwise, start one with **Deploy now** on the application's **Overview** tab. When the deployment's status changes to **Deployed**:

1. Upload a file to the application's domain:

   ```bash theme={null}
   curl -X PUT --data-binary @logo.png -H "Content-Type: image/png" \
     https://storefront-x7k2m.sevalla.app/files/logo.png
   ```

   The response is `{"key":"uploads/logo.png"}`.
2. Open `https://storefront-x7k2m.sevalla.app/files/logo.png` in a browser. The application redirects to a presigned URL on the bucket's endpoint, and the image opens.
3. On the bucket's **Objects** tab, open the `uploads` folder. `logo.png` is there.

If a request fails, read the application's [runtime logs](/applications/logs) for the S3 error, then check the [connection troubleshooting table](/object-storage/connect#troubleshoot-connections). The most common cause is the display name in `S3_BUCKET` instead of the **Bucket name**.

## Let browsers upload directly

Uploads that pass through the application use its bandwidth and memory. To let the browser send a file straight to the bucket, create a presigned URL for `PutObjectCommand` on the server and give it to the browser, and add a CORS rule to the bucket for your site's origin. See [Share files with presigned URLs](/object-storage/connect#share-files-with-presigned-urls).

## Serve files publicly

For files that anyone may read, such as product images, turn on public access on the bucket's **Settings** tab and link to the object's public URL instead of signing one each time. Cloudflare's CDN caches public files. See [Public URLs](/object-storage/connect#public-urls).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.