SKILL.md
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
# 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
# .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
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
npm install -g @tigrisdata/cli
Verify the installation:
tigris --version
t3 is an alias for tigris — all commands work with either.
Step 2: Authenticate
tigris login
Opens browser for OAuth. After login, verify with:
tigris whoami
For CI/CD or non-interactive environments:
tigris configure --access-key <key> --access-secret <secret>
Step 3: Create Bucket
tigris buckets create my-app-uploads
Key points:
- Buckets are private by default. Use
--publicfor publicly readable objects. - Buckets are global by default. Use
--locationsto pin to specific regions. - Type
helpafter any command to see its options (e.g.,tigris buckets create help).
Step 4: Create Access Key
tigris access-keys create "my-app-uploads-key"
This outputs an Access Key ID (tidxxx) and Secret Access Key (tsecyyy).
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:
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
TIGRISSTORAGEBUCKET sets the default bucket for all SDK calls. Add .env to .gitignore — never commit credentials.
Step 6: Assign Access Key to Bucket
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 (
tidxxx/tsecyyy) - The key assigned to the bucket with Editor role
- A
.envfile ready for the SDK
Step 7: Install SDK
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:
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:
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:
// 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
put(path: string, body: string | ReadableStream | Blob | Buffer, options?: PutOptions)
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
get(path: string, format: "string" | "file" | "stream", options?: GetOptions)
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
remove(path: string, options?: RemoveOptions)
import { remove } from "@tigrisdata/storage";
const result = await remove("notes/hello.txt");
if (result.error) {
console.error(result.error);
}
list — List Objects
list(options?: ListOptions)
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
head(path: string, options?: HeadOptions)
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
getPresignedUrl(path: string, options: GetPresignedUrlOptions)
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
// 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
"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.
# 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 TIGRISSTORAGEACCESSKEYID=... ... |
| 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