The Files API handles file uploads and downloads for records with file columns, providing seamless integration with object storage backends.
Base Path: /api/records/v1
Upload Files with Records
Files are uploaded as part of record creation or updates using multipart form data.
Create Record with File
POST /api/records/v1/{name}
curl -X POST https://your-instance.com/api/records/v1/profiles \
-H "Authorization: Bearer YOUR_AUTH_TOKEN" \
-F "name=John Doe" \
-F "bio=Software Developer" \
-F "avatar=@/path/to/avatar.jpg"
Path Parameters
Use multipart/form-data content type with both data fields and file fields:
curl -X POST https://your-instance.com/api/records/v1/documents \
-H "Authorization: Bearer YOUR_AUTH_TOKEN" \
-F "title=My Document" \
-F "description=Important file" \
-F "file=@/path/to/document.pdf" \
-F "file=@/path/to/another.pdf"
File Column Types
TrailBase supports two file column types:
- Single File (
FileUpload): One file per field
- Multiple Files (
FileUploads): Array of files per field
Response
{
"ids": ["abc123def456"]
}
The file metadata is automatically stored in the record’s file column.
Update Record with File
PATCH /api/records/v1/{name}/{record}
curl -X PATCH https://your-instance.com/api/records/v1/profiles/abc123 \
-H "Authorization: Bearer YOUR_AUTH_TOKEN" \
-F "name=John Doe Updated" \
-F "avatar=@/path/to/new-avatar.jpg"
File Replacement
When updating a file column:
- Old file is marked for deletion
- New file is uploaded
- Record is updated with new file metadata
- Old file is deleted asynchronously
If the update fails, newly uploaded files are automatically cleaned up to prevent orphaned files in storage.
Download Single File
Download a file from a single-file column.
GET /api/records/v1/{name}/{record}/file/{column_name}
curl -X GET https://your-instance.com/api/records/v1/profiles/abc123/file/avatar \
-H "Authorization: Bearer YOUR_AUTH_TOKEN" \
-o avatar.jpg
Path Parameters
Response
Returns the file content with appropriate headers:
Content-Type: image/jpeg
Content-Disposition: attachment
Download File from Array
Download a specific file from a multi-file column.
GET /api/records/v1/{name}/{record}/files/{column_name}/{file_name}
curl -X GET https://your-instance.com/api/records/v1/documents/abc123/files/attachments/document.pdf \
-H "Authorization: Bearer YOUR_AUTH_TOKEN" \
-o document.pdf
Path Parameters
Name of the file array column
Original filename from the file metadata
When you create a record with files, the file column stores metadata:
{
"id": "abc123",
"title": "My Document",
"attachments": [
{
"name": "document.pdf",
"size": 1024000,
"content_type": "application/pdf",
"objectstore_id": "unique-storage-id"
},
{
"name": "image.jpg",
"size": 512000,
"content_type": "image/jpeg",
"objectstore_id": "another-storage-id"
}
]
}
File Column Schema
Single File Column
CREATE TABLE profiles (
id BLOB PRIMARY KEY,
name TEXT NOT NULL,
avatar BLOB CHECK(is_file_upload(avatar))
);
Multiple Files Column
CREATE TABLE documents (
id BLOB PRIMARY KEY,
title TEXT NOT NULL,
attachments BLOB CHECK(is_file_uploads(attachments))
);
Storage Configuration
TrailBase supports multiple storage backends:
Local Filesystem
[storage]
type = "local"
path = "./data/files"
Amazon S3
[storage]
type = "s3"
bucket = "my-bucket"
region = "us-west-2"
access_key = "ACCESS_KEY"
secret_key = "SECRET_KEY"
Google Cloud Storage
[storage]
type = "gcs"
bucket = "my-bucket"
service_account_key = "/path/to/key.json"
Azure Blob Storage
[storage]
type = "azure"
account = "myaccount"
container = "my-container"
access_key = "ACCESS_KEY"
File Upload Constraints
File Size Limits
Configure maximum file size in your server settings:
[server]
max_upload_size = "10MB" # Default limit
Allowed File Types
Implement CHECK constraints or access rules to restrict file types:
CREATE TABLE profiles (
id BLOB PRIMARY KEY,
avatar BLOB CHECK(
is_file_upload(avatar) AND
json_extract(avatar, '$.content_type') IN ('image/jpeg', 'image/png', 'image/webp')
)
);
Access Control
File downloads respect record-level access control:
-- Users can only download their own files
read_access_rule: "_ROW_.owner = _USER_.id"
Example:
# Fails: User doesn't own this record
curl -X GET https://your-instance.com/api/records/v1/profiles/abc123/file/avatar \
-H "Authorization: Bearer OTHER_USER_TOKEN"
# Response: 403 Forbidden
# Success: User owns the record
curl -X GET https://your-instance.com/api/records/v1/profiles/abc123/file/avatar \
-H "Authorization: Bearer OWNER_TOKEN"
# Response: 200 OK (file content)
File Lifecycle
Upload Flow
- Client sends multipart request with file(s)
- TrailBase validates file metadata
- File uploaded to object storage
- Record created/updated with file metadata
- On success, file is committed
- On failure, file is cleaned up
Delete Flow
- Record deleted (or file column updated)
- File marked for deletion in
_file_deletions table
- Asynchronous cleanup process deletes file from storage
- Deletion retried up to 10 times on failure
- Abandoned after 10 failed attempts (logged as error)
Orphaned File Prevention
TrailBase uses transactional file management:
- Files uploaded during failed operations are cleaned up
- Database triggers track files for deletion
- Background jobs handle async file cleanup
- Failed deletions are retried automatically
JavaScript/TypeScript Client
Upload File
const formData = new FormData();
formData.append('title', 'My Document');
formData.append('file', fileInput.files[0]);
const response = await fetch('https://your-instance.com/api/records/v1/documents', {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`
},
body: formData
});
const result = await response.json();
console.log('Uploaded record ID:', result.ids[0]);
Upload Multiple Files
const formData = new FormData();
formData.append('title', 'Multiple Attachments');
// Append multiple files to the same field
for (const file of fileInput.files) {
formData.append('attachments', file);
}
const response = await fetch('https://your-instance.com/api/records/v1/documents', {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`
},
body: formData
});
Download File
const response = await fetch(
'https://your-instance.com/api/records/v1/profiles/abc123/file/avatar',
{
headers: {
'Authorization': `Bearer ${authToken}`
}
}
);
if (response.ok) {
const blob = await response.blob();
const url = URL.createObjectURL(blob);
// Display in img tag
document.querySelector('#avatar').src = url;
// Or trigger download
const a = document.createElement('a');
a.href = url;
a.download = 'avatar.jpg';
a.click();
URL.revokeObjectURL(url);
}
Update File
const formData = new FormData();
formData.append('name', 'Updated Name');
formData.append('avatar', newAvatarFile);
const response = await fetch(
'https://your-instance.com/api/records/v1/profiles/abc123',
{
method: 'PATCH',
headers: {
'Authorization': `Bearer ${authToken}`
},
body: formData
}
);
React File Upload Component
import { useState } from 'react';
interface UploadResult {
ids: string[];
}
function FileUpload({ apiName, authToken }: { apiName: string; authToken: string }) {
const [file, setFile] = useState<File | null>(null);
const [uploading, setUploading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleUpload = async (e: React.FormEvent) => {
e.preventDefault();
if (!file) return;
setUploading(true);
setError(null);
try {
const formData = new FormData();
formData.append('title', file.name);
formData.append('file', file);
const response = await fetch(
`https://your-instance.com/api/records/v1/${apiName}`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`
},
body: formData
}
);
if (!response.ok) {
throw new Error(`Upload failed: ${response.statusText}`);
}
const result: UploadResult = await response.json();
console.log('Upload successful:', result.ids[0]);
setFile(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Upload failed');
} finally {
setUploading(false);
}
};
return (
<form onSubmit={handleUpload}>
<input
type="file"
onChange={(e) => setFile(e.target.files?.[0] || null)}
disabled={uploading}
/>
<button type="submit" disabled={!file || uploading}>
{uploading ? 'Uploading...' : 'Upload'}
</button>
{error && <div style={{ color: 'red' }}>{error}</div>}
</form>
);
}
Image Optimization
For image files, consider implementing client-side compression:
async function compressImage(file, maxWidth = 1200, quality = 0.8) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
let { width, height } = img;
if (width > maxWidth) {
height = (height * maxWidth) / width;
width = maxWidth;
}
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob((blob) => resolve(blob), 'image/jpeg', quality);
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
});
}
// Usage
const originalFile = fileInput.files[0];
const compressedBlob = await compressImage(originalFile);
const compressedFile = new File([compressedBlob], originalFile.name, {
type: 'image/jpeg'
});
formData.append('avatar', compressedFile);
Direct File URLs
File download URLs are deterministic based on the record ID and column name:
/api/records/v1/{api_name}/{record_id}/file/{column_name}
/api/records/v1/{api_name}/{record_id}/files/{column_name}/{file_name}
You can construct URLs client-side without additional API calls:
function getAvatarUrl(recordId: string, authToken: string): string {
return `https://your-instance.com/api/records/v1/profiles/${recordId}/file/avatar?token=${authToken}`;
}
// Use in img tag
<img src={getAvatarUrl(userId, authToken)} alt="Avatar" />
Never expose auth tokens in public URLs. Use cookie-based auth for public file access or implement signed URLs.
File metadata is stored directly in the record. Fetch it via the standard read endpoint:
curl -X GET https://your-instance.com/api/records/v1/documents/abc123 \
-H "Authorization: Bearer YOUR_AUTH_TOKEN"
{
"id": "abc123",
"title": "My Document",
"file": {
"name": "document.pdf",
"size": 1024000,
"content_type": "application/pdf",
"objectstore_id": "storage-id"
}
}
Best Practices
- Validate files client-side before upload to improve UX
- Compress images before upload to reduce bandwidth and storage
- Use appropriate content types for accurate file handling
- Implement progress indicators for large file uploads
- Handle upload failures gracefully with retry logic
- Clean up blob URLs after use to prevent memory leaks
- Set reasonable file size limits based on your use case
- Use signed URLs for public file access instead of embedding tokens
- Consider CDN integration for frequently accessed files
- Monitor storage usage and implement cleanup policies
Troubleshooting
Upload Fails with 413 Payload Too Large
Increase the server’s max upload size:
[server]
max_upload_size = "50MB"
Files Not Deleted After Record Deletion
- Check
_file_deletions table for pending deletions
- Verify object storage credentials
- Review server logs for deletion errors
- Files are retried up to 10 times before being abandoned
Cannot Download File (403 Forbidden)
- Verify user has read access to the record
- Check row-level access rules
- Confirm file column is not excluded from API
- Ensure auth token is valid and not expired
File Upload Succeeds but Record Creation Fails
Files are automatically cleaned up when the transaction fails. This is expected behavior to prevent orphaned files.
Error Responses
Invalid file format, missing required fields, or constraint violation
Access denied by ACL or row-level access rule
Record, column, or file not found
API doesn’t support file operations or wrong HTTP method
File exceeds maximum upload size
Storage backend error or unexpected server failure