template-saas/docs/01-modulos/SAAS-011-storage.md
rckrdmrd 4dafffa386 feat: Add superadmin metrics, onboarding and module documentation
- Add MetricsPage and useOnboarding hook
- Update superadmin controller and service
- Add module documentation (docs/01-modulos/)
- Add CONTEXT-MAP.yml and Sprint 5 execution report
- Update project status and task traces

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 05:40:26 -06:00

6.1 KiB

SAAS-011: Storage

Metadata

  • Codigo: SAAS-011
  • Modulo: Storage
  • Prioridad: P1
  • Estado: Pendiente
  • Fase: 3 - Features Core

Descripcion

Sistema de almacenamiento de archivos: upload/download con presigned URLs, organizacion por tenant, limites por plan, y soporte multi-provider (S3, R2, MinIO).

Objetivos

  1. Upload seguro con presigned URLs
  2. Organizacion por tenant
  3. Limites de almacenamiento por plan
  4. Soporte multi-provider
  5. Procesamiento de imagenes

Alcance

Incluido

  • Upload via presigned URLs
  • Download via presigned URLs
  • Organizacion: tenant/tipo/archivo
  • Tracking de uso por tenant
  • Limites por plan
  • Thumbnails automaticos (imagenes)
  • Tipos MIME permitidos

Excluido

  • CDN propio - usar Cloudflare
  • Streaming de video
  • Compresion de archivos

Proveedores Soportados

Proveedor Uso Config
AWS S3 Produccion Bucket por region
Cloudflare R2 Produccion Sin egress fees
MinIO Desarrollo Docker local

Modelo de Datos

Tablas (schema: storage)

files

  • id, tenant_id, uploaded_by
  • filename, original_name
  • mime_type, size_bytes
  • path, bucket
  • metadata (JSONB)
  • created_at

storage_usage

  • id, tenant_id
  • total_files, total_bytes
  • updated_at

Endpoints API

Metodo Endpoint Descripcion
POST /storage/upload-url Obtener presigned upload URL
POST /storage/confirm Confirmar upload exitoso
GET /storage/files Listar archivos
GET /storage/files/:id Metadata de archivo
GET /storage/files/:id/download Presigned download URL
DELETE /storage/files/:id Eliminar archivo
GET /storage/usage Uso actual

Flujo de Upload

1. Cliente solicita upload URL
   POST /storage/upload-url
   Body: { filename, mimeType, size }

2. Backend valida:
   - Tipo MIME permitido
   - Tamaño dentro de limite
   - Espacio disponible

3. Backend genera presigned URL
   - Expira en 15 minutos
   - Limite de tamaño

4. Cliente sube directo a S3/R2
   PUT [presigned-url]
   Body: [file binary]

5. Cliente confirma
   POST /storage/confirm
   Body: { uploadId }

6. Backend registra archivo
   - Actualiza storage_usage

Implementacion

Servicio

interface StorageService {
  getUploadUrl(params: UploadParams): Promise<PresignedUrl>;
  confirmUpload(uploadId: string): Promise<FileRecord>;
  getDownloadUrl(fileId: string): Promise<string>;
  deleteFile(fileId: string): Promise<void>;
  getUsage(tenantId: string): Promise<StorageUsage>;
}

interface UploadParams {
  filename: string;
  mimeType: string;
  sizeBytes: number;
  folder?: string;
}

interface PresignedUrl {
  uploadId: string;
  url: string;
  fields?: Record<string, string>; // Para POST form
  expiresAt: Date;
}

Generacion de URLs

async getUploadUrl(params: UploadParams): Promise<PresignedUrl> {
  const uploadId = uuid();
  const key = `${this.tenantId}/${params.folder || 'files'}/${uploadId}/${params.filename}`;

  const command = new PutObjectCommand({
    Bucket: this.bucket,
    Key: key,
    ContentType: params.mimeType,
    ContentLength: params.sizeBytes,
    Metadata: {
      'tenant-id': this.tenantId,
      'upload-id': uploadId
    }
  });

  const url = await getSignedUrl(this.s3, command, { expiresIn: 900 });

  // Guardar pending upload
  await this.savePendingUpload(uploadId, params);

  return { uploadId, url, expiresAt: addMinutes(new Date(), 15) };
}

Limites por Plan

Plan Storage Max Archivo
Free 100 MB 5 MB
Starter 1 GB 25 MB
Pro 10 GB 100 MB
Enterprise Ilimitado 500 MB

Tipos MIME Permitidos

const ALLOWED_MIME_TYPES = {
  images: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
  documents: ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
  spreadsheets: ['application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
  data: ['text/csv', 'application/json']
};

const BLOCKED_EXTENSIONS = ['.exe', '.bat', '.sh', '.php', '.js'];

Procesamiento de Imagenes

// Al confirmar upload de imagen
if (isImage(file.mimeType)) {
  await this.imageProcessor.createThumbnails(file, [
    { name: 'thumb', width: 150, height: 150 },
    { name: 'medium', width: 800, height: 600 },
    { name: 'large', width: 1920, height: 1080 }
  ]);
}

Estructura de Paths

bucket/
├── tenant-uuid-1/
│   ├── avatars/
│   │   └── user-uuid/
│   │       └── avatar.jpg
│   ├── documents/
│   │   └── upload-id/
│   │       └── contract.pdf
│   └── imports/
│       └── upload-id/
│           └── data.csv
└── tenant-uuid-2/
    └── ...

Entregables

Entregable Estado Archivo
storage.module.ts Pendiente modules/storage/
storage.service.ts Pendiente services/
s3.provider.ts Pendiente providers/
image.processor.ts Pendiente services/
DDL storage schema Pendiente ddl/schemas/storage/

Dependencias

Depende de

  • SAAS-002 (Tenants)
  • SAAS-005 (Plans - limites)
  • AWS S3 / Cloudflare R2 / MinIO

Bloquea a

  • Upload de avatares
  • Adjuntos en modulos
  • Importacion de datos

Criterios de Aceptacion

  • Upload presigned funciona
  • Download presigned funciona
  • Limites se respetan
  • Thumbnails se generan
  • Uso se trackea
  • Archivos se aislan por tenant

Configuracion

{
  storage: {
    provider: 's3', // 's3' | 'r2' | 'minio'
    bucket: process.env.STORAGE_BUCKET,
    region: process.env.AWS_REGION,
    credentials: {
      accessKeyId: process.env.AWS_ACCESS_KEY_ID,
      secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
    },
    // Para R2
    endpoint: process.env.R2_ENDPOINT,
    // Para MinIO (dev)
    endpoint: 'http://localhost:9000',
    forcePathStyle: true
  }
}

Ultima actualizacion: 2026-01-07