Uploading and Managing Objects
Table of Contents
- Bucket List and access keys
- Bucket settings
- User Management (S3 sub-users)
- storage-hello: uploading via the S3 API
- Beginner Pitfalls
1. Bucket List and access keys
Once you have a project, the Storage page shows four tabs — Bucket List, Transcoder, Streaming, User Management — plus a PLAN SETTING button (top right). This chapter covers Bucket List and User Management; Transcoder and Streaming aren't covered yet.
The Bucket List tab shows:
- Usage — your current billing cycle's date range, plus Size and Data Transfer used against your plan's quota.
- Username — your project's S3 Access Key and Secret Key (masked by default), with an eye icon to show/hide them and a regenerate icon to issue a new key pair.
- Your project's name, with a CONSOLE button (opens in a new tab) and a card per bucket, each showing the bucket's name, a settings (gear) icon, a delete (trash) icon, and its public URL (with a copy icon).
- A floating CREATE BUCKET button to add another bucket, up to your plan's bucket limit.
Click PLAN SETTING to open a dialog showing your current plan (name, Active status, price, billing period) with an UPGRADE PLAN button.
2. Bucket settings
Click the settings (gear) icon on a bucket's card:
- Bucket Privacy — click Edit to choose Private (only you and your sub-users can access the objects) or Public (anyone with the object URL can read them).
- Bucket Policy — click Edit to set a raw JSON access policy
(S3-style
{"Version": "...", "Statement": [...]}). - CORS Configuration — click Edit to allow web applications on
other domains to interact with objects in this bucket: Origin
(allowed origins, added via Add Origin), Access Control Max Age
(second), Allowed Method checkboxes (
GET/PUT/POST/DELETE/HEAD), and Allowed Header (added via Add Header).
Each section saves independently via its own Edit dialog's SAVE button.
To delete a bucket, click the trash icon on its card — treat the confirmation as a standard confirm-to-delete dialog.
3. User Management (S3 sub-users)
The User Management tab issues additional, bucket-scoped S3 credentials instead of sharing your project's main Access Key/Secret Key:
- If you have no sub-users yet, click ADD, or use the floating ADD USER button.
- In the Add Sub-user S3 dialog: set an Access Key Name, then search for a bucket under Bucket & Permission to grant access to (selected buckets appear in a table with Bucket and Permission columns).
- Click SAVE.
4. storage-hello: uploading via the S3 API
A small Express app demonstrating S3-compatible upload, list, download, and delete against a bucket, using the AWS SDK pointed at Cloud+'s S3-compatible endpoint:
const { S3Client, PutObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectCommand } = require("@aws-sdk/client-s3");
const endpoint = process.env.STORAGE_ENDPOINT || "https://fsgw.sabay.com";
const bucket = process.env.STORAGE_BUCKET_NAME;
const region = process.env.STORAGE_REGION || "us-east-1"; // SDK requires one; Cloud+ ignores it
const forcePathStyle = process.env.STORAGE_FORCE_PATH_STYLE !== "false";
const s3 = new S3Client({
endpoint,
region,
forcePathStyle,
credentials: {
accessKeyId: process.env.STORAGE_ACCESS_KEY,
secretAccessKey: process.env.STORAGE_SECRET_KEY,
},
});
app.post("/api/objects", upload.single("file"), async (req, res) => {
await s3.send(new PutObjectCommand({
Bucket: bucket,
Key: req.file.originalname,
Body: req.file.buffer,
ContentType: req.file.mimetype,
}));
res.json({ key: req.file.originalname, size: req.file.size });
});
app.get("/api/objects", async (req, res) => {
const result = await s3.send(new ListObjectsV2Command({ Bucket: bucket }));
res.json({
objects: (result.Contents || []).map((obj) => ({
key: obj.Key,
size: obj.Size,
lastModified: obj.LastModified,
})),
});
});
STORAGE_ENDPOINT defaults to https://fsgw.sabay.com — the same host
used for programmatic S3 API access, not just public bucket URLs — with
forcePathStyle: true matching the confirmed <endpoint>/<bucket-name>
URL shape. STORAGE_ACCESS_KEY, STORAGE_SECRET_KEY, and
STORAGE_BUCKET_NAME are required, with no defaults, and come from the
Bucket List tab and the Bucket Information step above.
Run it locally:
nvm use && npm install
STORAGE_ACCESS_KEY="your-access-key" \
STORAGE_SECRET_KEY="your-secret-key" \
STORAGE_BUCKET_NAME="your-bucket-name" \
npm run dev
Visit http://localhost:3000, upload a file, then download or delete it
from the list. To deploy it to Run App: build for linux/amd64, push to a
registry, then follow
Part 1 to
create a stack from that image, setting STORAGE_ACCESS_KEY,
STORAGE_SECRET_KEY, and STORAGE_BUCKET_NAME as Shared Env keys.
This is a teaching example — no auth, no request validation beyond Express/multer defaults, no retry logic. It's not a template for a production upload service.
5. Beginner Pitfalls
- Sharing the project's main Access Key/Secret Key everywhere. Prefer a scoped sub-user (Section 3) for any app that only needs access to one bucket.
- Omitting a
regionfor the S3 client. The AWS SDK requires one even though Cloud+ Storage ignores its value — leaving it out entirely causes a client-side error, not a server response. - Assuming a bucket is publicly readable by default. Check Bucket Privacy under bucket settings — it must be explicitly set to Public for anyone with the object URL to read it.
- Not handling the delete-bucket confirmation as verified. This series hasn't walked through that exact dialog yet — treat it as a standard confirm-to-delete step until confirmed otherwise.
Buy Me a Coffee
Found this tutorial helpful? Consider buying me a coffee to support more free content!