Skip to content

Connecting clients

Object Storage speaks the standard AWS S3 protocol. Every official SDK, every CLI that knows about S3 — aws, aws s3api, boto3, @aws-sdk/client-s3, rclone, mc (MinIO Client), s3cmd, the AWS SDKs for Go / Java / PHP / Ruby / .NET — works against Runsite by changing only the endpoint URL.

You always need the same four values:

endpointhttps://s3.runsite.app
regionauto
access_key_idfrom the dashboard, starts with rs_ak_stg_
secret_access_keyfrom the dashboard, starts with rs_sk_stg_

Path-style addressing is what the dashboard examples use — set forcePathStyle: true (Node) or addressing_style: "path" (Python). Virtual-hosted style works too.

import os
import boto3
from botocore.config import Config
s3 = boto3.client(
"s3",
endpoint_url="https://s3.runsite.app",
aws_access_key_id=os.environ["S3_ACCESS_KEY_ID"],
aws_secret_access_key=os.environ["S3_SECRET_ACCESS_KEY"],
region_name="auto",
config=Config(s3={"addressing_style": "path"}),
)
s3.upload_file("local.png", "user-uploads", "avatars/u123.png")
obj = s3.get_object(Bucket="user-uploads", Key="avatars/u123.png")
data = obj["Body"].read()
s3.delete_object(Bucket="user-uploads", Key="avatars/u123.png")

boto3 automatically uses multipart uploads for files larger than 8 MB when you call upload_file or upload_fileobj.

import {
S3Client,
PutObjectCommand,
GetObjectCommand,
DeleteObjectCommand,
} from '@aws-sdk/client-s3';
const s3 = new S3Client({
endpoint: 'https://s3.runsite.app',
region: 'auto',
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
},
forcePathStyle: true,
});
await s3.send(
new PutObjectCommand({
Bucket: 'user-uploads',
Key: 'avatars/u123.png',
Body: fileBuffer,
ContentType: 'image/png',
}),
);
const get = await s3.send(
new GetObjectCommand({ Bucket: 'user-uploads', Key: 'avatars/u123.png' }),
);
const body = await get.Body.transformToByteArray();

For multipart uploads of large files, use Upload from @aws-sdk/lib-storage:

import { Upload } from '@aws-sdk/lib-storage';
await new Upload({
client: s3,
params: {
Bucket: 'user-uploads',
Key: 'video.mp4',
Body: stream,
ContentType: 'video/mp4',
},
}).done();

Configure once with a named profile:

Terminal window
aws configure --profile runsite
# AWS Access Key ID: rs_ak_stg_…
# AWS Secret Access Key: rs_sk_stg_…
# Default region: auto
# Default output: json

Then talk to Runsite by passing --endpoint-url:

Terminal window
aws --profile runsite --endpoint-url https://s3.runsite.app \
s3 ls
aws --profile runsite --endpoint-url https://s3.runsite.app \
s3 cp ./report.pdf s3://user-uploads/reports/2026-05.pdf
aws --profile runsite --endpoint-url https://s3.runsite.app \
s3 sync ./build s3://static-assets/build/

To avoid passing --endpoint-url every time, set it in ~/.aws/config:

[profile runsite]
region = auto
endpoint_url = https://s3.runsite.app
s3 =
addressing_style = path

rclone is the easiest tool for backups, sync jobs and one-off uploads of large directories.

Terminal window
rclone config create runsite s3 \
provider Other \
access_key_id rs_ak_stg_… \
secret_access_key rs_sk_stg_… \
endpoint https://s3.runsite.app \
region auto \
force_path_style true

Then:

Terminal window
rclone ls runsite:user-uploads
rclone copy ./photos runsite:user-uploads/photos -P
rclone sync ./build runsite:static-assets/build --delete
Terminal window
mc alias set runsite https://s3.runsite.app rs_ak_stg_… rs_sk_stg_…
mc ls runsite/
mc cp ./report.pdf runsite/user-uploads/reports/
mc mirror ./build runsite/static-assets/build
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
cfg, _ := config.LoadDefaultConfig(context.TODO(),
config.WithRegion("auto"),
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
os.Getenv("S3_ACCESS_KEY_ID"),
os.Getenv("S3_SECRET_ACCESS_KEY"),
"",
)),
)
client := s3.NewFromConfig(cfg, func(o *s3.Options) {
o.BaseEndpoint = aws.String("https://s3.runsite.app")
o.UsePathStyle = true
})
_, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
Bucket: aws.String("user-uploads"),
Key: aws.String("hello.txt"),
Body: strings.NewReader("Hello, Runsite"),
})
use Aws\S3\S3Client;
$s3 = new S3Client([
'version' => 'latest',
'region' => 'auto',
'endpoint' => 'https://s3.runsite.app',
'use_path_style_endpoint' => true,
'credentials' => [
'key' => getenv('S3_ACCESS_KEY_ID'),
'secret' => getenv('S3_SECRET_ACCESS_KEY'),
],
]);
$s3->putObject([
'Bucket' => 'user-uploads',
'Key' => 'avatars/u123.png',
'SourceFile' => '/tmp/u123.png',
]);

If the bucket is public, anyone can read objects directly over HTTPS — no SDK, no signing, no credentials:

https://s3.runsite.app/<bucket>/<key>

For example:

https://s3.runsite.app/marketing-assets/hero.png

The same object is also reachable via virtual-hosted style:

https://marketing-assets.s3.runsite.app/hero.png

This is what you embed in <img> tags, <a href>, CSS background-image, etc.

A per-bucket key still talks to the same endpoint — only the requests it can authenticate are restricted to that one bucket. From the SDK’s point of view nothing changes; trying to call ListBuckets or touch another bucket simply returns 403 Forbidden.

Wiring it into a Web Service

Add the four values (endpoint, region, access key, secret) as environment variables on your Runsite Web Service under Settings → Environment, then read them in your code with process.env.S3_… / os.environ["S3_…"]. Pick Save & Deploy so the new container picks them up.