Skip to main content
skill CX Codex

file-storage — Claude Code skill: what it does, how to install

Helps configure Tigris Object Storage buckets via CLI and handle file uploads in Node.js, Rails, or Laravel.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add tigrisdata/skills --skill file-storage -a claude-code
Open on GitHub

What file-storage does

This skill guides agents through provisioning Tigris Object Storage. It covers authenticating with @tigrisdata/cli and creating storage buckets with assigned access keys.

For application code, it implements the @tigrisdata/storage SDK across frameworks like Next.js, Remix, Express, Rails, and Laravel. Instructions detail how to handle file uploads via put with optional multipart streaming, track transfer progress, and toggle object visibility. Custom bucket targets can be passed directly inside the config argument.

Because the CLI reveals the Secret Access Key only once during key creation, losing it forces an immediate credential reset. Projects built with Python or Django need the separate tigris-python-sdk skill instead.

What it helps with

  • Create private or public buckets using the tigris CLI
  • Generate access keys and assign Editor or ReadOnly roles
  • Integrate file uploads into Next.js, Remix, or Express backends
  • Configure multipart uploads for large files with progress reporting
  • Set overwrite protection and random filename suffixes in @tigrisdata/storage

Example prompts for the agent

  • Write code to upload a public avatar using the @tigrisdata/storage SDK
  • Create a new Tigris bucket via CLI and generate an access key with Editor permissions
  • Implement a multipart video upload to Tigris with progress tracking

How to install file-storage

1Skills CLI (Vercel)

npx skills add tigrisdata/skills --skill file-storage -a claude-code

2Manually: put the files in place

~/.claude/skills/file-storage/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/file-storage/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about file-storage

How do I install file-storage in Claude Code?

Run npx skills add tigrisdata/skills --skill file-storage -a claude-code. The skill file is placed in ~/.claude/skills/file-storage/SKILL.md.

What does the file-storage skill do?

It guides the agent through configuring Tigris Object Storage via CLI and writing upload or download routines with @tigrisdata/storage.

Can I use this skill for Python or Django projects?

No, Python and Django workflows require the separate tigris-python-sdk skill. This package targets the JavaScript SDK and CLI workflows.

Source text

SKILL.md · MIT
---
name: file-storage
description: Use when working with Tigris file storage - uploading, downloading, deleting, listing files, presigned URLs, client uploads, or setting up Tigris CLI and SDK. Covers Next.js, Remix, Express, Rails, and Laravel. For Python/Django, see the tigris-python-sdk skill.
---

# Tigris File Storage

Store and serve files with Tigris Object Storage. Covers CLI setup (bucket, access keys, auth) and the `@tigrisdata/storage` SDK for application code.

## Quick Start

```bash
# 1. Install CLI & authenticate
npm install -g @tigrisdata/cli
tigris login

# 2. Create bucket and access key
tigris buckets create my-app-uploads
tigris access-keys create "my-app-uploads-key"
# ⚠ Save the Secret Access Key — shown only once
tigris access-keys assign tid_xxx --bucket my-app-uploads --role Editor

# 3. Install SDK
npm install @tigrisdata/storage
```

```bash
# .env
TIGRIS_STORAGE_ACCESS_KEY_ID=tid_xxx
TIGRIS_STORAGE_SECRET_ACCESS_KEY=tsec_yyy
TIGRIS_STORAGE_ENDPOINT=https://t3.storage.dev
TIGRIS_STORAGE_BUCKET=my-app-uploads
```

```typescript
import { put } from "@tigrisdata/storage";

// Files are private by default — only authenticated requests can access them
const result = await put("avatars/user-123.jpg", file);
if (result.error) throw result.error;
console.log(result.data?.url);

// Use access: "public" only when anonymous users need direct URL access
// const result = await put("avatars/user-123.jpg", file, { access: "public" });
```

See **Getting Started with CLI** below for detailed steps.

---

## Getting Started with CLI

### Step 1: Install CLI

```bash
npm install -g @tigrisdata/cli
```

Verify the installation:

```bash
tigris --version
```

`t3` is an alias for `tigris` — all commands work with either.

### Step 2: Authenticate

```bash
tigris login
```

Opens browser for OAuth. After login, verify with:

```bash
tigris whoami
```

For CI/CD or non-interactive environments:

```bash
tigris configure --access-key <key> --access-secret <secret>
```

### Step 3: Create Bucket

```bash
tigris buckets create my-app-uploads
```

Key points:

- Buckets are **private** by default. Use `--public` for publicly readable objects.
- Buckets are **global** by default. Use `--locations` to pin to specific regions.
- Type `help` after any command to see its options (e.g., `tigris buckets create help`).

### Step 4: Create Access Key

```bash
tigris access-keys create "my-app-uploads-key"
```

This outputs an Access Key ID (`tid_xxx`) and Secret Access Key (`tsec_yyy`).

**The Secret Access Key is only shown once.** Copy it immediately. The Name field is only for human identification — it has no functional impact.

### Step 5: Configure Environment

Create `.env` in your project root:

```bash
TIGRIS_STORAGE_ACCESS_KEY_ID=tid_xxx
TIGRIS_STORAGE_SECRET_ACCESS_KEY=tsec_yyy
TIGRIS_STORAGE_ENDPOINT=https://t3.storage.dev
TIGRIS_STORAGE_BUCKET=my-app-uploads
```

`TIGRIS_STORAGE_BUCKET` sets the default bucket for all SDK calls. Add `.env` to `.gitignore` — never commit credentials.

### Step 6: Assign Access Key to Bucket

```bash
tigris access-keys assign tid_xxx --bucket my-app-uploads --role Editor
```

**Roles:**

| Role       | Permissions                        | Use when                                 |
| ---------- | ---------------------------------- | ---------------------------------------- |
| `Editor`   | Read + write + delete objects      | App servers that upload/delete files     |
| `ReadOnly` | Read objects only                  | Apps that only serve/download files      |

Now you have:

- A bucket (`my-app-uploads`)
- An access key (`tid_xxx` / `tsec_yyy`)
- The key assigned to the bucket with Editor role
- A `.env` file ready for the SDK

### Step 7: Install SDK

```bash
npm install @tigrisdata/storage
# or
yarn add @tigrisdata/storage
```

Supports ES Modules and CommonJS.

---

## SDK Reference

All methods return `TigrisStorageResponse<T, E>`. Always check `error` first:

```typescript
const result = await put("file.txt", "hello");
if (result.error) {
  console.error(result.error);
  return;
}
console.log(result.data);
```

### config — Override Default Configuration

Every method accepts an optional `config` parameter of type `TigrisStorageConfig`:

```typescript
type TigrisStorageConfig = {
  bucket?: string;          // Override TIGRIS_STORAGE_BUCKET
  accessKeyId?: string;     // Override TIGRIS_STORAGE_ACCESS_KEY_ID
  secretAccessKey?: string; // Override TIGRIS_STORAGE_SECRET_ACCESS_KEY
  endpoint?: string;        // Override TIGRIS_STORAGE_ENDPOINT
};
```

Use `config` to target a different bucket or use different credentials per call:

```typescript
// Upload to a different bucket
await put("report.pdf", data, { config: { bucket: "reports-archive" } });

// Use a separate read-only key for downloads
await get("file.txt", "string", { config: { accessKeyId: "tid_ro", secretAccessKey: "tsec_ro" } });
```

### put — Upload

```typescript
put(path: string, body: string | ReadableStream | Blob | Buffer, options?: PutOptions)
```

```typescript
import { put } from "@tigrisdata/storage";

// Simple text upload
const result = await put("notes/hello.txt", "Hello, World!");

// Image with public access
const result = await put("avatars/user-123.jpg", file, {
  access: "public",
  contentType: "image/jpeg",
  addRandomSuffix: false,
});

// Large file with multipart and progress
const result = await put("videos/demo.mp4", fileStream, {
  multipart: true,
  onUploadProgress: ({ loaded, total, percentage }) => {
    console.log(`${loaded}/${total} bytes (${percentage}%)`);
  },
});

// Prevent accidental overwrite
const result = await put("config.json", data, {
  allowOverwrite: false,
});
```

**Put options:**

| Option             | Values              | Default    | Purpose                       |
| ------------------ | ------------------- | ---------- | ----------------------------- |
| access             | `public`, `private` | `private`  | Object visibility             |
| addRandomSuffix    | boolean             | `false`    | Append random suffix to avoid collisions on user-uploaded files with the same name |
| allowOverwrite     | boolean             | `true`     | Allow replacing existing file |
| contentType        | MIME string         | inferred   | Content type header           |
| contentDisposition | `inline`,`attachment`| `inline`  | Browser display behavior      |
| multipart          | boolean             | `false`    | Enable for large files        |
| onUploadProgress   | callback            | —          | `{loaded, total, percentage}` |
| config             | `TigrisStorageConfig` | —        | Override bucket/credentials (see config section above) |

**Response data:** `{ url, path, size, contentType, contentDisposition, modified }`

### get — Download

```typescript
get(path: string, format: "string" | "file" | "stream", options?: GetOptions)
```

```typescript
import { get } from "@tigrisdata/storage";

// Read as string (text, JSON)
const result = await get("notes/hello.txt", "string");
console.log(result.data); // "Hello, World!"

// Serve as file (for API routes)
const result = await get("avatars/user-123.jpg", "file", {
  contentDisposition: "inline",
});

// Trigger browser download
const result = await get("reports/q4.pdf", "file", {
  contentDisposition: "attachment",
});

// Stream large files
const result = await get("videos/demo.mp4", "stream");
```

**Get options:**

| Option             | Values               | Default  | Purpose               |
| ------------------ | -------------------- | -------- | --------------------- |
| contentDisposition | `inline`,`attachment`| `inline` | Display vs download   |
| contentType        | MIME string          | from upload | Override content type |
| encoding           | string               | `utf-8`  | Text encoding         |
| config             | `TigrisStorageConfig` | —       | Override bucket/credentials (see config section above) |

### remove — Delete

```typescript
remove(path: string, options?: RemoveOptions)
```

```typescript
import { remove } from "@tigrisdata/storage";

const result = await remove("notes/hello.txt");
if (result.error) {
  console.error(result.error);
}
```

### list — List Objects

```typescript
list(options?: ListOptions)
```

```typescript
import { list } from "@tigrisdata/storage";

// List all objects
const result = await list();
console.log(result.data?.items);

// Filter by prefix
const result = await list({ prefix: "avatars/" });

// Paginate through all objects
const allFiles = [];
let page = await list({ limit: 100 });
allFiles.push(...(page.data?.items ?? []));

while (page.data?.hasMore) {
  page = await list({
    limit: 100,
    paginationToken: page.data.paginationToken,
  });
  allFiles.push(...(page.data?.items ?? []));
}
```

**List options:**

| Option          | Purpose                                |
| --------------- | -------------------------------------- |
| prefix          | Filter keys starting with this string  |
| delimiter       | Group keys (e.g., `"/"` for folders)   |
| limit           | Max objects per page (default: 100)    |
| paginationToken | Continue from previous page            |
| config          | `TigrisStorageConfig` — override bucket/credentials (see config section above) |

**Response data:** `{ items, paginationToken, hasMore }`

### head — Object Metadata

```typescript
head(path: string, options?: HeadOptions)
```

```typescript
import { head } from "@tigrisdata/storage";

const result = await head("avatars/user-123.jpg");
if (!result.error) {
  console.log(result.data);
  // { path, size, contentType, contentDisposition, modified, url }
}
```

### getPresignedUrl — Temporary URLs

```typescript
getPresignedUrl(path: string, options: GetPresignedUrlOptions)
```

```typescript
import { getPresignedUrl } from "@tigrisdata/storage";

// Temporary download link (1 hour)
const result = await getPresignedUrl("reports/q4.pdf", {
  operation: "get",
  expiresIn: 3600,
});
console.log(result.data?.url);

// Temporary upload link (10 minutes)
const result = await getPresignedUrl("uploads/photo.jpg", {
  operation: "put",
  expiresIn: 600,
});
```

**Presigned URL options:**

| Option      | Values      | Default | Purpose           |
| ----------- | ----------- | ------- | ----------------- |
| operation   | `get`,`put` | —       | URL purpose       |
| expiresIn   | seconds     | `3600`  | Expiration time   |
| contentType | MIME string | —       | Required for PUT  |
| config      | `TigrisStorageConfig` | — | Override bucket/credentials (see config section above) |

**Response data:** `{ url, method, expiresIn }`

---

## Client-Side Uploads

Upload files directly from the browser to Tigris without routing bytes through your server. Uses presigned URLs under the hood.

### Server — Handle Upload Requests

```typescript
// app/api/upload/route.ts
import { NextRequest, NextResponse } from "next/server";
import { handleClientUpload } from "@tigrisdata/storage";

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const { data, error } = await handleClientUpload(body);
    if (error) {
      return NextResponse.json({ error: error.message }, { status: 500 });
    }
    return NextResponse.json({ data });
  } catch (error) {
    return NextResponse.json(
      { error: "Failed to process upload request" },
      { status: 500 },
    );
  }
}
```

### Client — Direct Upload

```typescript
"use client";

import { upload } from "@tigrisdata/storage/client";
import { useState } from "react";

export default function FileUpload() {
  const [progress, setProgress] = useState(0);
  const [url, setUrl] = useState<string | null>(null);

  const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;

    const result = await upload(file.name, file, {
      url: "/api/upload",
      access: "private",
      multipart: true,
      partSize: 10 * 1024 * 1024,
      onUploadProgress: ({ percentage }) => {
        setProgress(percentage);
      },
    });

    setUrl(result.url);
  };

  return (
    <>
      <input type="file" onChange={handleFileChange} />
      {progress > 0 && progress < 100 && <div>{progress}%</div>}
      {url && <div>Uploaded: {url}</div>}
    </>
  );
}
```

**Client upload options:**

| Option           | Required | Purpose                              |
| ---------------- | -------- | ------------------------------------ |
| url              | Yes      | Backend endpoint for presigned URLs  |
| access           | No       | `public` or `private` (default)      |
| multipart        | No       | Enable for large files               |
| partSize         | No       | Bytes per part (default: 5 MiB)      |
| concurrency      | No       | Parallel part uploads (default: 4)   |
| contentType      | No       | MIME type                            |
| onUploadProgress | No       | `{loaded, total, percentage}`        |

### React Component (Optional)

`npm install @tigrisdata/react` provides a drop-in `<Uploader>` component with file selection, progress, and error handling built in. See `@tigrisdata/react` docs for usage.

---

## Critical Rules

**Always:** Check `result.error` before `result.data` | Upload files as `private` by default — only set `access: "public"` when anonymous users need direct URL access | Use `handleClientUpload` for browser uploads (don't route bytes through server) | Use `multipart: true` for files over 100MB | Paginate `list()` with `hasMore` + `paginationToken` | Delete old files when replacing (no auto-cleanup) | Set `contentType` explicitly when it matters

**Never:** Expose access keys to the client (use `handleClientUpload` + `upload()` from `@tigrisdata/storage/client`) | Skip error checking | Use generic paths like `file.jpg` (use `avatars/${userId}.jpg` or timestamps) | Forget to save the Secret Access Key on creation (shown only once)

---

## Known Issues

| Problem                        | Cause & Fix                                                                             |
| ------------------------------ | --------------------------------------------------------------------------------------- |
| "Access denied" on upload      | Key not assigned to bucket. Run `tigris access-keys assign tid_xxx --bucket <name> --role Editor` |
| "Bucket not found" from SDK    | Wrong bucket name in `.env`. Verify with `tigris buckets list`                          |
| Secret Access Key lost         | Cannot recover. Create new: `tigris access-keys create "new-key"` and reassign          |
| Files not publicly accessible  | Bucket is private by default. Use `--public` flag or `access: "public"` on `put()`      |
| Upload hangs on large files    | Add `multipart: true` to put options for files over 100MB                               |
| List returns incomplete results| Default limit is 100. Use `hasMore` + `paginationToken` to paginate                    |
| Client upload fails (CORS/500) | Server route must use `handleClientUpload` from `@tigrisdata/storage`                   |

---

## CLI Quick Reference

`t3` is an alias for `tigris`. Type `help` after any command for options.

```bash
# Auth
tigris login
tigris whoami

# Buckets
tigris buckets create <name> [--public] [--locations <region>]
tigris buckets list
tigris buckets delete <name>

# Access keys
tigris access-keys create "<name>"
tigris access-keys assign <tid_xxx> --bucket <name> --role Editor

# Objects
tigris cp <src> <dest> [-r]         # Upload/download/copy
tigris mv <src> <dest> [-rf]        # Move or rename
tigris rm <path> [-rf]              # Delete
tigris ls [bucket/prefix]           # List
tigris stat <path>                  # Metadata
tigris presign <path>               # Presigned URL
tigris touch <path>                 # Create empty object
```

Remote paths use `t3://` prefix: `t3://my-bucket/path/file.txt`

---

## Framework Integration Guides

For framework-specific upload/download patterns, read the resource file for your framework:

| Framework | SDK | Resource |
|-----------|-----|----------|
| Next.js | `@tigrisdata/storage` (native) | Read `./resources/nextjs.md` — Server Actions, API Routes, next/image, client uploads |
| Remix | `@tigrisdata/storage` (native) | Read `./resources/remix.md` — action functions, loaders, client uploads |
| Express | `@tigrisdata/storage` (native) | Read `./resources/express.md` — Multer, streaming uploads, client uploads |
| Rails | `aws-sdk-s3` (no native Ruby SDK yet) | Read `./resources/rails.md` — Active Storage, direct uploads, image variants |
| Django | `tigris-boto3-ext` + `django-storages` | Use the **tigris-python-sdk** skill — covers FileField, django-storages, presigned URLs |
| Laravel | `league/flysystem-aws-s3-v3` (no native PHP SDK yet) | Read `./resources/laravel.md` — Storage facade, Livewire uploads, presigned URLs |

### Deployment

| Framework | Platform | Set env vars with |
|-----------|----------|-------------------|
| Next.js | Vercel | Dashboard → Settings → Environment Variables |
| Remix | Fly.io | `fly secrets set TIGRIS_STORAGE_ACCESS_KEY_ID=... ...` |
| Express | Docker | `-e` flags or `.env` in Compose |
| Rails | Fly.io / Kamal | `fly secrets set` or `kamal env push` |
| Laravel | Forge / Vapor | Dashboard → Environment or `vapor env:pull` |

---

## Related Skills

- **tigris-python-sdk** — Python access via boto3, Django uploads, snapshots, forks, Bundle API
- **tigris-bucket-management** — Advanced bucket options (regions, tiers, snapshots)
- **tigris-object-operations** — Detailed SDK function reference
- **tigris-snapshots-forking** — Point-in-time recovery and bucket forking

## Official Documentation

- SDK: https://www.tigrisdata.com/docs/sdks/tigris/
- Client Uploads: https://www.tigrisdata.com/docs/sdks/tigris/client-uploads/
- Examples: https://www.tigrisdata.com/docs/sdks/tigris/examples/

© tigrisdata · MIT · tigrisdata/skills

Similar skills

All skills for Cloud: AWS, Azure, GCP →

From the same set8

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor