Files API
Files API
The Files API handles file upload, download, and management in Cloudillo. It supports three file types: BLOB (binary files), CRDT (collaborative documents), and RTDB (real-time databases).
File Types
| Type | Description | Use Cases |
|---|---|---|
| BLOB | Binary files (images, PDFs, etc.) | Photos, documents, attachments |
| CRDT | Collaborative documents | Rich text, spreadsheets, diagrams |
| RTDB | Real-time databases | Structured data, forms, todos |
Image Variants
For BLOB images, Cloudillo automatically generates 5 variants:
| Variant | Code | Max Dimension | Quality | Format |
|---|---|---|---|---|
| Thumbnail | tn |
150px | Medium | JPEG/WebP |
| Icon | ic |
64px | Medium | JPEG/WebP |
| SD | sd |
640px | Medium | JPEG/WebP |
| HD | hd |
1920px | High | JPEG/WebP |
| Original | orig |
Original | Original | Original |
Automatic format selection:
- Modern browsers: WebP or AVIF
- Fallback: JPEG or PNG
Endpoints
List Files
GET /api/filesList all files accessible to the authenticated user.
Authentication: Required
Query Parameters:
fileTp- Filter by file type (BLOB, CRDT, RTDB)contentType- Filter by MIME typetags- Filter by tags (comma-separated)_limit- Max results (default: 50)_offset- Pagination offset
Example:
const api = cloudillo.createApiClient()
// List all images
const images = await api.files.get({
fileTp: 'BLOB',
contentType: 'image/*',
_limit: 20
})
// List tagged files
const projectFiles = await api.files.get({
tags: 'project-alpha,important'
})Response:
{
"data": [
{
"fileId": "b1~abc123",
"status": "M",
"contentType": "image/png",
"fileName": "photo.png",
"fileTp": "BLOB",
"createdAt": "2025-01-01T12:00:00Z",
"tags": ["vacation", "beach"],
"owner": {
"idTag": "alice@example.com",
"name": "Alice"
}
}
]
}Create File Metadata (CRDT/RTDB)
POST /api/filesCreate file metadata for CRDT or RTDB files. For BLOB files, use the one-step upload endpoint instead (see “Upload File (BLOB)” below).
Authentication: Required
Request Body:
{
fileTp: string // CRDT or RTDB
fileName?: string // Optional filename
tags?: string // Comma-separated tags
}Example:
// Create CRDT document
const file = await api.files.post({
fileTp: 'CRDT',
fileName: 'team-doc.crdt',
tags: 'collaborative,document'
})
console.log('File ID:', file.fileId) // e.g., "f1~abc123"
// Now connect via WebSocket to edit the document
import * as Y from 'yjs'
const yDoc = new Y.Doc()
const { provider } = await cloudillo.openYDoc(yDoc, file.fileId)Response:
{
"data": {
"fileId": "f1~abc123",
"status": "P",
"fileTp": "CRDT",
"fileName": "team-doc.crdt",
"createdAt": 1735000000
},
"time": 1735000000,
"reqId": "req_abc123"
}Note: For BLOB files (images, PDFs, etc.), use POST /api/files/{preset}/{file_name} instead, which creates metadata and uploads the file in a single step.
Upload File (BLOB)
POST /api/files/{preset}/{file_name}Upload binary file data directly. This creates the file metadata and uploads the binary in a single operation.
Authentication: Required
Path Parameters:
preset(string, required) - Image processing preset (e.g.,default,profile-picture,cover-photo)file_name(string, required) - Original filename with extension
Query Parameters:
created_at(number, optional) - Unix timestamp (seconds) for when the file was createdtags(string, optional) - Comma-separated tags (e.g.,vacation,beach,2025)
Content-Type: Binary content type (e.g., image/png, image/jpeg, application/pdf)
Request Body: Raw binary file data
Example:
const api = cloudillo.createApiClient()
// Upload image file
const imageFile = document.querySelector('input[type="file"]').files[0]
const blob = await imageFile.arrayBuffer()
const response = await fetch('/api/files/default/vacation-photo.jpg?tags=vacation,beach', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'image/jpeg'
},
body: blob
})
const result = await response.json()
console.log('File ID:', result.data.fileId) // e.g., "b1~abc123"
Complete upload with File object:
async function uploadFile(file: File, preset = 'default', tags?: string) {
const queryParams = new URLSearchParams()
if (tags) queryParams.set('tags', tags)
const url = `/api/files/${preset}/${encodeURIComponent(file.name)}${
queryParams.toString() ? '?' + queryParams.toString() : ''
}`
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': file.type
},
body: file
})
const result = await response.json()
return result.data.fileId
}
// Usage
const fileInput = document.querySelector('input[type="file"]')
fileInput.addEventListener('change', async (e) => {
const file = e.target.files[0]
const fileId = await uploadFile(file, 'default', 'vacation,beach')
console.log('Uploaded:', fileId)
// Display uploaded image
const img = document.createElement('img')
img.src = `/api/files/${fileId}?variant=sd`
document.body.appendChild(img)
})Response:
{
"data": {
"fileId": "b1~abc123",
"status": "M",
"fileTp": "BLOB",
"contentType": "image/jpeg",
"fileName": "vacation-photo.jpg",
"createdAt": 1735000000,
"tags": ["vacation", "beach"]
},
"time": 1735000000,
"reqId": "req_abc123"
}Download File
GET /api/files/:fileIdDownload a file. Returns binary data with appropriate Content-Type.
Query Parameters:
variant- Image variant (tn, ic, sd, hd, orig)
Example:
// Direct URL usage
<img src="/api/files/b1~abc123" />
// Get specific variant
<img src="/api/files/b1~abc123?variant=sd" />
// Fetch with API
const response = await fetch(`/api/files/${fileId}`)
const blob = await response.blob()
const url = URL.createObjectURL(blob)Responsive images:
<picture>
<source srcset="/api/files/b1~abc123?variant=hd" media="(min-width: 1200px)">
<source srcset="/api/files/b1~abc123?variant=sd" media="(min-width: 600px)">
<img src="/api/files/b1~abc123?variant=tn" alt="Photo">
</picture>Get File Descriptor
GET /api/files/:fileId/descriptorGet file metadata and available variants.
Example:
const descriptor = await api.files.id('b1~abc123').descriptor.get()
console.log('File type:', descriptor.contentType)
console.log('Size:', descriptor.size)
console.log('Variants:', descriptor.variants)Response:
{
"data": {
"fileId": "b1~abc123",
"contentType": "image/png",
"fileName": "photo.png",
"size": 1048576,
"createdAt": "2025-01-01T12:00:00Z",
"variants": [
{
"id": "tn",
"width": 150,
"height": 100,
"size": 5120,
"format": "webp"
},
{
"id": "sd",
"width": 640,
"height": 426,
"size": 51200,
"format": "webp"
},
{
"id": "hd",
"width": 1920,
"height": 1280,
"size": 204800,
"format": "webp"
},
{
"id": "orig",
"width": 3840,
"height": 2560,
"size": 1048576,
"format": "png"
}
]
}
}Update File Metadata
PATCH /api/files/:fileIdUpdate file metadata (tags, filename, etc.).
Authentication: Required (must be owner)
Request Body:
{
fileName?: string
tags?: string // Comma-separated
}Example:
await api.files.id('b1~abc123').patch({
fileName: 'renamed-photo.png',
tags: 'vacation,beach,2025'
})Delete File
DELETE /api/files/:fileIdDelete a file and all its variants.
Authentication: Required (must be owner)
Example:
await api.files.id('b1~abc123').delete()List Tags
GET /api/tagsList all tags used in files owned by the authenticated user.
Authentication: Required
Response:
{
"data": [
{
"tag": "vacation",
"count": 15
},
{
"tag": "project-alpha",
"count": 8
},
{
"tag": "important",
"count": 3
}
]
}Example:
const response = await fetch('/api/tags', {
headers: {
'Authorization': `Bearer ${token}`
}
})
const { data: tags } = await response.json()
// Display tags with counts
tags.forEach(({ tag, count }) => {
console.log(`${tag}: ${count} files`)
})Add Tag
PUT /api/files/:fileId/tag/:tagAdd a tag to a file.
Authentication: Required (must be owner)
Example:
await fetch(`/api/files/b1~abc123/tag/important`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`
}
})Remove Tag
DELETE /api/files/:fileId/tag/:tagRemove a tag from a file.
Authentication: Required (must be owner)
Example:
await fetch(`/api/files/b1~abc123/tag/important`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
})Get File Variant
GET /api/files/variant/:variantIdGet a specific image variant directly.
Example:
// Variant IDs are in the format: {fileId}~{variant}
<img src="/api/files/variant/b1~abc123~sd" />File Identifiers
Cloudillo uses content-addressable identifiers:
Format: {prefix}{version}~{hash}
Prefixes:
b- BLOB filesf- CRDT filesr- RTDB files
Examples:
b1~abc123def- BLOB filef1~xyz789ghi- CRDT filer1~mno456pqr- RTDB file
Variants:
b1~abc123def~sd- SD variant of BLOBb1~abc123def~tn- Thumbnail variant
CRDT Files
CRDT files store collaborative documents using Yjs.
Creating a CRDT file:
// 1. Create file metadata
const file = await api.files.post({
fileTp: 'CRDT',
tags: 'document,collaborative'
})
// 2. Open for editing
import * as Y from 'yjs'
const yDoc = new Y.Doc()
const { provider } = await cloudillo.openYDoc(yDoc, file.fileId)
// 3. Use shared types
const yText = yDoc.getText('content')
yText.insert(0, 'Hello, CRDT!')See CRDT documentation for details.
RTDB Files
RTDB files store structured real-time databases.
Creating an RTDB file:
// 1. Create file metadata
const file = await api.files.post({
fileTp: 'RTDB',
tags: 'database,todos'
})
// 2. Connect to database
import { RtdbClient } from '@cloudillo/rtdb'
const rtdb = new RtdbClient({
fileId: file.fileId,
token: cloudillo.accessToken,
url: 'wss://server.com/ws/rtdb'
})
// 3. Use collections
const todos = rtdb.collection('todos')
await todos.create({ title: 'Learn Cloudillo', done: false })See RTDB documentation for details.
Image Presets
Configure automatic image processing with presets:
const file = await api.files.post({
fileTp: 'BLOB',
contentType: 'image/jpeg',
preset: 'profile-picture' // Custom preset
})Default presets:
default- Standard 5-variant generationprofile-picture- Square crop, 400x400 maxcover-photo- 16:9 crop, 1920x1080 maxthumbnail-only- Only generate thumbnails
Tagging
Tags help organize and filter files.
Best practices:
- Use lowercase tags
- Use hyphens for multi-word tags (e.g.,
project-alpha) - Limit to 3-5 tags per file
- Use namespaced tags for projects (e.g.,
proj:alpha,proj:beta)
Tag filtering:
// Files with ANY of these tags
const files = await api.files.get({
tags: 'vacation,travel'
})
// Files with ALL of these tags (use multiple requests)
const vacationFiles = await api.files.get({ tags: 'vacation' })
const summerFiles = vacationFiles.data.filter(f =>
f.tags?.includes('summer')
)Permissions
File access is controlled by:
- Ownership - Owner has full access
- FSHR actions - Files shared via FSHR actions grant temporary access
- Public files - Files attached to public actions are publicly accessible
- Audience - Files attached to actions inherit action audience permissions
Sharing a file:
// Share file with read access
await api.actions.post({
type: 'FSHR',
subject: 'bob@example.com',
attachments: ['b1~abc123'],
content: {
permission: 'READ', // or 'WRITE'
message: 'Check out this photo!'
}
})Storage Considerations
File size limits:
- Free tier: 100 MB per file
- Pro tier: 1 GB per file
- Enterprise: Configurable
Total storage:
- Free tier: 10 GB
- Pro tier: 100 GB
- Enterprise: Unlimited
Variant generation:
- Only for image files (JPEG, PNG, WebP, AVIF, GIF)
- Automatic async processing
- Lanczos3 filtering for high quality
- Progressive JPEG for faster loading
Best Practices
1. Always Create Metadata First
// ✅ Correct order
const metadata = await api.files.post({ fileTp: 'BLOB', contentType: 'image/png' })
await uploadBinary(metadata.fileId, imageBlob)
// ❌ Wrong - upload will fail without metadata
await uploadBinary('unknown-id', imageBlob)2. Use Appropriate Variants
// ✅ Use thumbnails in lists
<img src={`/api/files/${fileId}?variant=tn`} />
// ✅ Use HD for detail views
<img src={`/api/files/${fileId}?variant=hd`} />
// ❌ Don't use original for thumbnails (wastes bandwidth)
<img src={`/api/files/${fileId}`} width="100" />3. Handle Upload Errors
async function uploadWithRetry(file: File, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
const metadata = await api.files.post({
fileTp: 'BLOB',
contentType: file.type
})
const formData = new FormData()
formData.append('fileId', metadata.fileId)
formData.append('file', file)
await api.files.upload.post(formData)
return metadata.fileId
} catch (error) {
if (i === maxRetries - 1) throw error
await new Promise(r => setTimeout(r, 1000 * (i + 1)))
}
}
}4. Progressive Enhancement
// Show preview immediately, upload in background
function previewAndUpload(file: File) {
// Show local preview
const reader = new FileReader()
reader.onload = (e) => {
setPreview(e.target.result)
}
reader.readAsDataURL(file)
// Upload in background
uploadImage(file).then(fileId => {
setUploadedId(fileId)
})
}5. Clean Up Unused Files
// Delete old temporary files
const oldFiles = await api.files.get({
tags: 'temp',
createdBefore: Date.now() / 1000 - 86400 // 24 hours ago
})
for (const file of oldFiles.data) {
await api.files.id(file.fileId).delete()
}See Also
- Actions API - Attach files to actions
- CRDT - Collaborative document editing
- RTDB - Real-time databases
- Authentication - File access control