alepha@docs:~/docs/guides/data$
cat 3-file-storage.md
4 min read
Last commit:

#File Storage

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.

#Two Layers of File Storage

#Layer 1: alepha/bucket - Raw Blob Storage

The $bucket primitive provides raw blob storage. It handles:

  • Uploading and downloading binary data
  • MIME type and size validation
  • Multiple storage backends (memory, filesystem, cloud)

This is the low-level layer. Files are stored as blobs with no metadata persistence.

typescript
 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}

#Layer 2: alepha/api/files - Managed File Storage

The AlephaApiFiles module is a superset of alepha/bucket. It adds:

  • Database persistence for file metadata
  • Time-to-live (TTL) with automatic cleanup
  • Tags for organization
  • Creator tracking (audit trail)
  • Checksum calculation (SHA-256)
  • Storage statistics
  • REST API endpoints
typescript
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:

  1. Stores the blob in the bucket
  2. Creates a database record with metadata
  3. Schedules automatic deletion if TTL is set

#Using the Raw Bucket Layer

#Defining Buckets

typescript
 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}

#Upload, Download, Delete

typescript
 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}

#Using the Managed File Layer

#Enable the Module

typescript
1import { Alepha, run } from "alepha";2import { AlephaApiFiles, FileService } from "alepha/api/files";3 4const alepha = Alepha.create().with(AlephaApiFiles);5 6run(alepha);

#Upload with Metadata

typescript
 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}

#TTL with Automatic Cleanup

Set TTL at the bucket level or per-upload:

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

#Query Files

typescript
 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}

#Storage Backends

#Default Behavior

By default, AlephaBucket automatically selects:

  • Test/Serverless: MemoryFileStorageProvider (in-memory, no persistence)
  • Other environments: LocalFileStorageProvider (filesystem)

#Cloud Storage Providers

Cloud providers are not bundled with the main alepha package. Install them separately:

#Azure Blob Storage

npm install @alepha/bucket-azure
typescript
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.

#Vercel Blob

npm install @alepha/bucket-vercel
typescript
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.

#Using Multiple Providers

You can use different storage backends for different buckets in the same app via the provider option:

typescript
 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 filesystem
  • AzureFileStorageProvider - Azure Blob Storage
  • VercelFileStorageProvider - Vercel Blob
  • undefined - Use the default from dependency injection

#Complete Example: Profile Picture Upload

typescript
 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}

#Bucket Events

The bucket layer emits events you can hook into:

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

#Comparison: Raw vs Managed

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:

  • You manage metadata yourself
  • You don't need TTL or tags
  • You want minimal dependencies

Use alepha/api/files when:

  • You need file metadata in the database
  • You want automatic TTL cleanup
  • You need audit trails (who uploaded what)
  • You want built-in REST endpoints

#Tips

  1. Use specific MIME types - image/jpeg is safer than image/*
  2. Set reasonable size limits - Don't trust the client
  3. Store file IDs, not URLs - URLs change, IDs don't
  4. Delete orphaned files - Clean up when records are deleted
  5. Use TTL for temp files - Automatic cleanup prevents storage bloat
  6. Use multiple providers - Temp files in memory, permanent files in cloud

#Summary

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
On This Page
No headings found...
ready
mainTypeScript
UTF-8guides_data_file_storage.md