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

# Object storage

> Store uploads, exports and generated files in an S3-compatible bucket.

A bucket is private S3-compatible storage in your project, for anything that has to outlive
a container's filesystem: uploads, generated exports, static assets, backups.

Siblings reach it at its service name. It has no public URL unless you turn one on.

Object storage requires the Hobby plan or higher. See [Plans](/plans).

## Creating a bucket

Select **New → Object storage**. A bucket has no compute allocation to choose, because the
S3 endpoint runs on slipway's side rather than yours.

## Connecting

The bucket speaks the S3 API, so any S3 client works unchanged. Two settings differ from
talking to AWS: use **path-style addressing**, because the in-cluster endpoint is a bare
service name and cannot carry a bucket in its hostname, and set the **region to `auto`**.

Every value you need is a service reference, so nothing is copied by hand:

```yaml slipway.yaml theme={"system"}
services:
  - name: api
    runtime: docker
    env:
      S3_ENDPOINT: ${service.files.endpoint}
      S3_BUCKET: ${service.files.bucket}
      S3_ACCESS_KEY_ID: ${service.files.access_key_id}
      S3_SECRET_ACCESS_KEY: ${service.files.secret_access_key}
      S3_REGION: auto
```

These resolve to the bucket's `default` key, which is created with the bucket and has
read-write access.

```python Python (boto3) theme={"system"}
import boto3, os

s3 = boto3.client(
    "s3",
    endpoint_url=os.environ["S3_ENDPOINT"],
    aws_access_key_id=os.environ["S3_ACCESS_KEY_ID"],
    aws_secret_access_key=os.environ["S3_SECRET_ACCESS_KEY"],
    region_name="auto",
)
s3.put_object(Bucket=os.environ["S3_BUCKET"], Key="hello.txt", Body=b"hi")
```

```javascript Node (AWS SDK v3) theme={"system"}
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";

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

await s3.send(new PutObjectCommand({
  Bucket: process.env.S3_BUCKET,
  Key: "hello.txt",
  Body: "hi",
}));
```

```go Go (aws-sdk-go-v2) theme={"system"}
client := s3.New(s3.Options{
    Region:       "auto",
    BaseEndpoint: aws.String(os.Getenv("S3_ENDPOINT")),
    UsePathStyle: true,
    Credentials: credentials.NewStaticCredentialsProvider(
        os.Getenv("S3_ACCESS_KEY_ID"),
        os.Getenv("S3_SECRET_ACCESS_KEY"), ""),
})
```

## Access keys

The **Keys** tab lists the bucket's access keys. Each is read-write (full access) or
read-only (`GET`, `HEAD` and listing, with uploads and deletes refused as `AccessDenied`).

Create a key per consumer rather than sharing one, so revoking a single integration does
not take down the rest.

Revoking a key takes effect within about two minutes and needs no redeploy. Keys are
checked against a live key set rather than baked into the running container.

> [!WARNING]
>
> Deleting the `default` key breaks `${service.X.access_key_id}` and
> `${service.X.secret_access_key}` for every sibling that references them. Those deploys fail
> with `no_credentials` until a `default` key exists again or the references point at another
> key.

## External access

By default a bucket is reachable only from inside its project. **External access**, on the
Connection card, serves the S3 API on the public internet over HTTPS at
`https://<service>.<your-domain>`, so tools, CI and apps elsewhere can use it.

Requests still need an access key. External access changes where the bucket can be reached
from, not who may read it.

Turning it on redeploys the service, because the public hostname is part of the service's
configuration rather than a setting applied to a running container. Turning it off
withdraws the hostname on the next deploy.

> [!NOTE]
>
> A presigned URL is only valid against the endpoint it was signed for. One signed against
> the in-cluster endpoint (`http://files:9000`) will not work from a browser. Sign against
> the public hostname instead.

## Public access

Public access controls what a caller with **no** access key may do. It applies within about
two minutes and needs no redeploy.

| Mode | An anonymous caller can |
| - | - |
| Private (default) | Nothing. Every request needs a key. |
| Public objects | Fetch an object by its exact key. The bucket cannot be listed. |
| Public browse | Also list the bucket and browse it as a file index. |

Use **Public objects** for avatars, downloads and static assets: the URLs you hand out
work, and nobody can enumerate what else is there.

**Public browse** is deliberate enumeration. Anyone with the URL sees every object, and
opening the bucket in a browser renders a file index with folders, sizes and dates. Use it
for release artifacts or documentation trees, not for user uploads.

Writes always require a read-write key, in every mode.

Public access is independent of external access. A public mode without external access
means anonymous reads work only from inside the project, which is useful when a sibling
should read objects without holding a key.

## Previews

A full-stack [preview](/previews/pr-previews) gets its own bucket, created when the preview
deploys and destroyed with it, so preview code cannot read or overwrite production objects.

A [single-service preview](/previews/service-previews) reaches the base project's bucket,
because it is deliberately running against the base project's live siblings. Use a
preview-scoped variable or secret to point it elsewhere.

## The Files tab

The **Files** tab is a basic browser: navigate folders, upload, download, delete, and see
what the bucket holds. It is for inspection and one-off fixes. Anything routine belongs in
your app, through the S3 API.

## Deleting

Deleting the service, or the project that owns it, permanently destroys the bucket and
everything in it. There is no undelete and no retention window. Copy out anything you need
first.

## Pricing

| What | Rate |
| - | - |
| CPU | \$8.76 per vCPU per month |
| Memory | \$4.38 per GiB per month |
| Disk | \$0.25 per GiB per month |
| Volume backups | \$0.05 per GiB per month |
| Object storage | \$0.05 per GiB per month |
| Egress beyond the plan allowance | \$0.01 per GB |

Compute is metered by the second against the allocation a service is configured with, so
a service that sleeps costs nothing while it is asleep. Disk, backups and object storage
are metered on what is provisioned or held, whether or not the service is running.

You are billed for what the bucket stores, and nothing else:

* **Egress is free**, however many downloads there are.
* **Requests are free.** Uploads, listings and deletes are not metered.
* **The endpoint is not charged for.** It runs on slipway's side, which is why a bucket has
  no compute allocation and no Compute tab.

Object storage and volume backups share a rate because both are objects in the same store.
A live disk costs five times more because it is a replicated block device.

Usage is rounded up to whole GiB, so a bucket holding one small file is billed for one GiB.
A bucket keeps costing while everything around it sleeps, because the objects are still
there.

Totals are counted hourly rather than live: measuring a bucket means walking every object,
so slipway does it on a schedule and slows that schedule for very large buckets. The
Metrics tab shows when the figure was last counted, and a large upload appears on the next
pass.

## FAQ

**Does a downgrade delete my bucket?**

**No.** Object storage is gated at creation only. An existing bucket keeps working and only
new ones are blocked.

**Can two services share a bucket?**

**Yes.** That is the usual reason to choose object storage over a [disk](/data/disks),
which only one service can mount.


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