# How vcup works

> End-to-end flow: CLI streams to POST /api/upload, Vercel Blob put with random suffix, proxy slug served via /f rewrite to api/f, and optional delete path.

- Repository: MaxLeiter/vcup
- GitHub: https://github.com/MaxLeiter/vcup
- Human docs: https://grok-wiki.com/public/docs/maxleiter-vcup-05c16ae77ebb
- Complete Markdown: https://grok-wiki.com/public/docs/maxleiter-vcup-05c16ae77ebb/llms-full.txt

## Source Files

- `api/upload.ts`
- `api/f.ts`
- `api/delete.ts`
- `vercel.json`
- `cli.ts`
- `package.json`

---

---
title: "How vcup works"
description: "End-to-end flow: CLI streams to POST /api/upload, Vercel Blob put with random suffix, proxy slug served via /f rewrite to api/f, and optional delete path."
---

vcup is a Bun CLI (`cli.ts`) plus three Vercel serverless handlers (`api/upload.ts`, `api/f.ts`, `api/delete.ts`) backed by `@vercel/blob`. Uploads stream into `put()` with `addRandomSuffix: true`; the API returns a short proxy URL under `/f/{slug}` and the direct blob URL. Public reads hit `/f/:slug*`, which `vercel.json` rewrites to `api/f.ts` for inline streaming from `BLOB_STORE_URL`. Deletes call `del()` with the raw blob URL after optional CLI-side slug resolution.

## System boundaries

```text
┌─────────────┐     POST /api/upload      ┌──────────────────┐
│  cli.ts     │ ─────────────────────────►│  api/upload.ts   │
│  (Bun)      │     Bearer + X-Filename   │  @vercel/blob put│
└─────────────┘                           └────────┬─────────┘
       │                                            │
       │ prints url or raw                          ▼
       │                                   Vercel Blob store
       ▼                                            │
  ~/.vcuprc / env                                   │ public object URL
                                                    │
┌─────────────┐     GET /f/:slug* (rewrite)         │
│  Browser    │ ─────────────────────────►┌────────▼─────────┐
└─────────────┘                           │  api/f.ts        │
                                          │  fetch(store/slug)│
                                          └──────────────────┘

┌─────────────┐     DELETE /api/delete    ┌──────────────────┐
│  vcup rm    │ ─────────────────────────►│  api/delete.ts   │
└─────────────┘     Bearer + X-Blob-Url   │  @vercel/blob del│
                                          └──────────────────┘
```

| Layer | Entry | Auth | Storage touch |
| --- | --- | --- | --- |
| CLI | `vcup <file>`, stdin, `vcup rm` | `Authorization: Bearer` from config | None (HTTP only) |
| Upload API | `POST /api/upload` | `VCUP_TOKEN` | `put(filename, stream)` |
| Proxy API | `GET /f/:slug*` → `/api/f` | None | `fetch(BLOB_STORE_URL/slug)` |
| Delete API | `DELETE /api/delete` | `VCUP_TOKEN` | `del(blobUrl)` |

## Upload path

The CLI loads `url` and `token` from `VCUP_API_URL` + `VCUP_TOKEN` (preferred) or `~/.vcuprc`, then builds the request body from a file path, stdin (`paste.txt` when piped), or exits with help when neither applies. Directories are rejected before any network call.

Upload uses a chunked `ReadableStream` with stderr progress (`progressStream` in `cli.ts`), `POST` to `{config.url}/api/upload`, and Bun’s `duplex: "half"` so the body streams without buffering the whole file in the handler path on the client.

:::endpoint POST /api/upload
Accepts the raw request body as the blob payload (`bodyParser: false`). Requires `Authorization: Bearer {VCUP_TOKEN}` and `X-Filename` with the object name used for `put()`.
:::

<ParamField header="Authorization" type="string" required>
Bearer token must equal server `VCUP_TOKEN`. Missing or mismatched token → `401 Unauthorized`.
</ParamField>

<ParamField header="X-Filename" type="string" required>
Filename passed to `@vercel/blob` `put()`. Missing → `400` with `Missing x-filename header`.
</ParamField>

The handler calls:

```ts
await put(filename, req, { access: "public", addRandomSuffix: true });
```

`addRandomSuffix: true` appends entropy to the stored pathname so repeated uploads of the same filename do not collide. The proxy slug is the blob URL pathname with the leading `/` removed:

```ts
const slug = new URL(blob.url).pathname.slice(1);
```

<ResponseField name="url" type="string">
Proxy link: `{baseUrl}/f/{slug}`. `baseUrl` is `VCUP_BASE_URL` when set, otherwise `x-forwarded-proto` + `host` from the upload request.
</ResponseField>

<ResponseField name="raw" type="string">
Direct Vercel Blob public URL returned by `put()`.
</ResponseField>

<ResponseExample>

```json
{
  "url": "https://your-deployment.vercel.app/f/paste-abc123.txt",
  "raw": "https://abc123.public.blob.vercel-storage.com/paste-abc123.txt"
}
```

</ResponseExample>

By default the CLI prints `url`. With `--raw`, it prints `raw` instead. Non-OK responses print status and body to stderr and exit `1`.

```mermaid
sequenceDiagram
  participant CLI as cli.ts
  participant API as api/upload.ts
  participant Blob as Vercel Blob

  CLI->>API: POST /api/upload<br/>Bearer, X-Filename, body stream
  API->>API: Validate method, token, filename
  API->>Blob: put(filename, req, addRandomSuffix)
  Blob-->>API: blob.url
  API->>API: slug = pathname of blob.url
  API-->>CLI: 200 { url, raw }
  CLI-->>CLI: stdout: url or raw per --raw
```

## Proxy serving (`/f`)

`vercel.json` maps browser-friendly paths to the proxy handler:

```json
{ "source": "/f/:slug*", "destination": "/api/f" }
```

`api/f.ts` accepts `GET` only. It parses `slug` from the path after `/f/`, requires `BLOB_STORE_URL`, and fetches `{BLOB_STORE_URL}/{slug}`. On success it sets:

| Header | Value |
| --- | --- |
| `Content-Type` | From `mime-types` `lookup()` on the filename segment, else `application/octet-stream` |
| `Content-Disposition` | `inline` (browser renders when possible) |
| `Cache-Control` | `public, max-age=31536000, immutable` |

The upstream body is streamed to the client with `ReadableStream` `getReader()` loops. Upstream non-OK statuses pass through; fetch failures return `502 Failed to fetch file`. Missing `BLOB_STORE_URL` returns `500 BLOB_STORE_URL not configured`.

<Note>
The proxy route is public: no `VCUP_TOKEN` check. Anyone with the `/f/{slug}` link can read the object while it exists in the store.
</Note>

```mermaid
sequenceDiagram
  participant Browser
  participant Vercel as Vercel rewrite
  participant F as api/f.ts
  participant Store as BLOB_STORE_URL

  Browser->>Vercel: GET /f/{slug}
  Vercel->>F: GET /api/f (same slug in path)
  F->>Store: fetch(store/slug)
  Store-->>F: object bytes
  F-->>Browser: 200 streamed, inline disposition
```

## Delete path

`vcup rm <url>` (alias `delete`) sends `DELETE` to `{config.url}/api/delete` with `Authorization: Bearer` and `X-Blob-Url` set to the blob store URL.

If the argument contains `/f/`, the CLI extracts the slug and, when `BLOB_STORE_URL` is set in the **client** environment, rewrites to `{BLOB_STORE_URL}/{slug}` before calling the API. Without client `BLOB_STORE_URL`, proxy URLs cannot be resolved and the CLI exits with an error instructing you to pass the raw URL or set `BLOB_STORE_URL`.

:::endpoint DELETE /api/delete
Authenticated delete by blob URL. Requires `X-Blob-Url` header with the full Vercel Blob URL (not the proxy `/f/` URL).
:::

<ParamField header="Authorization" type="string" required>
Same `VCUP_TOKEN` Bearer check as upload.
</ParamField>

<ParamField header="X-Blob-Url" type="string" required>
URL passed to `@vercel/blob` `del()`. Missing → `400 Missing x-blob-url header`.
</ParamField>

Success: `200` with body `Deleted`. The CLI prints `Deleted` on stdout.

```mermaid
sequenceDiagram
  participant CLI as cli.ts
  participant API as api/delete.ts
  participant Blob as Vercel Blob

  alt proxy URL argument
    CLI->>CLI: slug from /f/ + client BLOB_STORE_URL
  end
  CLI->>API: DELETE /api/delete<br/>Bearer, X-Blob-Url
  API->>Blob: del(url)
  API-->>CLI: 200 Deleted
```

## URL shapes and slug semantics

| Kind | Pattern | Produced by | Used for |
| --- | --- | --- | --- |
| Proxy | `{base}/f/{slug}` | Upload JSON `url` | Sharing, inline browser view |
| Raw | `https://….blob.vercel-storage.com/…` | Upload JSON `raw`, `--raw` flag | Direct blob access, delete target |
| Slug | Pathname after random suffix (may include `/`) | Derived from `blob.url` | Proxy path segment; must match store object key |

The slug is opaque: it mirrors the blob store pathname, including any random suffix segments `put()` adds.

## Runtime and dependencies

| Component | Runtime |
| --- | --- |
| CLI | Bun (`#!/usr/bin/env bun`), published as `@maxleiter/vcup` bin → `cli.ts` |
| API routes | Vercel serverless Node handlers (`IncomingMessage` / `ServerResponse`) |
| Blob SDK | `@vercel/blob` `put` / `del`; store credentials via `BLOB_READ_WRITE_TOKEN` (Vercel integration, not referenced in handler source) |

Local API development uses `npm run dev` → `vercel dev` per `package.json`.

## Configuration touchpoints

Server env vars drive handler behavior:

| Variable | Role in flow |
| --- | --- |
| `VCUP_TOKEN` | Upload/delete auth |
| `BLOB_STORE_URL` | Proxy fetch base in `api/f.ts`; optional CLI slug resolution for `rm` |
| `VCUP_BASE_URL` | Override proxy host in upload JSON `url` |
| `BLOB_READ_WRITE_TOKEN` | Required by Vercel Blob for `put`/`del` (wired at deploy) |

Client config (`~/.vcuprc` or `VCUP_API_URL` / `VCUP_TOKEN`) only needs the deployment origin and token for upload/delete. Client `BLOB_STORE_URL` is optional unless deleting via proxy URLs.

<Warning>
Upload and delete share one secret (`VCUP_TOKEN`). Rotate it on both the Vercel project and every CLI config that uploads or deletes.
</Warning>

## HTTP status summary

| Route | Method | Common errors |
| --- | --- | --- |
| `/api/upload` | POST | `405`, `401`, `400` (filename), upstream/blob failures |
| `/api/f` (via `/f/…`) | GET | `405`, `400` (slug), `500` (no store URL), upstream status, `502` |
| `/api/delete` | DELETE | `405`, `401`, `400` (blob URL) |

## Related pages

<CardGroup>
  <Card title="Proxy and raw URLs" href="/proxy-and-raw-urls">
    Compare `url` vs `raw`, rewrite behavior, and when the store URL is required.
  </Card>
  <Card title="Authentication" href="/authentication">
    Bearer checks, public `/f` reads, and token configuration.
  </Card>
  <Card title="Upload and delete files" href="/upload-and-delete">
    CLI commands, stdin uploads, `--raw`, and `vcup rm` resolution rules.
  </Card>
  <Card title="API reference" href="/api-reference">
    Per-route headers, status codes, and response shapes.
  </Card>
  <Card title="Environment variables" href="/environment-variables">
    Server and client variables for the full lifecycle.
  </Card>
</CardGroup>
