Uploading and Managing Objects

Table of Contents

  1. Bucket List and access keys
  2. Bucket settings
  3. User Management (S3 sub-users)
  4. storage-hello: uploading via the S3 API
  5. 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:

  1. If you have no sub-users yet, click ADD, or use the floating ADD USER button.
  2. 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).
  3. 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 region for 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!