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.

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:

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

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

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.

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

      TypeNameTarget
      CNAMEyour hostname, e.g. assetsdomains.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.

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.

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.

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.

SituationWarning sentRemoved
Added but never finished setting up7 days after it was added7 days after the warning
Stopped working and stayed downAfter 7 days down7 days after the warning
Plan changed to one without custom domainsWhen the plan changes7 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.

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 monthWhat happens
80% of the allowanceThe team owner gets an email. Files keep serving.
100% of the allowanceAnother email. Files keep serving.
125% of the allowancePublic 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.

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.

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.

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.