Skip to main content

Uploading Artifacts

Boards supports direct artifact uploads, allowing users to bring their own content into boards alongside AI-generated artifacts. This feature enables workflows like:

  • Uploading reference images for image-to-image generation
  • Adding source videos for video editing
  • Importing audio files for processing
  • Uploading text files as prompts or inputs

Supported Artifact Types

The upload system supports the following artifact types:

  • Images: JPEG, PNG, GIF, WebP, BMP, SVG
  • Videos: MP4, QuickTime (MOV), AVI, WebM, MPEG, MKV
  • Audio: MP3, WAV, OGG, WebM, M4A
  • Text: Plain text, Markdown, JSON, HTML, CSV

Upload Methods

Boards provides two upload methods:

1. File Upload (Multipart)

Direct file upload from the user's device via REST API endpoint.

Endpoint: POST /api/uploads/artifact

Advantages:

  • Progress tracking support
  • Large file handling
  • Works with all file types

2. URL Download

Download artifacts from external URLs via GraphQL mutation.

Mutation: uploadArtifact

Advantages:

  • No local file needed
  • Works with web-hosted content
  • Useful for automation

Using the Upload Hook

The @weirdfingers/boards package provides a useUpload hook for React applications:

import { useUpload, ArtifactType } from '@weirdfingers/boards';

function MyComponent() {
const { upload, isUploading, progress, error } = useUpload();

const handleFileUpload = async (file: File) => {
try {
const result = await upload({
boardId: 'your-board-id',
artifactType: ArtifactType.IMAGE,
source: file, // Can be File or URL string
userDescription: 'My uploaded image',
});
console.log('Upload complete:', result.id);
} catch (err) {
console.error('Upload failed:', err);
}
};

return (
<div>
<input
type="file"
onChange={(e) => e.target.files?.[0] && handleFileUpload(e.target.files[0])}
disabled={isUploading}
/>
{isUploading && <progress value={progress} max={100} />}
{error && <p>Error: {error.message}</p>}
</div>
);
}

Hook API

Parameters

interface UploadRequest {
boardId: string; // UUID of the target board
artifactType: ArtifactType; // Type of artifact (IMAGE, VIDEO, AUDIO, TEXT)
source: File | string; // File object or URL string
userDescription?: string; // Optional description
parentGenerationId?: string; // Optional parent generation UUID
}

Return Value

interface UploadHook {
upload: (request: UploadRequest) => Promise<UploadResult>;
isUploading: boolean; // True while upload in progress
progress: number; // Upload progress (0-100)
error: Error | null; // Error if upload failed
}

interface UploadResult {
id: string; // UUID of created generation
status: string; // 'completed' or 'failed'
storageUrl: string; // URL to access the artifact
}

Upload UI Component

The Baseboards example app includes a full-featured upload component with:

  • Drag-and-drop support
  • File picker
  • URL input
  • Clipboard paste (for images)
  • Progress indication
  • Error handling

See apps/baseboards/src/components/boards/UploadArtifact.tsx for the complete implementation.

Security Features

The upload system includes multiple security measures:

SSRF Protection

URL uploads are validated to prevent Server-Side Request Forgery (SSRF) attacks:

  • ✅ Allowed: http:// and https:// public URLs
  • ❌ Blocked: localhost, 127.0.0.1, private IP ranges (10.x.x.x, 192.168.x.x, 172.16-31.x.x)
  • ❌ Blocked: Link-local addresses (169.254.x.x, AWS metadata endpoint)
  • ❌ Blocked: Non-HTTP schemes (file://, ftp://, etc.)

File Type Validation

  • MIME type validation ensures uploaded content matches the declared artifact type
  • File extensions are checked against an allowlist
  • Content-Type headers are verified

File Size Limits

Default maximum file size is 100MB (configurable via BOARDS_MAX_UPLOAD_SIZE).

Filename Sanitization

All filenames are sanitized to prevent:

  • Path traversal attacks (../, absolute paths)
  • Null byte injection
  • Dangerous characters (<>:"|?*)

File Size Configuration

Configure the maximum upload size in your backend .env:

# Allow uploads up to 500MB
BOARDS_MAX_UPLOAD_SIZE=524288000

Allowed extensions are configured in packages/backend/src/boards/config.py.

GraphQL Mutation

For URL-based uploads via GraphQL:

mutation UploadArtifactFromUrl($input: UploadArtifactInput!) {
uploadArtifact(input: $input) {
id
status
storageUrl
artifactType
generatorName
}
}

Variables:

{
"input": {
"boardId": "550e8400-e29b-41d4-a716-446655440000",
"artifactType": "IMAGE",
"fileUrl": "https://example.com/image.jpg",
"userDescription": "Reference image",
"parentGenerationId": null
}
}

REST API

For file uploads via REST:

curl -X POST http://localhost:8088/api/uploads/artifact \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "board_id=550e8400-e29b-41d4-a716-446655440000" \
-F "artifact_type=image" \
-F "file=@/path/to/image.jpg" \
-F "user_description=My upload"

Response:

{
"id": "650e8400-e29b-41d4-a716-446655440001",
"status": "completed",
"storageUrl": "http://localhost:8088/api/storage/...",
"artifactType": "image",
"generatorName": "user-upload-image"
}

Generator Naming Convention

Uploaded artifacts are stored as Generation records with a special generator_name pattern:

  • user-upload-image - Uploaded images
  • user-upload-video - Uploaded videos
  • user-upload-audio - Uploaded audio files
  • user-upload-text - Uploaded text files

This allows uploads to integrate seamlessly with the generation lineage system.

Storage

Uploaded artifacts are stored using the configured storage provider:

  • Local: Files stored in BOARDS_STORAGE_LOCAL_BASE_PATH
  • S3: Uploaded to configured S3 bucket
  • Supabase Storage: Uploaded to Supabase bucket
  • Google Cloud Storage: Uploaded to GCS bucket

See the Storage documentation for configuration details.

Error Handling

The upload system provides detailed error messages for common issues:

ErrorCauseSolution
"URL not allowed"SSRF protection blocked the URLUse a public HTTP/HTTPS URL
"Invalid file type"MIME type doesn't match artifact typeEnsure file type matches declared type
"File size exceeds maximum"File too largeReduce file size or increase limit
"Permission denied"User can't upload to boardUser must be board owner or editor
"Board not found"Invalid board IDCheck board ID is correct

Best Practices

  1. Client-side validation: Validate file size and type before upload to improve UX
  2. Progress feedback: Always show upload progress for files >1MB
  3. Error recovery: Allow users to retry failed uploads
  4. Cancellation: Implement upload cancellation for large files
  5. Chunked uploads: For very large files (>100MB), consider implementing chunked uploads

Example: Drag-and-Drop Upload

import { useUpload, ArtifactType } from '@weirdfingers/boards';
import { useCallback, useState } from 'react';

function DragDropUpload({ boardId }: { boardId: string }) {
const { upload, isUploading, progress } = useUpload();
const [dragActive, setDragActive] = useState(false);

const handleDrop = useCallback(async (e: React.DragEvent) => {
e.preventDefault();
setDragActive(false);

const files = e.dataTransfer.files;
if (files.length > 0) {
const file = files[0];

// Determine artifact type from file MIME type
let artifactType = ArtifactType.IMAGE;
if (file.type.startsWith('video/')) {
artifactType = ArtifactType.VIDEO;
} else if (file.type.startsWith('audio/')) {
artifactType = ArtifactType.AUDIO;
}

await upload({ boardId, artifactType, source: file });
}
}, [upload, boardId]);

return (
<div
onDrop={handleDrop}
onDragOver={(e) => { e.preventDefault(); setDragActive(true); }}
onDragLeave={() => setDragActive(false)}
className={dragActive ? 'border-blue-500' : 'border-gray-300'}
style={{ border: '2px dashed', padding: '2rem', textAlign: 'center' }}
>
{isUploading ? (
<div>Uploading... {Math.round(progress)}%</div>
) : (
<div>Drag and drop files here</div>
)}
</div>
);
}