File uploads are one of those features that seem simple until you actually implement them. MIME type validation, size limits, cloud storage integration, signed URLs...
Alepha provides two layers for file storage, depending on your needs.
alepha/bucket - Raw Blob StorageThe $bucket primitive provides raw blob storage. It handles:
This is the low-level layer. Files are stored as blobs with no metadata persistence.
1import { $bucket } from "alepha/bucket"; 2 3class MediaService { 4 avatars = $bucket({ 5 name: "avatars", 6 mimeTypes: ["image/jpeg", "image/png"], 7 maxSize: 5, // MB 8 }); 9 10 async upload(file: FileLike): Promise<string> {11 // Returns a blob ID - you manage metadata yourself12 return await this.avatars.upload(file);13 }14}
alepha/api/files - Managed File StorageThe AlephaApiFiles module is a superset of alepha/bucket. It adds:
1import { Alepha, run } from "alepha";2import { AlephaApiFiles } from "alepha/api/files";3 4const alepha = Alepha.create().with(AlephaApiFiles);5 6run(alepha);
With AlephaApiFiles, every upload automatically:
1import { $bucket } from "alepha/bucket"; 2 3class MediaService { 4 // Profile pictures: images only, 5MB max 5 avatars = $bucket({ 6 name: "avatars", 7 mimeTypes: ["image/jpeg", "image/png", "image/webp"], 8 maxSize: 5, 9 });10 11 // Documents: PDFs and office files, 50MB max12 documents = $bucket({13 name: "documents",14 mimeTypes: [15 "application/pdf",16 "application/msword",17 "application/vnd.openxmlformats-officedocument.wordprocessingml.document",18 ],19 maxSize: 50,20 });21}
1class AvatarApi { 2 media = $inject(MediaService); 3 4 upload = $action({ 5 method: "POST", 6 path: "/avatar", 7 schema: { 8 body: t.object({ file: t.file() }), 9 response: t.object({ fileId: t.text() }),10 },11 handler: async ({ body }) => {12 const fileId = await this.media.avatars.upload(body.file);13 return { fileId };14 },15 });16 17 download = $action({18 path: "/avatar/:id",19 handler: async ({ params }) => {20 return await this.media.avatars.download(params.id);21 },22 });23 24 delete = $action({25 method: "DELETE",26 path: "/avatar/:id",27 handler: async ({ params }) => {28 await this.media.avatars.delete(params.id);29 return { ok: true };30 },31 });32}
1import { Alepha, run } from "alepha";2import { AlephaApiFiles, FileService } from "alepha/api/files";3 4const alepha = Alepha.create().with(AlephaApiFiles);5 6run(alepha);
1class DocumentService { 2 fileService = $inject(FileService); 3 4 async uploadContract(file: FileLike, user: UserAccountToken) { 5 // File is stored + metadata persisted in database 6 const entity = await this.fileService.uploadFile(file, { 7 bucket: "documents", 8 user, // Creator tracking 9 tags: ["contract", "legal"],10 expirationDate: "2025-12-31", // Auto-deleted after this date11 });12 13 return entity; // Includes id, name, size, mimeType, checksum, etc.14 }15}
Set TTL at the bucket level or per-upload:
1class TempService { 2 // All files in this bucket expire after 24 hours 3 tempFiles = $bucket({ 4 name: "temp", 5 ttl: [24, "hours"], 6 }); 7 8 async uploadTemp(file: FileLike) { 9 // Automatically deleted after 24 hours10 return await this.tempFiles.upload(file);11 }12 13 async uploadWithCustomTTL(file: FileLike) {14 // Override: expire in 1 hour15 return await this.tempFiles.upload(file, {16 ttl: [1, "hour"],17 });18 }19}
The FileJobs scheduler runs every 15 minutes to purge expired files.
1class AdminService { 2 fileService = $inject(FileService); 3 4 async listUserFiles(userId: string) { 5 return await this.fileService.findFiles({ 6 creator: userId, 7 sort: "-createdAt", 8 page: 0, 9 size: 20,10 });11 }12 13 async getStorageStats() {14 // Total size, file count, breakdown by bucket and MIME type15 return await this.fileService.getStorageStats();16 }17}
By default, AlephaBucket automatically selects:
MemoryFileStorageProvider (in-memory, no persistence)LocalFileStorageProvider (filesystem)Cloud providers are not bundled with the main alepha package. Install them separately:
npm install @alepha/bucket-azure
1import { Alepha, run } from "alepha";2import { AlephaBucketAzure } from "@alepha/bucket-azure";3 4const alepha = Alepha.create().with(AlephaBucketAzure);5 6run(alepha);
Set AZURE_STORAGE_CONNECTION_STRING in your environment.
npm install @alepha/bucket-vercel
1import { Alepha, run } from "alepha";2import { AlephaBucketVercel } from "@alepha/bucket-vercel";3 4const alepha = Alepha.create().with(AlephaBucketVercel);5 6run(alepha);
Set BLOB_READ_WRITE_TOKEN in your environment.
You can use different storage backends for different buckets in the same app via the provider option:
1import { $bucket, MemoryFileStorageProvider } from "alepha/bucket"; 2import { AzureFileStorageProvider } from "@alepha/bucket-azure"; 3import { VercelFileStorageProvider } from "@alepha/bucket-vercel"; 4 5class StorageService { 6 // Temporary files: in-memory (fast, no persistence) 7 temp = $bucket({ 8 name: "temp", 9 provider: "memory", // Shorthand for MemoryFileStorageProvider10 maxSize: 10,11 });12 13 // User avatars: Azure Blob Storage14 avatars = $bucket({15 name: "avatars",16 provider: AzureFileStorageProvider,17 mimeTypes: ["image/jpeg", "image/png"],18 maxSize: 5,19 });20 21 // Public assets: Vercel Blob (CDN-backed)22 assets = $bucket({23 name: "assets",24 provider: VercelFileStorageProvider,25 maxSize: 50,26 });27 28 // Documents: Use default provider (from DI container)29 documents = $bucket({30 name: "documents",31 // No provider specified - uses FileStorageProvider from DI32 mimeTypes: ["application/pdf"],33 maxSize: 100,34 });35}
Provider options:
"memory" - In-memory storage (shorthand)MemoryFileStorageProvider - In-memory storage (explicit)LocalFileStorageProvider - Local filesystemAzureFileStorageProvider - Azure Blob StorageVercelFileStorageProvider - Vercel Blobundefined - Use the default from dependency injection 1import { $bucket } from "alepha/bucket"; 2import { AzureFileStorageProvider } from "@alepha/bucket-azure"; 3 4class ProfileService { 5 repo = $repository(userEntity); 6 7 avatars = $bucket({ 8 name: "avatars", 9 provider: AzureFileStorageProvider,10 mimeTypes: ["image/jpeg", "image/png", "image/webp"],11 maxSize: 5,12 });13 14 uploadAvatar = $action({15 method: "POST",16 path: "/profile/avatar",17 secure: true,18 schema: {19 body: t.object({ file: t.file() }),20 response: t.object({ avatarUrl: t.text() }),21 },22 handler: async ({ body, user }) => {23 // Delete old avatar if exists24 const profile = await this.repo.findById(user.id);25 if (profile.avatarId) {26 await this.avatars.delete(profile.avatarId);27 }28 29 // Upload new one30 const fileId = await this.avatars.upload(body.file);31 32 // Update user record33 await this.repo.updateById(user.id, { avatarId: fileId });34 35 return { avatarUrl: `/api/avatar/${fileId}` };36 },37 });38 39 getAvatar = $action({40 path: "/avatar/:id",41 handler: async ({ params }) => {42 return await this.avatars.download(params.id);43 },44 });45}
The bucket layer emits events you can hook into:
1class FileAuditService { 2 onUpload = $hook({ 3 on: "bucket:file:uploaded", 4 handler: async ({ id, file, bucket, options }) => { 5 console.log(`File ${file.name} uploaded to ${bucket.name}`); 6 }, 7 }); 8 9 onDelete = $hook({10 on: "bucket:file:deleted",11 handler: async ({ id, bucket }) => {12 console.log(`File ${id} deleted from ${bucket.name}`);13 },14 });15}
The AlephaApiFiles module uses these hooks internally to persist metadata to the database.
| Feature | alepha/bucket |
alepha/api/files |
|---|---|---|
| Blob storage | Yes | Yes |
| MIME/size validation | Yes | Yes |
| Multiple backends | Yes | Yes |
| Database metadata | No | Yes |
| TTL/auto-delete | No | Yes |
| Tags | No | Yes |
| Creator tracking | No | Yes |
| Checksum | No | Yes |
| REST API | No | Yes |
| Storage stats | No | Yes |
Use alepha/bucket when:
Use alepha/api/files when:
image/jpeg is safer than image/*| Need | Solution |
|---|---|
| Raw blob storage | $bucket({ name, mimeTypes, maxSize }) |
| Managed files with metadata | AlephaApiFiles module |
| Accept file in action | t.file() in schema |
| Upload file | bucket.upload(file) |
| Download file | bucket.download(fileId) |
| Delete file | bucket.delete(fileId) |
| Use Azure storage | Install @alepha/bucket-azure, use AlephaBucketAzure |
| Use Vercel storage | Install @alepha/bucket-vercel, use AlephaBucketVercel |
| Multiple providers | Set provider option per bucket |
| Auto-delete expired files | Set ttl option with AlephaApiFiles |