Skip to content

Buckets

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

  1. Open the dashboard 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 and you’ll get a fresh secret to copy.

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). Enabling public-read requires a paid plan.

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.

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.

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:

FieldWhere it shows up in the dashboardNotes
Endpoint”S3 configuration” card, Endpoint rowSame for every bucket on the platform. Always https://storage.zevcloud.net.
RegionRegion rowRequired by AWS SDKs for signing. Use exactly the value shown.
BucketBucket rowThe full bucket name, including the platform prefix. Copy this verbatim.
Access Key IDAccess Key ID rowVisible to team owners and admins. Other members see a “credentials hidden” stub.
Secret Access KeyShown ONCE at bucket creation and again at every key rotationNot stored in a way ZevCloud can read back. If you lose it, rotate the key to get a fresh one.

JavaScript (AWS SDK v3)

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.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
},
forcePathStyle: true,
})
await s3.send(new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: 'images/avatar.png',
Body: imageBuffer,
ContentType: 'image/png',
}))

Python (boto3)

import boto3
s3 = boto3.client(
's3',
endpoint_url='https://storage.zevcloud.net',
region_name='eu-central-1',
aws_access_key_id=os.environ['S3_ACCESS_KEY_ID'],
aws_secret_access_key=os.environ['S3_SECRET_ACCESS_KEY'],
)
s3.put_object(
Bucket=os.environ['S3_BUCKET'],
Key='images/avatar.png',
Body=image_bytes,
ContentType='image/png',
)

AWS CLI

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.

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:

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.

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:

    VariableValue
    S3_ENDPOINTRegion-scoped S3 endpoint URL
    S3_REGIONThe bucket’s region
    S3_BUCKETThe full bucket name
    S3_ACCESS_KEY_IDBucket’s active access key ID
    S3_SECRET_ACCESS_KEYBucket’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.

  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.

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

import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const s3 = new S3Client({
endpoint: process.env.S3_ENDPOINT,
region: process.env.S3_REGION,
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
},
forcePathStyle: true,
});
// Upload
await s3.send(new PutObjectCommand({
Bucket: process.env.S3_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.S3_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.

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

Section titled “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.

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

SurfaceLooks likeUse forAuth
S3 endpoint (under “S3 configuration”)https://storage.zevcloud.netProgrammatic 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

Section titled “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.

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

<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

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:

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.

If your frontend uploads directly to ZevCloud Storage using presigned URLs from your backend, you’ll need CORS configured on the bucket. Set CORS rules through any S3-compatible tool against the S3 endpoint, the way you would against any other S3 provider. Example with the AWS CLI:

aws s3api put-bucket-cors \
--endpoint-url https://storage.zevcloud.net \
--bucket <bucket> \
--cors-configuration file://cors.json

A reasonable starting cors.json that allows browser PUT uploads from your app’s origin:

{
"CORSRules": [
{
"AllowedOrigins": ["https://yoursite.com"],
"AllowedMethods": ["GET", "PUT", "HEAD"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3600
}
]
}

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.

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:

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.

Instead of the default URL, you can serve the bucket’s objects from your own hostname like assets.yoursite.com.

  1. Open the bucket detail page and click Custom domains.

  2. Click Attach 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 automatically. The domain is active right away.

    • Domain registered elsewhere. Type the hostname you want to use. The dashboard shows the CNAME target you need to point the hostname at. Set the CNAME with your DNS provider, come back to the dashboard, and click Verify. Verification is rate-limited, so wait a minute between attempts if your DNS hasn’t propagated yet.

  4. Once verified, the domain serves objects from the bucket. SSL is handled for you.

To detach: click Detach on the domain row. Traffic on that hostname stops resolving to the bucket immediately. You can leave the CNAME pointing at the platform (nothing breaks if you do) or remove it on your DNS side.

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.

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.

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.

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.

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.