# Storage



Clusterbase's object storage lets you keep static files (images, CSS, PDFs, etc.) on the edge CDN and serve them alongside your functions. Files live inside a **store** — a named bucket tied to your organization. You can have multiple stores per organization.

The command is available as `ccp store` (primary) or `ccp storage` (alias) — use whichever you prefer. A store can optionally be attached to a [Project](/docs/ccp/projects) or left standalone.

## Create a Store [#create-a-store]

```bash
ccp storage create my-assets
# ✓ Created store "my-assets"
```

A store needs to exist before you can upload files to it. When you run any storage command, CCP will use the `store_id` from your project's `.ccp/config.json` if present, or prompt you to select one.

## Upload Files [#upload-files]

```bash
ccp storage put logo.png styles.css
# ◼ Uploaded logo.png
#   https://assets.cluster.app/edge/serve/...
#
# ◼ Uploaded styles.css
#   https://assets.cluster.app/edge/serve/...
```

Multiple files upload in parallel. `upload` is an alias for `put` if you prefer.

## Download a File [#download-a-file]

```bash
ccp storage get logo.png
# ✓ Downloaded logo.png
```

Use `-o` / `--output` to save to a specific path:

```bash
ccp storage get logo.png --output ./assets/logo.png
```

## List Files [#list-files]

```bash
ccp storage ls
# NAME        SIZE     KIND
# logo.png    12.4 KB  media
# styles.css  3.2 KB   static
```

`list` is an alias for `ls`.

Each file prints on one line, its name carrying the served URL as a clickable OSC 8 hyperlink rather than printing it — unless output is piped or redirected, in which case the link is dropped in favor of a plain trailing `URL` column so the address stays copyable. Pass `--json` for full filenames, exact sizes, and complete URLs with no styling.

## Remove Files [#remove-files]

```bash
ccp storage rm logo.png
# ✓ Removed logo.png
```

`remove` is an alias for `rm`.

## Image Transformations [#image-transformations]

Files uploaded to a store are served through Clusterbase's edge with on-the-fly image processing. Append query parameters to the served URL to resize or change format — the result is computed once and cached, so repeat hits at the same parameters are cheap.

| Param              | Effect                                                        |
| ------------------ | ------------------------------------------------------------- |
| `?w=<px>`          | Resize width. Height is auto-scaled to preserve aspect ratio. |
| `?h=<px>`          | Resize height. Width is auto-scaled.                          |
| `?w=<px>&h=<px>`   | Resize to exact dimensions.                                   |
| `?format=original` | Bypass conversion and serve the raw uploaded bytes.           |

```html
<!-- Original encoding (default) -->
<img src="https://assets.cluster.app/edge/serve/me/photo.jpg">

<!-- WebP-encoded; explicit opt-in -->
<img src="https://assets.cluster.app/edge/serve/me/photo.jpg?format=webp">

<!-- Resized to 400px wide, original encoding -->
<img src="https://assets.cluster.app/edge/serve/me/photo.jpg?w=400">

<!-- Resized + WebP -->
<img src="https://assets.cluster.app/edge/serve/me/photo.jpg?w=400&format=webp">

<!-- Exact 200x200, original encoding -->
<img src="https://assets.cluster.app/edge/serve/me/photo.jpg?w=200&h=200">
```

### Default: Original passthrough [#default-original-passthrough]

By default, the served response returns the original uploaded bytes — same content-type, no transcoding. To opt into WebP encoding, pass `?format=webp` explicitly. WebP is materially smaller than JPEG/PNG at equivalent quality on every modern browser, so it's the right choice for most image embeds — just opt in per-request rather than receiving it implicitly.

Non-image content (CSS, JS, PDF, JSON, etc.) is served as-is regardless of params — only supported image MIME types go through the processor. Resize parameters (`?w=`, `?h=`) imply transcoding even if `format` isn't set, since the bytes change either way.

### Caching [#caching]

URLs uploaded via `ccp store put` are currently mutable: the same URL is served on every upload, and the response carries `Cache-Control: no-store` to keep callers correct on Cloud CDN, which over-retains mutable URLs even with short max-age values. Image transforms (`?w=`, `?h=`, `?format=`) are still computed once and cached at the origin (in-memory + GCS at `_cache/{path}/{key}`), but the client-side response itself is not CDN-cacheable — every request reaches origin.

A future CCP release will switch the default upload path to a **content-addressed** family that produces immutable URLs cacheable forever by Cloud CDN. The URL embeds a content hash (`{store}/m/{hash}/{filename}`), re-uploads with new bytes mint a new URL, and old URLs keep serving from GCS until lifecycle cleanup. See [infra issue #109](https://github.com/Peteskiis/cluster-infra/issues/109) for the migration plan.

Either way, sticking to a small canonical set of sizes (e.g. `?w=400`, `?w=800`, `?w=1600`) is much cheaper than passing arbitrary widths per page — each unique tuple is a fresh transcode on the first hit.

## Subcommand Reference [#subcommand-reference]

| Subcommand       | Aliases  | Description                                  |
| ---------------- | -------- | -------------------------------------------- |
| `create <name>`  | —        | Create a new store                           |
| `put <files...>` | `upload` | Upload one or more files                     |
| `get <filename>` | —        | Download a file (optional `-o` / `--output`) |
| `ls`             | `list`   | List files in the store                      |
| `rm <filename>`  | `remove` | Delete a file                                |

## Function Context [#function-context]

Storage commands need to know which store to operate on. CCP resolves this automatically if your project's `.ccp/config.json` has a `store_id`. Otherwise, it prompts you to select an organization and store interactively (set `CCP_ORG_ID` to supply the organization without that prompt), and persists the chosen `store_id` back to `.ccp/config.json` so subsequent commands skip the prompt.

### Bypass the pin: `-a` / `--all` [#bypass-the-pin--a----all]

Pass `-a` or `--all` to any storage subcommand (`put`, `get`, `ls`, `rm`) to bypass the persisted `store_id` and re-prompt for store selection:

```bash
ccp storage ls --all       # list stores across all orgs, pick one for this command
ccp storage put logo.png -a
```

Useful when you need to operate on a store that isn't the one pinned to the current project, or in scripts where the pinned ID isn't appropriate. The flag is purely per-invocation — it doesn't update the persisted `store_id`.
