---
title: Buckets
description: Create, connect, and manage ZevCloud Storage buckets. Public vs private, connecting to services, custom domains, key rotation, and lifecycle.
---

A bucket is the unit you create, name, and pay against in ZevCloud Storage. This page walks through everything you can do with one: create it, connect it to a service, attach a custom domain, rotate its key, and tear it down.

If you're new to the storage feature, start with the [Object Storage overview](/guide/storage) first.

## Creating a bucket

1. Open the [dashboard](https://console.zevcloud.net) and click **Storage** in the left nav.

2. Click **Create bucket**. The form asks for four things:

   - **Name suffix.** The piece you choose; the platform prefixes your team slug to it so bucket names stay unique across the whole platform without you needing to coordinate with anyone. Allowed: lowercase letters, digits, and hyphens. Maximum length is the same as S3's.
   - **Region.** Pick the region closest to where your app runs or where your users are. If you're connecting this bucket to a ZevCloud service, picking the same region keeps the traffic inside one data centre.
   - **Public read.** Off for private buckets (uploads from your app, backups, anything that should require credentials to read). On for buckets whose objects should be loadable from a browser at a stable URL. Only available on paid plans.
   - **Object Lock.** Off by default. Turn on for compliance workloads where data must not be overwritten or deleted within a retention period. **This is permanent**. Object Lock cannot be turned off later on the same bucket.

3. Click **Create**. The bucket lands in `active` status within a few seconds and the create response includes the access key ID and a one-time secret.

4. **Copy the secret access key now.** The dashboard shows it once. The access key ID stays visible on the detail page; the secret never comes back.

If you closed the tab without copying the secret, no problem. [Rotate the key](#rotating-keys) and you'll get a fresh secret to copy.

### Picking public-read vs private

This decision is at create time, so it's worth thinking through.

- **Private** is the default and the right answer for most things. App uploads, document storage, backups, build artifacts, anything where you don't want random people fetching your objects without credentials. Your app and other authorised tools read and write using the access key.

- **Public-read** makes every object in the bucket fetchable at a stable URL with no credentials, the same way an asset on a CDN works. Right answer for: marketing site images, video files, public PDFs and downloads, any static file you want loaded directly by a browser. Writes still require the access key. Only reads become public.

You can flip a bucket between private and public-read after creation from the bucket detail page (see [Toggling public-read on an existing bucket](#toggling-public-read-on-an-existing-bucket)). Enabling public-read requires a paid plan.

### Bucket names: what to expect

The dashboard checks availability as you type. The name suffix you pick becomes part of the full bucket name, which is what you'll see in tools and use in your SDK calls. The "Bucket name" line on the detail page is what to copy into your S3 client, not the suffix you typed.

If the suffix you want is already taken by another team's bucket on the platform, the dashboard tells you up front and the create flow appends a short unique tail so the create still succeeds. Your bucket name might end up with a tail like `-abcd` in that case.

## Listing and inspecting buckets

Open **Storage** in the dashboard's left nav. The Buckets tab shows every bucket on the team, with current status, region, size, and a quick "owner can view credentials" badge.

Click any bucket to open its detail page. You get:

- **Bucket name and endpoint** to plug into S3 clients.
- **Access key ID** (owners and admins only).
- **Usage** as a percentage of the team's plan cap.
- **Objects tab** for browsing, uploading, and downloading directly.
- **Connected services tab** for seeing which of your app services have this bucket wired in.
- **Custom domains tab** for attaching your own hostnames (paid plans).
- **Settings tab** for rotation, deletion, and the public-read state.

The **Refresh usage** button on the detail page recalculates the bucket size right now. The dashboard's usage counter otherwise updates on a short interval, so if you just uploaded or deleted a lot of data through an external tool the displayed size may be stale until the next refresh.

## Authenticating with the S3 endpoint

ZevCloud Storage speaks the standard S3 API. Any S3-compatible client works: AWS SDKs (boto3, aws-sdk for JavaScript, etc.), the AWS CLI, `rclone`, `mc`, `s3cmd`, anything that takes an endpoint URL.

You need four values, all from the bucket's detail page:

| Field | Where it shows up in the dashboard | Notes |
|-------|------------------------------------|-------|
| **Endpoint** | "S3 configuration" card, Endpoint row | Same for every bucket on the platform. Always `https://storage.zevcloud.net`. |
| **Region** | Region row | Required by AWS SDKs for signing. Use exactly the value shown. |
| **Bucket** | Bucket row | The full bucket name, including the platform prefix. Copy this verbatim. |
| **Access Key ID** | Access Key ID row | Visible to team owners and admins. Other members see a "credentials hidden" stub. |
| **Secret Access Key** | Shown ONCE at bucket creation and again at every key rotation | Not stored in a way ZevCloud can read back. If you lose it, rotate the key to get a fresh one. |

> **Warning: The secret is shown once**
>
> Capture the secret access key when the dashboard displays it after bucket creation or rotation. ZevCloud encrypts it at rest and never exposes the plaintext again. If you missed it, click **Rotate key** on the bucket detail page to mint a fresh pair. The old key keeps working for 24 hours so anything currently using it has time to update.

### Code examples

**JavaScript (AWS SDK v3)**

```js
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3'

const s3 = new S3Client({
  endpoint: 'https://storage.zevcloud.net',
  region: 'eu-central-1',
  credentials: {
    accessKeyId: process.env.ZEVSTORAGE_ACCESS_KEY_ID,
    secretAccessKey: process.env.ZEVSTORAGE_SECRET_ACCESS_KEY,
  },
  forcePathStyle: true,
})

await s3.send(new PutObjectCommand({
  Bucket: process.env.ZEVSTORAGE_BUCKET,
  Key: 'images/avatar.png',
  Body: imageBuffer,
  ContentType: 'image/png',
}))
```

**Python (boto3)**

```python
import boto3

s3 = boto3.client(
    's3',
    endpoint_url='https://storage.zevcloud.net',
    region_name='eu-central-1',
    aws_access_key_id=os.environ['ZEVSTORAGE_ACCESS_KEY_ID'],
    aws_secret_access_key=os.environ['ZEVSTORAGE_SECRET_ACCESS_KEY'],
)

s3.put_object(
    Bucket=os.environ['ZEVSTORAGE_BUCKET'],
    Key='images/avatar.png',
    Body=image_bytes,
    ContentType='image/png',
)
```

**AWS CLI**

```bash
export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
export AWS_REGION=eu-central-1

aws s3 ls s3://<bucket>/ --endpoint-url https://storage.zevcloud.net
aws s3 cp ./file.png s3://<bucket>/images/avatar.png --endpoint-url https://storage.zevcloud.net
```

You'll likely want a shell function or alias since the endpoint flag is repetitive.

### A few things worth knowing

**Use path-style addressing.** Set `forcePathStyle: true` (JavaScript) or `s3.client.config.path_style = True` (boto3 by default works). Path-style means URLs look like `https://storage.zevcloud.net/<bucket>/<key>` rather than `<bucket>.storage.zevcloud.net/<key>`. Path-style is what ZevCloud Storage expects.

**The region value is required even though there's one region today.** AWS SigV4 (the signing algorithm every S3 client uses) bakes the region into the signature. The value must match what the bucket detail page shows. Don't make one up.

**Presigned URLs work for time-limited shares from private buckets.** When you don't want to make a bucket fully public but want to hand a single object to a browser temporarily, mint a presigned GET URL on your backend and return it to the client:

```js
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'

const url = await getSignedUrl(
  s3,
  new GetObjectCommand({ Bucket: '<bucket>', Key: 'private/report.pdf' }),
  { expiresIn: 300 }, // 5 minutes
)
```

The URL works in a browser without any auth header and stops working when it expires.

**Credentials hidden for members.** If you're a team member (not owner or admin), the dashboard hides the Access Key ID and never shows the secret. Ask an owner or admin for the credentials, or have them rotate on your behalf.

## Connecting a bucket to a service

The cleanest way for a ZevCloud-deployed app to use a bucket is the **Connect storage bucket** flow on the service's Variables tab. This injects the bucket's credentials as managed environment variables and keeps them in sync with key rotations automatically.

1. Open the service in the dashboard. Click **Variables**.

2. Click **Connect storage bucket**. Pick the bucket from the dropdown.

3. Five managed environment variables now appear on the service:

   | Variable | Value |
   |----------|-------|
   | `ZEVSTORAGE_ENDPOINT` | Region-scoped S3 endpoint URL |
   | `ZEVSTORAGE_REGION` | The bucket's region |
   | `ZEVSTORAGE_BUCKET` | The full bucket name |
   | `ZEVSTORAGE_ACCESS_KEY_ID` | Bucket's active access key ID |
   | `ZEVSTORAGE_SECRET_ACCESS_KEY` | Bucket's active secret access key |

   These are marked as managed in the dashboard. You can't edit them by hand. The platform owns the values; if you rotate the bucket's key, these update automatically.

   Alongside each one, the same value is also published under an older `S3_` name (`S3_BUCKET`, `S3_ACCESS_KEY_ID`, and so on). Those names came first and still work, so nothing you have already deployed needs changing. Use the `ZEVSTORAGE_` names in new code: ZevStorage speaks the S3 protocol, but the storage is ours, not Amazon's, and the clearer name saves the next person reading your config from guessing.

4. Redeploy the service so the runtime picks up the new env. From this point on, your app code can use the variables exactly as it would with any S3 SDK.

### Connecting more than one bucket

A service can hold one main bucket plus any number of named ones. Give the second bucket a name when you connect it, and that name becomes part of its variables:

| Variable | Value |
|----------|-------|
| `ZEVSTORAGE_UPLOADS_ENDPOINT` | Region-scoped endpoint URL |
| `ZEVSTORAGE_UPLOADS_REGION` | The bucket's region |
| `ZEVSTORAGE_UPLOADS_BUCKET` | The full bucket name |
| `ZEVSTORAGE_UPLOADS_ACCESS_KEY_ID` | Bucket's active access key ID |
| `ZEVSTORAGE_UPLOADS_SECRET_ACCESS_KEY` | Bucket's active secret access key |

Names are upper-case letters, numbers and underscores, starting with a letter, up to 24 characters. `UPLOADS`, `BACKUPS` and `USER_MEDIA` all work.

A named bucket gets no `S3_` mirror, because there is no existing code to keep working. Only the main bucket carries the older names.

> **Note:**
>
> The dashboard asks for a name as soon as a service already has a main bucket. Two unnamed buckets would write to the same five variables, so whichever connected last would quietly take over the first one's settings inside the container.

Code-side, you just read the env vars. Full example with the AWS SDK for JavaScript:

```js
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const s3 = new S3Client({
  endpoint: process.env.ZEVSTORAGE_ENDPOINT,
  region: process.env.ZEVSTORAGE_REGION,
  credentials: {
    accessKeyId: process.env.ZEVSTORAGE_ACCESS_KEY_ID,
    secretAccessKey: process.env.ZEVSTORAGE_SECRET_ACCESS_KEY,
  },
  forcePathStyle: true,
});

// Upload
await s3.send(new PutObjectCommand({
  Bucket: process.env.ZEVSTORAGE_BUCKET,
  Key: 'uploads/user-123/avatar.png',
  Body: imageBuffer,
  ContentType: 'image/png',
}));

// Presigned read URL the browser can fetch (private bucket)
const url = await getSignedUrl(
  s3,
  new GetObjectCommand({
    Bucket: process.env.ZEVSTORAGE_BUCKET,
    Key: 'uploads/user-123/avatar.png',
  }),
  { expiresIn: 3600 },
);
```

`boto3` and every other S3 SDK follow the same pattern with the same four config values.

### Disconnecting

On the service's Variables tab, click the connected bucket entry and choose **Disconnect**. The five managed env vars are removed. The bucket itself is untouched. Your app loses access; redeploy after disconnecting if it needs to start cleanly without those vars.

### Connecting one bucket to multiple services

Yes. Connect it from each service separately. Each connection injects the same managed env vars, all pointing at the same bucket and key pair. A key rotation updates every connected service simultaneously.

### Connecting several buckets to one service

Also yes, and you no longer need to enter the second one's credentials by hand. Connect it the same way and give it a name, and it arrives as its own set of managed variables under that name. See [Connecting more than one bucket](#connecting-more-than-one-bucket) for the naming rules.

If you already worked around this with hand-entered variables, they keep working, but they are outside rotation. Connecting the bucket properly and dropping the manual copies is worth the one code change to the new variable names.

## Public buckets and custom domains

Every public-read bucket exposes its objects at **two different URLs**. They do different jobs and a typical integration uses both.

### The S3 endpoint vs the public URL

| Surface | Looks like | Use for | Auth |
|---------|-----------|---------|------|
| **S3 endpoint** (under "S3 configuration") | `https://storage.zevcloud.net` | Programmatic access from your app or scripts using an S3 SDK. Uploads, deletes, listing, anything that needs auth or logic. | Required (access key + secret) |
| **Public URL** (under "Public URL") | `https://cdn.zevcloud.net/<bucket-name>/` | Loading objects directly in a browser. `<img src>`, video players, download links, anywhere you want a plain HTTP fetch. | None. Anyone with the URL can read. |

The S3 endpoint never changes between private and public buckets. It's how every authenticated S3 client talks to ZevCloud Storage regardless of bucket visibility. The public URL only appears for public-read buckets and only resolves while the bucket is public AND the team is on a paid plan.

A typical integration: your backend uploads `images/avatar.png` to a public bucket using the S3 endpoint plus credentials. Your frontend then loads it from `https://cdn.zevcloud.net/<bucket-name>/images/avatar.png` with no token and no signing. Two URLs, one bucket, different purposes.

If a bucket is private, the public URL section is hidden. The only way to read objects is via the S3 endpoint with credentials, or by minting a short-lived presigned URL from your backend.

### Toggling public-read on an existing bucket

You don't need to create a new bucket to change visibility. From the bucket detail page:

1. Open the bucket. The header row shows the current state (Public-read or Private) next to the region.

2. Click **Make public** or **Make private** in the action row.

3. Confirm in the modal. The dashboard explains what changes. Flipping to public means anyone with the URL can read every object. Flipping to private means saved public URLs stop working immediately. Apps using S3 credentials are not affected either way.

4. The change applies instantly. If you went public, the Public URL section appears with your base URL. If you went private, that section disappears and public requests stop serving on the next request.

Public-read requires a paid plan. On the Free plan the button opens an upgrade prompt instead of flipping the bucket.

### Using the public URL in your app

The public URL is a plain HTTP URL. Any client that can fetch HTTP can read it. A few practical patterns:

**HTML image and download links**

```html
<img src="https://cdn.zevcloud.net/<bucket>/images/avatar.png" alt="Avatar" />
<a href="https://cdn.zevcloud.net/<bucket>/files/whitepaper.pdf">Download</a>
<video controls src="https://cdn.zevcloud.net/<bucket>/videos/demo.mp4"></video>
```

**Frontend fetch from JavaScript**

```js
const res = await fetch('https://cdn.zevcloud.net/<bucket>/data/menu.json')
const menu = await res.json()
```

No SDK, no signing, no headers. The bucket name goes after the host; the object key goes after that. Object keys can contain slashes and are treated as the full path.

**Range requests work** for partial downloads and HTML5 video seek:

```bash
curl -H "Range: bytes=0-1023" https://cdn.zevcloud.net/<bucket>/videos/demo.mp4
```

Returns `HTTP/2 206 Partial Content` with the requested byte range.

**ETag and Last-Modified are returned** so conditional requests (`If-None-Match`, `If-Modified-Since`) get `304 Not Modified` responses and your caching layer can skip transferring unchanged objects.

### CORS for browser uploads

If your frontend uploads directly to ZevCloud Storage using presigned URLs from your backend, you'll need CORS configured on the bucket.

Configure it in the dashboard: **Storage → your bucket → CORS**. Add a rule with your app's origin, the methods you need (`GET` and `PUT` for presigned uploads), and save.

Your bucket access key cannot set CORS itself — `put-bucket-cors` and `get-bucket-cors` return `AccessDenied` for bucket keys by design. Bucket-level configuration is applied by the platform on your behalf, which keeps your key scoped to reading and writing objects rather than reconfiguring the bucket. Use the dashboard, or the API:

```bash
curl -X PUT https://api.zevcloud.net/v1/storage/buckets/<bucketId>/cors \
  -H "Authorization: Bearer <token>" \
  -H "x-team-id: <teamId>" \
  -H "Content-Type: application/json" \
  -d '{
    "rules": [
      {
        "allowedOrigins": ["https://yoursite.com"],
        "allowedMethods": ["GET", "PUT", "HEAD"],
        "allowedHeaders": ["*"],
        "maxAgeSeconds": 3600
      }
    ]
  }'
```

A browser always sends an `OPTIONS` preflight before a `PUT` whose content type isn't CORS-simple — which includes `image/*` and `application/json`. Without a matching rule the preflight fails and the upload never leaves the browser.

GET requests for public-read objects don't strictly need CORS unless you're using `fetch()` with credentials or reading response bodies in a Worker. Browsers happily load `<img>` and `<video>` from any origin without CORS.

#### Presigned uploads and the AWS SDK checksum default

If you generate presigned upload URLs with `@aws-sdk/client-s3` **v3.729 or newer**, set `requestChecksumCalculation: 'WHEN_REQUIRED'` on the client:

```js
const s3 = new S3Client({
  endpoint: 'https://storage.zevcloud.net',
  region: '<region>',
  credentials: { accessKeyId, secretAccessKey },
  forcePathStyle: true,
  requestChecksumCalculation: 'WHEN_REQUIRED',
})
```

From v3.729 the SDK defaults to `WHEN_SUPPORTED`, which stamps a CRC32 checksum into the presigned URL. The browser that later uses that URL never computes one, so the upload is rejected:

```
400 InvalidDigest: Failed to validate checksum for algorithm Crc32
```

The error names the checksum, but the cause is the SDK default — the body was never hashed. `WHEN_REQUIRED` sends a checksum only where the operation actually needs one, which is what presigning wants.

Presigned URLs minted by ZevCloud's own API (`POST /v1/storage/buckets/:id/objects/upload-url`) are already generated this way and need no client-side change.

### Cache behavior and invalidation

Public URLs sit behind a CDN edge by default. Objects are cached according to standard HTTP cache headers. If you don't set any, the edge applies a sensible default TTL.

The simplest pattern for cacheable content: include a version hash in the object key when you upload, and use the versioned key in your HTML. `images/avatar-v3.png` instead of overwriting `images/avatar.png`. The browser and the edge both naturally treat the new URL as a fresh object, no purge required.

If you do need explicit cache control, set `Cache-Control` and `Expires` headers when you `PutObject`:

```js
await s3.putObject({
  Bucket: '<bucket>',
  Key: 'images/hero.jpg',
  Body: imageBuffer,
  ContentType: 'image/jpeg',
  CacheControl: 'public, max-age=31536000, immutable',
})
```

Headers set at upload time are returned on every subsequent fetch.

### Attaching a custom domain (paid plans)

Instead of the default URL, you can serve the bucket's objects from your own hostname like `assets.yoursite.com`. The bucket must be public-read.

1. Open the bucket detail page and find **Custom domains**.

2. Click **Add domain**.

3. Two paths depending on whether you bought the domain through ZevCloud:

   - **Domain registered with ZevCloud.** Pick it from the dropdown and choose a subdomain (`assets`, `cdn`, `files`, …). The platform writes the DNS record for you.

   - **Domain registered elsewhere.** Type the hostname you want to use, then create this record with your DNS provider:

     | Type | Name | Target |
     |---|---|---|
     | CNAME | your hostname, e.g. `assets` | `domains.zevcloud.net` |

     The target is also shown on the domain row until the domain is active.

4. The domain shows **Setting up** while DNS propagates and its HTTPS certificate is issued, then turns **Active** on its own, usually within a few minutes. Click **Verify** to check sooner.

Once active, an object at `images/logo.png` is served at `https://assets.yoursite.com/images/logo.png`. There is no bucket name in the path. The default public URL keeps working alongside it, and both appear under **Public URLs** on the bucket's Overview tab and in each file's details, ready to copy.

**Point the CNAME at `domains.zevcloud.net`, not at `cdn.zevcloud.net`.** A hostname CNAMEd at the public bucket URL is refused and never becomes active. If you set up a domain before this changed, update the record's target and click **Verify**.

If your DNS provider offers a proxy option on records, the CNAME works with it on or off. If the domain stays in **Setting up** for more than an hour, check that the CNAME is saved on the exact hostname you added and that no CAA record on your domain blocks certificate issuance.

To attach the root of a domain (`yoursite.com` rather than a subdomain), your DNS provider must support CNAME records at the root, sometimes called CNAME flattening or ALIAS records.

To detach: click **Detach** on the domain row. Requests on that hostname stop being served by the bucket immediately. Remove the CNAME on your DNS side afterwards.

#### Which plans include custom domains

Custom domains are a feature of selected paid storage plans, shown on each plan in the plan picker. The bucket also has to be public-read. On a plan without them, the **Domains** tab on the bucket page says so and links to the plans.

#### If a domain stops working

ZevCloud checks active custom domains every hour. If one stops working, usually because its DNS record was changed or deleted, the domain shows **Setting up** again with the reason, and the team owner gets an email. Restore the record and the domain becomes active again on its own. Your files are unaffected and stay available at their default URL the whole time.

#### Automatic removal

Unused custom domains are removed so they don't sit on a bucket indefinitely. The team owner is always emailed first, at least 7 days before anything is removed, and fixing the cause cancels the removal.

| Situation | Warning sent | Removed |
|---|---|---|
| Added but never finished setting up | 7 days after it was added | 7 days after the warning |
| Stopped working and stayed down | After 7 days down | 7 days after the warning |
| Plan changed to one without custom domains | When the plan changes | 7 days after the warning |

A domain scheduled for removal shows the date on its row. Removing a custom domain never touches the bucket or its files.

## Data transfer

Each storage plan includes a monthly data transfer allowance for public files. It resets on the 1st of each month (UTC).

**What counts:** data downloaded from your buckets' public URLs and custom domains.

**What doesn't:** repeat downloads of a public file served from our CDN cache, and uploads. Downloads made with an S3 SDK or the S3 API aren't counted yet either.

Your current usage is on the **Storage** page, and each bucket's own transfer this month is on its **Overview** tab.

| Usage this month | What happens |
|---|---|
| 80% of the allowance | The team owner gets an email. Files keep serving. |
| 100% of the allowance | Another email. Files keep serving. |
| 125% of the allowance | Public URLs and custom domains pause, and the owner is emailed. |

While paused, requests to public URLs and custom domains return `403`. Nothing is deleted, and files can still be read with the S3 API and presigned URLs. Serving resumes when the allowance resets on the 1st, or within about a minute of upgrading to a plan with more transfer.

The gap between 100% and 125% is there so a busy week doesn't take your files offline before you've seen the email.

### Many downloads at once

To keep the platform responsive, the number of downloads served at the same moment from any one bucket is capped. If a bucket goes over the cap, for example during a sudden traffic spike, some requests get `503 Service Unavailable` with a `Retry-After` header. Browsers, video players and download tools retry these on their own. Files that are already in the CDN cache aren't affected.

### Video

Public URLs and custom domains support range requests, so video players can seek and resume without downloading the whole file. For adaptive streaming across many connection speeds, transcode your video to HLS or DASH and upload the segments to a bucket.

## Rotating the access key

Rotate when you suspect exposure, when a teammate with the key leaves, or as routine hygiene.

1. Open the bucket detail page, go to **Settings**, click **Rotate key**.

2. The response includes the **new** access key ID and a one-time secret. Copy both now.

3. For 24 hours after rotation, **both** the old and new keys work. Any service or external tool using the old key keeps working during the grace window, giving it time to restart and pick up the new value.

4. After 24 hours the old key is revoked. Any client still using it gets an authentication error.

Connected services pick up the new key automatically. The managed env vars on the service update the moment the rotation succeeds. A redeploy isn't needed unless your app caches the credentials in memory at boot; in that case schedule a restart inside the 24-hour grace window.

For external clients (your laptop, CI runners, third-party tools), update the stored credentials within the 24-hour window. Anywhere the access key shows up is somewhere to update.

> **Warning: Hand-entered credentials do not follow rotation**
>
> This only applies to **managed** variables, the ones the Connect flow created. If you typed a bucket's access key into the Variables tab yourself, that value is yours to maintain: rotation will not touch it, and 24 hours later your app starts failing to authenticate with nothing to warn you. Connect the bucket instead and delete the hand-entered copies, so rotation reaches every service that uses it.

## Refreshing usage

Usage is recalculated on a short interval automatically. If you just made a large change through an external S3 client and the dashboard counter looks stale, click **Refresh usage** on the bucket detail page to recompute immediately.

## Retrying a bucket stuck in error

Rarely, a bucket lands in `error` status, most commonly because a transient issue interrupted provisioning. Open the bucket and click **Retry**. The platform re-runs the provisioning flow against the same row, preserving the name you picked. On success the bucket flips to `active` and you get a fresh secret access key in the response.

## Deleting a bucket

> **Warning: Bucket deletion is irreversible**
>
> Delete drains the objects first and then removes the bucket. There is no undo. If you might want the data later, copy it elsewhere with `rclone` or `mc mirror` first.

Owner or admin only. Open the bucket detail page, go to **Settings**, click **Delete bucket**, and confirm the bucket name. The deletion runs synchronously, which is fast on small buckets but can take a while on large ones. The dashboard shows progress.

If a service has the bucket connected, its managed env vars will fail to refresh after the bucket is gone. Disconnect the bucket from each service first, then delete.

## Switching plans

Open **Storage** in the dashboard and click your current plan badge. The plan picker shows every plan with the storage cap, public-read availability, and Naira price per month.

- Upgrading to a larger paid plan creates an invoice. Pay it via the link in the dashboard. The new plan takes effect the moment the invoice settles.
- Downgrading to a smaller plan is allowed if your current usage fits inside the smaller plan's cap. The picker disables ineligible destinations and tells you why.
- Switching to the Free plan disables public-read serving on any public bucket immediately. The platform stops responding to public URLs and saved links go cold. The bucket row keeps its `isPublicRead` flag, so re-upgrading to a paid plan restores serving automatically without re-toggling each bucket. Custom domains attached to public buckets also stop resolving while on Free.

Only downgrades are throttled. Switching to a smaller plan is limited to once per 24 hours to prevent the upgrade-use-downgrade flap pattern. Upgrades are never throttled, so you can move up tiers freely.

## Where to go next

- **[Object Storage overview](/guide/storage)** for concepts, what's included, and the billing model.
- **[Storage API](/api/storage)** for every endpoint with request and response shapes.
- **[Environment Variables](/guide/env-vars)** for the broader picture of managed vs user-defined env vars on services.
- **[Plans & Pricing](/guide/plans)** for current Naira prices per plan.
