Skip to main content
A Node.js application on Sevalla stores files in an object storage 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.

At a glance

What this guide adds to the Express application:

Environment variables

The Express application’s own variables are in its table.

Before you begin

  • An Express application deployed on Sevalla, as in Deploy 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 for every field.

Add the bucket details to the application

1

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.
2

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:They apply at the next deployment.
See Connect with the S3 API for what each value is.

Install the AWS SDK

  • @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:
storage.js
Sevalla buckets run on Cloudflare R2, which ignores the region, but the SDK requires one: use auto. The two WHEN_REQUIRED options make the SDK add checksums only to the requests that need them. Without them, it signs the checksum of an empty body (x-amz-checksum-crc32=AAAAAA==) into every presigned upload URL, which doesn’t match the file that’s uploaded with it.

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:
server.js
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.
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, using http://localhost:3000.

Deploy and verify

Commit and push the changes, package-lock.json included: Ingot installs the new packages from it with npm ci and starts the server from the start script as before. The build doesn’t need the bucket’s variables, only the running server does. 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 PNG file from your computer, such as logo.png in the current folder. Use your application’s domain instead of storefront-x7k2m.sevalla.app:
    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 returns 500, read the application’s runtime logs for the S3 error: For other errors, see the connection troubleshooting table.

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.

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.

Deploy with the API

This sequence adds the bucket of this guide to the application from Deploy Express through the Sevalla API, so an agent or a script can run it as is. APP_ID is that application, and the changes on this page are pushed to its branch. Each step names the operation, shows the curl command, and gives the matching CLI command and MCP server tool. You need an API key with the Company developer role in the SEVALLA_API_KEY environment variable (the CLI reads the same key from SEVALLA_API_TOKEN), curl and jq. A call that fails returns status and message instead of the resource, and jq -r .id prints null: stop and read the message.
1

Create the bucket

Create object storage bucket (createObjectStorageV3) creates a private bucket. location is where most reads and writes come from: weur, Western Europe, suits an application in Frankfurt, Germany.
CLI: sevalla object-storage create --name storefront-media --location weur. MCP: sevalla_api_create with createObjectStorageV3 and the same body.
2

Read the bucket details

Get object storage bucket (getObjectStorageV3) returns the bucket’s endpoint, bucket_name, access_key and secret_key.
CLI: sevalla object-storage get "$BUCKET_ID" --json. MCP: sevalla_api_read with getObjectStorageV3.
3

Add the environment variables

Create environment variable (createEnvironmentVariableV3) adds one variable per call. add_var sends each one as available during runtime only, since the build doesn’t need them:
CLI: sevalla apps env-vars create "$APP_ID" --key S3_BUCKET --value "$(jq -r .bucket_name <<< "$BUCKET")", once per variable; the CLI makes each one available during the build too. MCP: set_environment_variables with the four variables in one call, each with runtime true and buildtime false.
4

Deploy

Trigger deployment (triggerDeploymentV3) builds the latest commit of the branch and rolls it out.
CLI: sevalla apps deployments trigger "$APP_ID". MCP: trigger_deployment.
5

Wait for the deployment

Get deployment (getDeploymentV3) returns the deployment’s status: waiting, then inProgress, then success or failed. The loop polls every 15 seconds for up to 30 minutes and keeps polling through a failed request.
CLI: sevalla apps deployments get "$DEPLOYMENT_ID" --app-id "$APP_ID". MCP: get_deployment.
6

Check the deployment

List application domains (listApplicationDomainsV3) returns the application’s domain. Upload logo.png from the current folder, list the files, and follow the redirect to the presigned URL:
The output, with your file’s size in bytes:
List objects (listObjectStorageObjectsV3) lists the same file from the Sevalla side. CLI: sevalla apps domains list --app-id "$APP_ID" and sevalla object-storage objects list --bucket-id "$BUCKET_ID". MCP: get_application, and sevalla_api_read with listObjectStorageObjectsV3.