Package Exports
- octoload
- octoload/client
- octoload/nextjs
- octoload/react-router
Readme
Octoload
Minimal, composable image upload system with CLI for generating Drizzle schemas and typed clients
Octoload provides direct-to-storage uploads using presigned URLs, eliminating server bottlenecks while maintaining full type safety and database integration.
✨ Features
- 🚀 Direct-to-storage uploads - Files bypass your server, uploaded directly to S3/R2
- 🛠️ CLI-first approach - Generate schemas, routes, and types automatically
- 🔒 Full TypeScript safety - End-to-end type safety with zero
anytypes - 🏗️ Framework adapters - React Router v7, Next.js (more coming)
- 📊 Drizzle integration - Generated schemas work with any Drizzle-compatible database
- 🔄 Multipart uploads - Handle large files efficiently with progress tracking
- 🏷️ Flexible metadata - Tags, alt text, ownership, and custom fields
🚀 Quick Start
Installation
npm install octoload drizzle-ormInitialize Project
npx octoload init --adapter=s3 --framework=react-routerThis generates:
- Database schema (
schema/octoload.ts) - Route handlers (
app/routes/api/uploads.*) - Configuration file (
octoload.config.ts) - Type definitions (fully typed client and server interfaces)
Basic Usage
// Client-side upload
import { OctoloadClient } from 'octoload/client';
const client = new OctoloadClient({
baseUrl: 'https://your-app.com'
});
const result = await client.uploadFile(file, {
onProgress: ({ percentage }) => console.log(`${percentage}% uploaded`),
onStateChange: (state) => console.log(`Upload ${state}`)
});
console.log('Image uploaded:', result.image.id);Server Configuration
// octoload.config.ts
import { defineConfig } from 'octoload';
export default defineConfig({
storage: {
type: 's3',
bucket: process.env.S3_BUCKET!,
region: process.env.S3_REGION!,
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
limits: {
maxFileSize: 10 * 1024 * 1024, // 10MB
allowedTypes: ['image/jpeg', 'image/png', 'image/webp'],
}
});📚 Documentation
Core Concepts
- Design Rationale - Why Octoload exists and architectural decisions
- API Reference - Complete API documentation
- React Router Guide - Framework-specific integration
- Type Safety - TypeScript patterns and best practices
Framework Guides
- React Router v7 - Complete setup and usage
- Next.js - App Router and Pages Router support
- Express - Traditional Node.js integration (coming soon)
- Hono - Edge runtime support (coming soon)
Advanced Topics
- Storage Adapters - S3, R2, and custom adapters
- Database Schema - Understanding generated tables
- Authentication - User ownership and permissions
- Hooks & Events - Custom processing and webhooks
🏗️ Architecture
Direct-to-Storage Flow
sequenceDiagram
participant C as Client
participant S as Your Server
participant DB as Database
participant R2 as R2/S3
C->>S: POST /api/uploads/presign
S->>DB: Create image record (processing)
S->>R2: Generate presigned URL
S->>C: Return presigned URL
C->>R2: Upload file directly
C->>S: POST /api/uploads/finalize
S->>R2: Verify upload
S->>DB: Update status (ready)
S->>C: Return image metadataDatabase Schema
Generated schema includes:
-- Core image metadata
CREATE TABLE images (
id UUID PRIMARY KEY,
owner_id UUID,
filename VARCHAR NOT NULL,
content_type VARCHAR NOT NULL,
byte_size INTEGER NOT NULL,
status upload_status NOT NULL DEFAULT 'processing',
storage_key VARCHAR NOT NULL,
checksum VARCHAR,
is_public BOOLEAN DEFAULT false,
alt VARCHAR,
title VARCHAR,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Multipart upload sessions
CREATE TABLE upload_sessions (
id UUID PRIMARY KEY,
image_id UUID REFERENCES images(id) ON DELETE CASCADE,
upload_id VARCHAR, -- S3 multipart upload ID
part_count INTEGER NOT NULL,
expires_at TIMESTAMP NOT NULL
);
-- Optional: Asset variants (thumbnails, etc.)
CREATE TABLE asset_variants (
id UUID PRIMARY KEY,
image_id UUID REFERENCES images(id) ON DELETE CASCADE,
variant_type VARCHAR NOT NULL, -- 'thumbnail', 'medium', 'large'
storage_key VARCHAR NOT NULL,
width INTEGER,
height INTEGER,
byte_size INTEGER NOT NULL
);
-- Optional: Flexible tagging
CREATE TABLE image_tags (
id UUID PRIMARY KEY,
image_id UUID REFERENCES images(id) ON DELETE CASCADE,
tag VARCHAR NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);🔧 CLI Commands
# Initialize new project
npx octoload init [options]
--adapter <s3|r2> Storage adapter (default: s3)
--framework <react-router|nextjs> Framework integration
--database <postgres|mysql|sqlite> Database type
# Generate/update database schema
npx octoload generate
# Run database migrations
npx octoload migrate
# Add new storage adapter
npx octoload add adapter <name>
# Add framework integration
npx octoload add framework <name>🎯 Use Cases
Perfect For
✅ Modern web applications requiring scalable file uploads
✅ Teams using Drizzle ORM and TypeScript
✅ Applications with strict type safety requirements
✅ Projects needing cost-effective file storage
✅ Multi-tenant applications with user-owned content
Not Ideal For
❌ Simple prototypes (consider simpler solutions first)
❌ Legacy applications without TypeScript
❌ Projects requiring image processing (use with Sharp/ImageMagick)
❌ Applications needing built-in CDN (use CloudFront separately)
🔒 Security Features
- Presigned URL expiration (configurable, default 1 hour)
- File type validation (MIME type checking)
- File size limits (configurable per endpoint)
- Checksum verification (SHA-256 integrity checks)
- User ownership (files tied to authenticated users)
- Access control (public/private file permissions)
🌍 Ecosystem Integration
Octoload works seamlessly with popular tools:
// 🔐 Authentication
import { auth } from './lib/better-auth';
import { createPresignHandler } from 'octoload';
export const action = createPresignHandler({
config,
db,
schema,
getUser: async (context) => {
const session = await auth.api.getSession({
headers: context.request.headers
});
return session?.user || null;
}
});
// 🖼️ Image Processing
import sharp from 'sharp';
export default defineConfig({
hooks: {
afterFinalize: async (image) => {
// Generate thumbnails
const thumbnail = await sharp(imageBuffer)
.resize(200, 200)
.jpeg()
.toBuffer();
await uploadVariant(image.id, 'thumbnail', thumbnail);
}
}
});
// 📊 Analytics
import { track } from './lib/analytics';
export default defineConfig({
hooks: {
afterFinalize: async (image) => {
track('image_uploaded', {
userId: image.ownerId,
fileSize: image.byteSize,
contentType: image.contentType
});
}
}
});📈 Performance
Scaling Characteristics
- Concurrent uploads: Handled by S3/R2 infrastructure
- File size limits: Up to 5TB (multipart uploads)
- Server resources: Constant regardless of file size
- Database load: Minimal (metadata operations only)
Upload performance depends on client network, storage region, and file size.
🤝 Contributing
We welcome contributions! See CONTRIBUTING.md for guidelines.
Development Setup
git clone https://github.com/kahwee/octoload.git
cd octoload
npm install
# or
pnpm installRunning Tests
npm test # Run test suite
npm run typecheck # Type checking
npm run fix # Format code
# or with pnpm
pnpm test
pnpm typecheck
pnpm fix📝 License
MIT License - see LICENSE for details.
🙏 Acknowledgments
Inspired by excellent CLI tools in the ecosystem:
- Drizzle Kit - Database schema management
- Prisma - Type-safe database access
- Better Auth - Modern authentication
- T3 Stack - Full-stack TypeScript patterns
Documentation • Examples • Community
Made with ❤️ for the TypeScript community