1. Vision General
El sistema de media de Nvito maneja todos los archivos subidos por usuarios: fotos de la galería del evento, imagenes hero de invitaciones, logos de organización, fondos de sección, audio del guestbook, y videos del evento.
El diseño sigue el principio de upload directo a S3: el cliente nunca envia el archivo al servidor de Nvito. En su lugar, solicita una URL pre-firmada (presigned URL), sube el archivo directamente al bucket de S3, y luego confirma la operación. Esto elimina la carga de procesamiento del API y permite manejar archivos grandes sin timeout.
Numeros clave
| Concepto | Detalle |
|---|---|
| Validez de presigned URL | 15 minutos |
| Tamano máximo por archivo | Depende del plan (5MB a 2GB) |
| Formatos imagen | JPEG, PNG, WebP, AVIF, GIF |
| Formatos audio | MP3, WAV, OGG, M4A, WebM |
| Formatos video | MP4, MOV, WebM |
| Thumbnails generados | 3 tamaños (150px, 400px, 800px) |
2. Presigned URL Flow
El upload comienza cuando el cliente solicita permiso para subir un archivo. El servidor no recibe el archivo, solo genera una URL temporal con permisos de escritura.
Paso 1: Solicitar URL
El cliente envia POST /v1/media/get-upload-url con:
| Campo | Tipo | Descripción |
|---|---|---|
eventId | UUID | Evento al que pertenece el media |
category | Enum | Categoría del archivo (ver sección 5) |
fileName | String | Nombre original del archivo |
contentType | String | MIME type declarado por el cliente |
fileSize | Number | Tamano en bytes |
Paso 2: Validación previa
Antes de generar la URL, MediaService valida:
- Permisos: El usuario tiene acceso al evento (via
OrganizationResolverService) - MIME type: Esta en la whitelist de tipos permitidos para la categoria
- Tamano: No excede el limite del plan de la organización
- Cuota: La organización no ha excedido su cuota total de almacenamiento
Paso 3: Generar presigned URL
StorageService.generatePresignedUrl() genera una URL con:
- Metodo: PUT
- Bucket: Segun la categoría (ver sección 7)
- Key:
{organizationId}/{eventId}/{category}/{uuid}.{ext} - Expiracion: 15 minutos
- Condiciones:
Content-Typedebe coincidir con el declarado,Content-Lengthdentro del rango
Paso 4: Upload directo
El cliente ejecuta un PUT directo contra la presigned URL de S3 con el archivo como body. Este request no pasa por nvito-api.
Respuestá al cliente
| Campo | Descripción |
|---|---|
uploadUrl | URL pre-firmada para PUT directo a S3 |
mediaId | UUID generado para el registro Media |
key | Ruta del archivo en el bucket |
expiresAt | Momento en que la URL expira |
3. Confirmación y Validación
Después de subir el archivo a S3, el cliente debe confirmar la operación. Esta confirmación dispara la validación real del archivo.
Endpoint de confirmación
POST /v1/media/confirm-upload con { mediaId }
Pipeline de validación
- Verificar existencia: Confirmar que el archivo existe en S3 con el key esperado
- Verificar tamano real: Comparar
Content-Lengthdel objeto en S3 con el tamano declarado - Magic bytes check: Leer los primeros bytes del archivo y verificar que coincidan con el MIME type declarado. Esto previene ataques donde se renombra un ejecutable como
.jpg
| MIME type | Magic bytes esperados |
|---|---|
image/jpeg | FF D8 FF |
image/png | 89 50 4E 47 0D 0A 1A 0A |
image/webp | 52 49 46 46 ... 57 45 42 50 |
image/gif | 47 49 46 38 |
audio/mpeg | FF FB o 49 44 33 (ID3) |
video/mp4 | 00 00 00 ... 66 74 79 70 (ftyp) |
- Actualizar registro: Marcar el
MediacomoCONFIRMED, almacenar metadata (tamano real, dimensiones si es imagen) - Disparar procesamiento: Encolar job de procesamiento en Bull queue
Registro Media
| Campo | Tipo | Descripción |
|---|---|---|
id | UUID | Identificador único |
eventId | UUID | Evento asociado |
organizationId | UUID | Organización propietaria |
category | Enum | Categoría del media |
originalFileName | String | Nombre original del archivo |
key | String | Ruta en S3 |
bucket | String | Bucket de almacenamiento |
contentType | String | MIME type verificado |
fileSize | Number | Tamano en bytes |
width | Number (nullable) | Ancho en pixeles (imagenes) |
height | Number (nullable) | Alto en pixeles (imagenes) |
duration | Number (nullable) | Duracion en segundos (audio/video) |
thumbnails | JSON | URLs de thumbnails generados |
status | Enum | PENDING, CONFIRMED, PROCESSING, READY, ERROR |
uploadedBy | UUID | Usuario que subio el archivo |
createdAt | DateTime | Momento de creación |
4. Procesamiento de Imagenes
Las imagenes confirmadas pasan por un pipeline de procesamiento usando la libreria sharp.
Pipeline
- Metadata extraction: Leer dimensiones (width, height), formato, espacio de color, y datos EXIF
- EXIF rotation: Aplicar rotación automática basada en la orientacion EXIF (común en fotos de celular)
- Strip EXIF: Eliminar datos EXIF sensibles (ubicación GPS, modelo de dispositivo) por privacidad
- Thumbnails: Generar 3 variantes redimensionadas:
| Variante | Tamano max | Uso |
|---|---|---|
thumb | 150x150px (crop center) | Grilla de galería, listas |
medium | 400x400px (fit inside) | Previews, cards |
large | 800x800px (fit inside) | Lightbox en invitaciones |
- Formato WebP: Todas las variantes se generan en WebP para optimizar tamano
- Upload de variantes: Las 3 variantes se suben al bucket
nvito-assetscon keys derivados del original - Actualizar registro: Guardar URLs de thumbnails en el campo JSON del Media
Procesamiento asincrono
El procesamiento corre en una Bull queue (media-processing) con:
- Concurrencia: 2 jobs simultaneos por worker
- Timeout: 60 segundos por job
- Reintentos: 3 intentos con backoff exponencial
- Dead letter: Jobs fallidos se mueven a DLQ para revision manual
5. Categorías de Media
Cada archivo subido pertenece a una categoría que determina su bucket, procesamiento y visibilidad.
| Categoría | Bucket | Procesamiento | Visibilidad |
|---|---|---|---|
GALLERY | nvito-uploads | Thumbnails + WebP | Publica (invitación) |
HERO | nvito-assets | Thumbnails + WebP | Publica (invitación) |
LOGO | nvito-assets | Resize 200px + WebP | Publica (invitación) |
BACKGROUND | nvito-assets | Resize 1920px + WebP | Publica (invitación) |
AUDIO | nvito-uploads | Solo validación | Publica (guestbook) |
VIDEO | nvito-uploads | Solo validación | Publica (galería) |
6. Limites por Plan
Cada organización tiene un plan que determina los limites de almacenamiento:
| Plan | Limite por archivo | Cuota total | Archivos max |
|---|---|---|---|
| FREE | 5 MB | 100 MB | 20 |
| ESSENTIAL | 50 MB | 2 GB | 200 |
| PLUS | 200 MB | 10 GB | 1,000 |
| VIP | 500 MB | 50 GB | 5,000 |
| PREMIUM | 2 GB | 200 GB | Ilimitado |
Verificacion de cuota
Antes de generar una presigned URL, el sistema calcula el almacenamiento usado:
- Suma
fileSizede todos losMediaconstatus != ERRORde la organización - Compara con la cuota del plan
- Si la suma + el nuevo archivo excede la cuota, retorna
413 Payload Too Largecon detalle de uso
7. Infraestructura de Storage
4 Buckets
| Bucket | Proposito | Acceso |
|---|---|---|
nvito-uploads | Archivos subidos por usuarios (galería, audio, video) | Privado, acceso via presigned URL |
nvito-assets | Imagenes procesadas (hero, logo, background, thumbnails) | Publico via CDN |
nvito-private | Archivos internos (exports, reportes) | Privado, solo API |
nvito-templates | HTML compilado de invitaciones | Publico via CDN |
CDN y Storage por ambiente
| Ambiente | Storage | CDN |
|---|---|---|
| Desarrollo | MinIO (local, puerto 9000) | Directo desde MinIO |
| Producción | Cloudflare R2 | Cloudflare CDN (cdn.nvito.mx) |
Estructura de keys
Los archivos siguen una estructura de directorios lógica:
{bucket}/
{organizationId}/
{eventId}/
gallery/
{uuid}.jpg (original)
{uuid}_thumb.webp (150px)
{uuid}_medium.webp (400px)
{uuid}_large.webp (800px)
hero/
{uuid}.jpg
audio/
{uuid}.mp3
video/
{uuid}.mp4
8. Galería Publica en Invitaciones
La sección de galería en las invitaciones públicas muestra las fotos del evento en una grilla responsive con lightbox.
Flujo de visualización
- La invitación HTML incluye una sección
gallerycon URLs de thumbnailsmedium(400px) - Al hacer clic en una foto, se abre el lightbox con la variante
large(800px) - Las imagenes se cargan desde CDN (
cdn.nvito.mx) con cache headers agresivos
Galería colaborativa (día del evento)
Los invitados pueden subir fotos desde la app movil o PWA:
- Invitado selecciona o toma una foto desde su dispositivo
- La app solicita presigned URL y sube directo a S3
- Confirma el upload y la foto entra en cola de procesamiento
- Al estar lista, aparece automáticamente en la galería del evento
- El anfitrion puede moderar: aprobar o rechazar fotos desde el admin o la app
9. Audio Guestbook
El audio guestbook permite a los invitados grabar mensajes de voz para los anfitriones.
Grabación
| Plataforma | Tecnología | Formato nativo |
|---|---|---|
| App nativa | expo-av (Audio.Recording) | M4A (iOS), OGG (Android) |
| PWA | MediaRecorder API | WebM (Chrome), OGG (Firefox) |
Flujo de grabación y moderación
- Invitado presiona "Grabar mensaje" en la sección de audio guestbook
- La app verifica el límite por invitado (3 audios) via
/my-count - La app solicita permiso de micrófono
- Graba hasta 60 segundos (límite configurable)
- Muestra preview con botón de reproducción
- Invitado confirma y el audio se sube via presigned URL flow
- El audio queda en estado pendiente de moderación
- El anfitrión (HOST) recibe una notificación push y revisa el audio desde la pantalla Memorias
- El anfitrión puede aprobar (
PATCH .../approve) o rechazar (DELETE ...) el audio - Si se aprueba, el audio es visible en la galería de audios del evento
- Si se rechaza, el audio se elimina del storage y del registro
- El administrador puede gestionar todos los audios desde el Media Center del admin
Estados de moderación de audio
| Estado | Descripción |
|---|---|
PENDING | Audio recién subido, pendiente de revisión por el host |
APPROVED | Audio aprobado por el host, visible públicamente |
REJECTED | Audio rechazado y eliminado |
Metadata de audio
| Campo | Descripción |
|---|---|
duration | Duración en segundos |
guestId | Invitado que grabó el mensaje |
guestName | Nombre para mostrar en la lista |
recordedAt | Momento de la grabación |
status | Estado de moderación (PENDING, APPROVED, REJECTED) |
10. Límites por Invitado
Cada invitado tiene un límite de contenido que puede subir al evento. Esto previene abuso y mantiene la calidad del contenido.
| Tipo de contenido | Límite por invitado | Endpoint de verificación |
|---|---|---|
| Fotos | 3 | GET /mobile/events/:id/gallery/my-count |
| Audios | 3 | GET /mobile/events/:id/audio-guestbook/my-count |
| Mensajes de felicitación | 3 | GET /mobile/events/:id/messages/my-count |
Respuesta de conteo
Todos los endpoints de conteo retornan:
{ "count": 2, "limit": 3 }
count: Cantidad actual subida por el invitadolimit: Máximo permitido
Verificación doble
- Frontend: Consulta el conteo antes de mostrar el botón de acción. Si
count >= limit, el botón se deshabilita con mensaje informativo. - Backend: Valida el límite al recibir la petición. Si se excede, retorna
403 Forbidden.
11. Paginación Server-Side
Los endpoints de media soportan paginación server-side para manejar grandes volúmenes de contenido eficientemente.
| Parámetro | Tipo | Default | Descripción |
|---|---|---|---|
page | number | 1 | Número de página |
limit | number | 20 | Items por página |
Respuesta paginada
{
"data": [...],
"pagination": {
"page": 1,
"limit": 20,
"total": 87,
"pages": 5
}
}
Endpoints con paginación
GET /mobile/events/:id/gallery— Galería de fotosGET /mobile/events/:id/audio-guestbook— Mensajes de audioGET /mobile/events/:id/messages— Mensajes de felicitación (solo HOST)
En la app móvil, la pantalla Host Memorias usa useInfiniteQuery de TanStack React Query para implementar infinite scroll sobre estos endpoints paginados.
12. Diagrama del Flujo de Upload
Flujo de Upload de Media
13. Archivos Clave
nvito-api (Backend)
| Archivo | Responsabilidad |
|---|---|
media.service.ts | Orquestador: get-upload-url, confirm-upload, listado |
storage.service.ts | Abstracción S3: presigned URLs, head, get, delete |
image-processor.service.ts | Pipeline sharp: resize, thumbnails, WebP, metadata |
media.controller.ts | Endpoints REST para media |
media-processing.processor.ts | Bull processor para procesamiento asincrono |
nvito-admin (Panel de Administración)
| Archivo | Responsabilidad |
|---|---|
lib/api/services/media.service.ts | Servicio API para upload desde admin |
lib/actions/media-actions.ts | Server actions: getUploadUrlAction, confirmUploadAction |
components/media/upload-dialog.tsx | Dialog de upload con drag & drop |
components/media/gallery-manager.tsx | Gestión de galería del evento |
nvito-client (App Nativa)
| Archivo | Responsabilidad |
|---|---|
src/api/services/media.service.ts | Servicio API para upload movil |
src/hooks/queries/use-media.ts | Hook de React Query para media |
app/(app)/(host)/gallery.tsx | Pantalla de galería (anfitrion) |
app/(app)/(guest)/gallery-guest.tsx | Pantalla de galería (invitado) |
app/(app)/(guest)/interact.tsx | Audio guestbook del invitado |
nvito-pwa (Progressive Web App)
| Archivo | Responsabilidad |
|---|---|
src/lib/api/services/media.service.ts | Servicio API via BFF proxy |
src/hooks/use-media-recorder.ts | Wrapper de MediaRecorder API |
src/app/(app)/(host)/gallery/page.tsx | Galería del anfitrion |
src/app/(app)/(guest)/gallery-guest/page.tsx | Galería del invitado |