> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/trailbaseio/trailbase/llms.txt
> Use this file to discover all available pages before exploring further.

# Files API

> File upload, download, and storage management for record file columns

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

```bash POST /api/records/v1/{name} theme={null}
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

<ParamField path="name" type="string" required>
  Record API name
</ParamField>

### Request Format

Use `multipart/form-data` content type with both data fields and file fields:

```bash theme={null}
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:

1. **Single File** (`FileUpload`): One file per field
2. **Multiple Files** (`FileUploads`): Array of files per field

### Response

```json theme={null}
{
  "ids": ["abc123def456"]
}
```

The file metadata is automatically stored in the record's file column.

## Update Record with File

```bash PATCH /api/records/v1/{name}/{record} theme={null}
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:

1. Old file is marked for deletion
2. New file is uploaded
3. Record is updated with new file metadata
4. Old file is deleted asynchronously

<Note>
  If the update fails, newly uploaded files are automatically cleaned up to prevent orphaned files in storage.
</Note>

## Download Single File

Download a file from a single-file column.

```bash GET /api/records/v1/{name}/{record}/file/{column_name} theme={null}
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

<ParamField path="name" type="string" required>
  Record API name
</ParamField>

<ParamField path="record" type="string" required>
  Record ID
</ParamField>

<ParamField path="column_name" type="string" required>
  Name of the file column
</ParamField>

### 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.

```bash GET /api/records/v1/{name}/{record}/files/{column_name}/{file_name} theme={null}
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

<ParamField path="name" type="string" required>
  Record API name
</ParamField>

<ParamField path="record" type="string" required>
  Record ID
</ParamField>

<ParamField path="column_name" type="string" required>
  Name of the file array column
</ParamField>

<ParamField path="file_name" type="string" required>
  Original filename from the file metadata
</ParamField>

### File Metadata

When you create a record with files, the file column stores metadata:

```json theme={null}
{
  "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

```sql theme={null}
CREATE TABLE profiles (
  id BLOB PRIMARY KEY,
  name TEXT NOT NULL,
  avatar BLOB CHECK(is_file_upload(avatar))
);
```

### Multiple Files Column

```sql theme={null}
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

```toml theme={null}
[storage]
type = "local"
path = "./data/files"
```

### Amazon S3

```toml theme={null}
[storage]
type = "s3"
bucket = "my-bucket"
region = "us-west-2"
access_key = "ACCESS_KEY"
secret_key = "SECRET_KEY"
```

### Google Cloud Storage

```toml theme={null}
[storage]
type = "gcs"
bucket = "my-bucket"
service_account_key = "/path/to/key.json"
```

### Azure Blob Storage

```toml theme={null}
[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:

```toml theme={null}
[server]
max_upload_size = "10MB"  # Default limit
```

### Allowed File Types

Implement CHECK constraints or access rules to restrict file types:

```sql theme={null}
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:

```sql theme={null}
-- Users can only download their own files
read_access_rule: "_ROW_.owner = _USER_.id"
```

Example:

```bash theme={null}
# 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

1. Client sends multipart request with file(s)
2. TrailBase validates file metadata
3. File uploaded to object storage
4. Record created/updated with file metadata
5. On success, file is committed
6. On failure, file is cleaned up

### Delete Flow

1. Record deleted (or file column updated)
2. File marked for deletion in `_file_deletions` table
3. Asynchronous cleanup process deletes file from storage
4. Deletion retried up to 10 times on failure
5. 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

```javascript theme={null}
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

```javascript theme={null}
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

```javascript theme={null}
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

```javascript theme={null}
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

```typescript theme={null}
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:

```javascript theme={null}
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:

```javascript theme={null}
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" />
```

<Warning>
  Never expose auth tokens in public URLs. Use cookie-based auth for public file access or implement signed URLs.
</Warning>

## File Metadata API

File metadata is stored directly in the record. Fetch it via the standard read endpoint:

```bash theme={null}
curl -X GET https://your-instance.com/api/records/v1/documents/abc123 \
  -H "Authorization: Bearer YOUR_AUTH_TOKEN"
```

```json theme={null}
{
  "id": "abc123",
  "title": "My Document",
  "file": {
    "name": "document.pdf",
    "size": 1024000,
    "content_type": "application/pdf",
    "objectstore_id": "storage-id"
  }
}
```

## Best Practices

1. **Validate files client-side** before upload to improve UX
2. **Compress images** before upload to reduce bandwidth and storage
3. **Use appropriate content types** for accurate file handling
4. **Implement progress indicators** for large file uploads
5. **Handle upload failures gracefully** with retry logic
6. **Clean up blob URLs** after use to prevent memory leaks
7. **Set reasonable file size limits** based on your use case
8. **Use signed URLs** for public file access instead of embedding tokens
9. **Consider CDN integration** for frequently accessed files
10. **Monitor storage usage** and implement cleanup policies

## Troubleshooting

### Upload Fails with 413 Payload Too Large

Increase the server's max upload size:

```toml theme={null}
[server]
max_upload_size = "50MB"
```

### Files Not Deleted After Record Deletion

1. Check `_file_deletions` table for pending deletions
2. Verify object storage credentials
3. Review server logs for deletion errors
4. Files are retried up to 10 times before being abandoned

### Cannot Download File (403 Forbidden)

1. Verify user has read access to the record
2. Check row-level access rules
3. Confirm file column is not excluded from API
4. 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

<ResponseField name="400" type="Bad Request">
  Invalid file format, missing required fields, or constraint violation
</ResponseField>

<ResponseField name="403" type="Forbidden">
  Access denied by ACL or row-level access rule
</ResponseField>

<ResponseField name="404" type="Not Found">
  Record, column, or file not found
</ResponseField>

<ResponseField name="405" type="Method Not Allowed">
  API doesn't support file operations or wrong HTTP method
</ResponseField>

<ResponseField name="413" type="Payload Too Large">
  File exceeds maximum upload size
</ResponseField>

<ResponseField name="500" type="Internal Server Error">
  Storage backend error or unexpected server failure
</ResponseField>
