file-storage
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.
Savant verdict: Review advised
Security analysis or source-hub review identified patterns requiring attention.
Live evaluation
Not evaluated yet. Workspaces can request a live evaluation.
Security assessment
Scan pending.
Structure
- No license declaredConfirm you may reuse this skill before importing it into your repository.
- The source hub's security checks flagged this skillSocket: warn (3 alerts: gptAnomaly)
SKILL.md
---
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/