Object Storage Buckets
An Object Storage resource is an S3-compatible bucket for files your app
stores and serves — uploads, images, exports, static assets. Like every
resource, a bucket lives inside an Environment. You work with it three ways:
a Files browser in the dashboard, the paas buckets CLI commands, and any
standard S3 client using credentials the CLI hands you.
Creating and managing buckets requires the object-storage management permission on the project. Organization Owners and Admins have it; other members get it through group permissions.
Create a bucket
- Open the Environment — for example Organization
acme→ Projectshop→ Environmentstaging— and click Add Resource. - Choose Object Storage, and give it a name, such as
media. The platform’s S3 bucket name combines the environment and this name and must fit in 63 characters, so a very long name is refused with the longest one that fits. - Pick a Storage tier:
- Capacity (the default) — large, low-cost, single-node storage. Durable against disk failure, but not against a whole node being lost, and not backed up. A good fit for assets you can regenerate or re-upload.
- High availability — replicated storage that survives node loss. Use it when service must survive node loss. Replication does not recover accidental deletion; review durability and recovery and keep independent copies of irreplaceable files. The tier decides which of your plan’s two object-storage pools the bucket’s quota counts toward (see Plans, Limits & Usage). You aren’t locked into this choice — see Moving a bucket to a different tier below.
- Optionally set a Bucket Quota (GB) — a cap on the total data the bucket can hold. Leave it blank for the 10 GB default.
- Click Create Resource.
Provisioning takes a moment; until it finishes, the bucket’s pages show
“Bucket is still being provisioned.” The underlying S3 bucket name is
generated by the platform — you’ll find it on the resource’s Overview tab
or via paas buckets info.
The High availability tier may not yet be offered in Production-type Environments on your installation, and either tier can be unavailable; creating one fails with a clear error.
From the CLI, paas buckets create does the same thing:
paas buckets create media -o acme -p shop -e staging --tier ha
paas buckets list -o acme -p shop -e staging
paas buckets delete media -o acme -p shop -e staging --confirm media
--tier capacity|ha picks the starting tier, same as the dashboard’s
Storage tier field (default capacity) — see Moving a bucket to a
different tier to change it later;
--quota-gb is the same as the
dashboard’s Bucket Quota (GB) field. --public makes the bucket public
immediately instead of leaving it private until you flip visibility
separately — for an ha bucket, whose storage isn’t ready the instant it’s
created, the CLI retries for a few seconds and tells you to run
paas buckets visibility public by hand if it’s still not ready. paas buckets delete asks you to type the name to confirm; pass --confirm <NAME>
in scripts.
Moving a bucket to a different tier
The tier you pick at creation isn’t permanent. Move a bucket between Capacity and High availability at any time from its Settings tab — Change storage tier — or from the CLI:
paas buckets migrate media --tier ha -o acme -p shop -e staging
The CLI asks you to type the bucket’s name to confirm; pass --yes in a
script, and add --wait to have the CLI poll until the move finishes instead
of returning right away.
What happens. The move runs in the background:
- Objects are copied to the new tier while the bucket keeps serving normally.
- Near the end, writes to the bucket are paused for a few minutes (reads keep working) while the final copy is checked; uploads during that window are rejected, so retry them afterwards.
- The bucket switches over to the new tier, and any app attached to it restarts automatically to pick up the new connection details.
- A last check confirms the move.
The bucket’s name and public URL never change. The Overview and
Settings tabs show the move’s progress (copying, finishing up, switching
over, verifying) while it runs; paas buckets list/info show it as
<tier> -> <target tier> (<phase>) in the tier column. Capacity and High
availability draw from separate pools in your plan’s object-storage quota
(see Plans, Limits & Usage) — once a move
finishes, the bucket’s quota counts against its new tier’s pool instead of
its old one. A move is refused up front (the same “limit exceeded” error as
creating a bucket) if the bucket’s quota doesn’t fit in the target tier’s pool.
A bucket can’t be deleted, and a second move can’t be started, while one is
already in progress.
If a move fails, the bucket keeps serving from its current tier — nothing is lost. In the dashboard, an alert on the bucket’s Overview and Settings tabs offers a way to resolve it:
- If the move hadn’t switched over yet: Try again retries the same move,
or Keep on
<current tier>gives up and cleans up the partial copy. - If the move had already switched over and only the final check failed: Retry final check re-runs just that check — the bucket is already on the new tier.
From the CLI, re-run paas buckets migrate with the same --tier to retry,
or with the bucket’s current tier to give up (only possible before it has
switched over).
Browse files in the dashboard
The bucket’s Files tab is an object browser:
- Folders come from key prefixes — click a folder to descend, and use the breadcrumb trail to jump back up. Click Load more if a listing is truncated.
- Upload puts a file (one at a time) into the current folder. Uploads go straight to storage over a short-lived signed link.
- Download opens the object in a new tab, also via a short-lived link, so it works whether the bucket is public or private.
- Copy URL appears on each file when the bucket is public — it copies the object’s permanent public URL.
- Delete permanently removes the object after a confirmation.
Anyone who can view the project can browse and download; uploading and deleting require the object-storage management permission.
The CLI lists objects too, optionally under a prefix:
$ paas -o acme -p shop -e staging buckets ls -a media
KEY SIZE MODIFIED
avatars/ - -
banner.png 48213 2026-08-30T14:02:11Z
$ paas buckets ls avatars/ -a media
For bucket commands, -a names the Object Storage resource. If a listing has
more results than one page, the CLI prints a note (pagination is not yet
supported there — use the dashboard’s Load more).
paas buckets put and get move files the same way the dashboard’s
Upload and Download buttons do — straight between your machine and
storage, over a short-lived signed link:
$ paas buckets put ./banner.png -a media
Uploaded ./banner.png to banner.png (image/png)
$ paas buckets get banner.png ./local-banner.png -a media
Downloaded banner.png to ./local-banner.png
$ paas buckets rm avatars/old.png -a media
Deleted avatars/old.png
put guesses the object’s content type from the file’s extension (text/html,
image/png, and so on, falling back to application/octet-stream); pass
--content-type to set it explicitly, which matters most for a public
bucket serving a website — the wrong type can make a browser download an
.html file instead of rendering it. get’s destination defaults to the
key’s base name (banner.png for a key of avatars/banner.png); pass - to
write the object’s bytes straight to stdout instead of a file. A download
that fails partway never touches an existing file at the destination.
To upload into a folder, give put a key:
paas buckets put ./alice.png avatars/alice.png -a media stores it under that
key; without one, the key is the file’s name.
Public URL and visibility
Every bucket gets a generated public hostname of its own. Buckets start private: nothing is readable without credentials. Flip visibility on the resource’s Overview tab — Make public asks you to confirm, because anyone with the URL can then read every object in the bucket. Making a bucket public never allows writes, and it does not let anyone anonymously list the bucket’s contents — objects are fetched by their exact key.
While public, an object is served at the bucket URL plus its key:
<bucket-url>/avatars/alice.png. The Files tab’s Copy URL button
builds these links for you.
From the CLI:
$ paas -o acme -p shop -e staging buckets url -a media
https://<generated-name>.<storage-domain>
$ paas buckets visibility public -a media
public: true
Warning: anyone with the URL can read every object in this bucket.
$ paas buckets visibility private -a media
paas buckets url exits with an error if no public hostname has been
assigned yet. Visibility changes are recorded in the organization’s
audit log.
If the bucket (or its Organization, Project or Environment) goes over its
monthly transfer allocation,
public access is switched off until usage is back under budget: Visibility
reads Private — blocked, and the CLI shows
private (blocked; public once unblocked) — see
Troubleshooting.
S3 credentials via the CLI
To use your own S3 tooling — sync scripts, backups, an S3 GUI —
paas buckets creds prints the bucket’s credentials as shell export
statements (it requires the object-storage management permission, and each
read is audit-logged):
$ paas buckets creds -a media
export AWS_ACCESS_KEY_ID='...'
export AWS_SECRET_ACCESS_KEY='...'
export S3_ENDPOINT='https://...'
export S3_BUCKET='...'
export S3_REGION='us-east-1'
# eval "$(paas buckets creds -a media)" then use any S3 client with --endpoint-url "$S3_ENDPOINT"
Load them into your shell and point any S3 client at the endpoint:
eval "$(paas buckets creds -a media)"
aws s3 ls "s3://$S3_BUCKET" --endpoint-url "$S3_ENDPOINT"
The only --format supported is env (the default). Add --json to any
paas buckets command for machine-readable output, and use
paas buckets info -a media for a one-screen summary (availability, bucket
name, endpoint, region, tier, visibility, URL).
Attaching a bucket to an app
Attach a bucket to a Web Application or Background Worker in the same Environment from the app’s Config tab: the attach card lists both Databases and buckets (each type-labeled), and attaching a bucket requires the object-storage management permission. See Attaching a Database for the shared card — the same flow works for buckets, except the second field is a credential prefix rather than a variable name.
A bucket attached with no prefix — the app’s primary bucket — injects five SDK-standard environment variables, which most S3 client libraries pick up directly. Capacity buckets work with default client settings. HA buckets need path-style addressing (see Configuring your S3 client):
| Variable | Contents |
|---|---|
S3_ENDPOINT | The bucket’s S3 endpoint URL. |
S3_BUCKET | The bucket name. |
S3_REGION | The region to pass to S3 clients. |
AWS_ACCESS_KEY_ID | The access key for this bucket. |
AWS_SECRET_ACCESS_KEY | The secret key for this bucket. |
Configuring your S3 client
Path-style addressing for HA buckets. Capacity buckets accept both
forms, so SDK defaults work. HA buckets need the bucket in the path
(https://<endpoint>/<bucket>/<key>), not in the host name. Many AWS SDKs
default to the host-name form when the endpoint is a name, which fails with
“host not found” for HA buckets. Turn path-style on:
| Client | Setting |
|---|---|
| AWS SDK for JavaScript v3 | new S3Client({ endpoint, forcePathStyle: true }) |
| AWS SDK for Go v2 | s3.NewFromConfig(cfg, func(o *s3.Options) { o.UsePathStyle = true }) |
| boto3 (Python) | Config(s3={"addressing_style": "path"}) |
| AWS SDK for Rust | aws_sdk_s3::config::Builder::from(&cfg).force_path_style(true) |
| AWS SDK for Java v2 | S3Client.builder().forcePathStyle(true) |
| AWS CLI | addressing_style = path under s3 = in the config profile |
Checksum settings. An app with a capacity bucket attached also gets
AWS_REQUEST_CHECKSUM_CALCULATION=WHEN_REQUIRED and
AWS_RESPONSE_CHECKSUM_VALIDATION=WHEN_REQUIRED. Capacity storage now
accepts every SDK’s default checksums, so these are only a compatibility
default. A value you set yourself (paas config set) takes precedence.
To attach a second bucket to the same app, give it a prefix (say
ASSETS): the five variables are then injected as ASSETS_ENDPOINT,
ASSETS_BUCKET, ASSETS_REGION, ASSETS_ACCESS_KEY_ID, and
ASSETS_SECRET_ACCESS_KEY, so they can’t collide with the primary bucket’s.
An app has at most one prefix-less bucket, and each bucket attaches once per
app.
Every attachment also injects a status link — STATUS_URL for the primary
bucket, <PREFIX>_STATUS_URL otherwise — a public, unauthenticated URL that
reports the bucket’s status and stays fixed for the bucket’s lifetime. It
returns {"state": "active" | "read_only", "tier": …} — read_only while
writes are paused during a tier move,
so your app can check it rather than fail uploads.
You can confirm the variables reached the app with a one-off command:
paas -o acme -p shop -e production -a web run -- printenv S3_ENDPOINT
Use CLI 0.9.0 or later to select the app explicitly. Omit --prefix for the primary bucket; use a distinct bucket and prefix for a second attachment:
paas apps attach web media -o acme -p shop -e production
paas apps attach web assets --prefix ASSETS -o acme -p shop -e production
paas apps attachments web -o acme -p shop -e production
paas apps detach web ATTACHMENT_ID -o acme -p shop -e production
Replace ATTACHMENT_ID with the ID from the attachment list. Detaching leaves the bucket intact. See app attachment commands and the older CLI aliases.
Next steps
- Attaching a Database — the same attachment model, for databases
- Managing Secrets — keep credentials out of your code
- Plans, Limits & Usage — how quotas count against your plan
- Access Control — who can manage object storage