Docs

Flujo de Media y Galería

Upload seguro, procesamiento y distribución de media via CDN

v1.1PublicadoMarzo 2026Equipo de desarrollo, arquitectos, stakeholders

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

ConceptoDetalle
Validez de presigned URL15 minutos
Tamano máximo por archivoDepende del plan (5MB a 2GB)
Formatos imagenJPEG, PNG, WebP, AVIF, GIF
Formatos audioMP3, WAV, OGG, M4A, WebM
Formatos videoMP4, MOV, WebM
Thumbnails generados3 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:

CampoTipoDescripción
eventIdUUIDEvento al que pertenece el media
categoryEnumCategoría del archivo (ver sección 5)
fileNameStringNombre original del archivo
contentTypeStringMIME type declarado por el cliente
fileSizeNumberTamano en bytes

Paso 2: Validación previa

Antes de generar la URL, MediaService valida:

  1. Permisos: El usuario tiene acceso al evento (via OrganizationResolverService)
  2. MIME type: Esta en la whitelist de tipos permitidos para la categoria
  3. Tamano: No excede el limite del plan de la organización
  4. 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-Type debe coincidir con el declarado, Content-Length dentro 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

CampoDescripción
uploadUrlURL pre-firmada para PUT directo a S3
mediaIdUUID generado para el registro Media
keyRuta del archivo en el bucket
expiresAtMomento 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

  1. Verificar existencia: Confirmar que el archivo existe en S3 con el key esperado
  2. Verificar tamano real: Comparar Content-Length del objeto en S3 con el tamano declarado
  3. 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 typeMagic bytes esperados
image/jpegFF D8 FF
image/png89 50 4E 47 0D 0A 1A 0A
image/webp52 49 46 46 ... 57 45 42 50
image/gif47 49 46 38
audio/mpegFF FB o 49 44 33 (ID3)
video/mp400 00 00 ... 66 74 79 70 (ftyp)
  1. Actualizar registro: Marcar el Media como CONFIRMED, almacenar metadata (tamano real, dimensiones si es imagen)
  2. Disparar procesamiento: Encolar job de procesamiento en Bull queue

Registro Media

CampoTipoDescripción
idUUIDIdentificador único
eventIdUUIDEvento asociado
organizationIdUUIDOrganización propietaria
categoryEnumCategoría del media
originalFileNameStringNombre original del archivo
keyStringRuta en S3
bucketStringBucket de almacenamiento
contentTypeStringMIME type verificado
fileSizeNumberTamano en bytes
widthNumber (nullable)Ancho en pixeles (imagenes)
heightNumber (nullable)Alto en pixeles (imagenes)
durationNumber (nullable)Duracion en segundos (audio/video)
thumbnailsJSONURLs de thumbnails generados
statusEnumPENDING, CONFIRMED, PROCESSING, READY, ERROR
uploadedByUUIDUsuario que subio el archivo
createdAtDateTimeMomento de creación

4. Procesamiento de Imagenes

Las imagenes confirmadas pasan por un pipeline de procesamiento usando la libreria sharp.

Pipeline

  1. Metadata extraction: Leer dimensiones (width, height), formato, espacio de color, y datos EXIF
  2. EXIF rotation: Aplicar rotación automática basada en la orientacion EXIF (común en fotos de celular)
  3. Strip EXIF: Eliminar datos EXIF sensibles (ubicación GPS, modelo de dispositivo) por privacidad
  4. Thumbnails: Generar 3 variantes redimensionadas:
VarianteTamano maxUso
thumb150x150px (crop center)Grilla de galería, listas
medium400x400px (fit inside)Previews, cards
large800x800px (fit inside)Lightbox en invitaciones
  1. Formato WebP: Todas las variantes se generan en WebP para optimizar tamano
  2. Upload de variantes: Las 3 variantes se suben al bucket nvito-assets con keys derivados del original
  3. 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íaBucketProcesamientoVisibilidad
GALLERYnvito-uploadsThumbnails + WebPPublica (invitación)
HEROnvito-assetsThumbnails + WebPPublica (invitación)
LOGOnvito-assetsResize 200px + WebPPublica (invitación)
BACKGROUNDnvito-assetsResize 1920px + WebPPublica (invitación)
AUDIOnvito-uploadsSolo validaciónPublica (guestbook)
VIDEOnvito-uploadsSolo validaciónPublica (galería)

6. Limites por Plan

Cada organización tiene un plan que determina los limites de almacenamiento:

PlanLimite por archivoCuota totalArchivos max
FREE5 MB100 MB20
ESSENTIAL50 MB2 GB200
PLUS200 MB10 GB1,000
VIP500 MB50 GB5,000
PREMIUM2 GB200 GBIlimitado

Verificacion de cuota

Antes de generar una presigned URL, el sistema calcula el almacenamiento usado:

  1. Suma fileSize de todos los Media con status != ERROR de la organización
  2. Compara con la cuota del plan
  3. Si la suma + el nuevo archivo excede la cuota, retorna 413 Payload Too Large con detalle de uso

7. Infraestructura de Storage

4 Buckets

BucketPropositoAcceso
nvito-uploadsArchivos subidos por usuarios (galería, audio, video)Privado, acceso via presigned URL
nvito-assetsImagenes procesadas (hero, logo, background, thumbnails)Publico via CDN
nvito-privateArchivos internos (exports, reportes)Privado, solo API
nvito-templatesHTML compilado de invitacionesPublico via CDN

CDN y Storage por ambiente

AmbienteStorageCDN
DesarrolloMinIO (local, puerto 9000)Directo desde MinIO
ProducciónCloudflare R2Cloudflare 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

  1. La invitación HTML incluye una sección gallery con URLs de thumbnails medium (400px)
  2. Al hacer clic en una foto, se abre el lightbox con la variante large (800px)
  3. 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:

  1. Invitado selecciona o toma una foto desde su dispositivo
  2. La app solicita presigned URL y sube directo a S3
  3. Confirma el upload y la foto entra en cola de procesamiento
  4. Al estar lista, aparece automáticamente en la galería del evento
  5. 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

PlataformaTecnologíaFormato nativo
App nativaexpo-av (Audio.Recording)M4A (iOS), OGG (Android)
PWAMediaRecorder APIWebM (Chrome), OGG (Firefox)

Flujo de grabación y moderación

  1. Invitado presiona "Grabar mensaje" en la sección de audio guestbook
  2. La app verifica el límite por invitado (3 audios) via /my-count
  3. La app solicita permiso de micrófono
  4. Graba hasta 60 segundos (límite configurable)
  5. Muestra preview con botón de reproducción
  6. Invitado confirma y el audio se sube via presigned URL flow
  7. El audio queda en estado pendiente de moderación
  8. El anfitrión (HOST) recibe una notificación push y revisa el audio desde la pantalla Memorias
  9. El anfitrión puede aprobar (PATCH .../approve) o rechazar (DELETE ...) el audio
  10. Si se aprueba, el audio es visible en la galería de audios del evento
  11. Si se rechaza, el audio se elimina del storage y del registro
  12. El administrador puede gestionar todos los audios desde el Media Center del admin

Estados de moderación de audio

EstadoDescripción
PENDINGAudio recién subido, pendiente de revisión por el host
APPROVEDAudio aprobado por el host, visible públicamente
REJECTEDAudio rechazado y eliminado

Metadata de audio

CampoDescripción
durationDuración en segundos
guestIdInvitado que grabó el mensaje
guestNameNombre para mostrar en la lista
recordedAtMomento de la grabación
statusEstado 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 contenidoLímite por invitadoEndpoint de verificación
Fotos3GET /mobile/events/:id/gallery/my-count
Audios3GET /mobile/events/:id/audio-guestbook/my-count
Mensajes de felicitación3GET /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 invitado
  • limit: Máximo permitido

Verificación doble

  1. Frontend: Consulta el conteo antes de mostrar el botón de acción. Si count >= limit, el botón se deshabilita con mensaje informativo.
  2. 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ámetroTipoDefaultDescripción
pagenumber1Número de página
limitnumber20Items 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 fotos
  • GET /mobile/events/:id/audio-guestbook — Mensajes de audio
  • GET /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

13. Archivos Clave

nvito-api (Backend)

ArchivoResponsabilidad
media.service.tsOrquestador: get-upload-url, confirm-upload, listado
storage.service.tsAbstracción S3: presigned URLs, head, get, delete
image-processor.service.tsPipeline sharp: resize, thumbnails, WebP, metadata
media.controller.tsEndpoints REST para media
media-processing.processor.tsBull processor para procesamiento asincrono

nvito-admin (Panel de Administración)

ArchivoResponsabilidad
lib/api/services/media.service.tsServicio API para upload desde admin
lib/actions/media-actions.tsServer actions: getUploadUrlAction, confirmUploadAction
components/media/upload-dialog.tsxDialog de upload con drag & drop
components/media/gallery-manager.tsxGestión de galería del evento

nvito-client (App Nativa)

ArchivoResponsabilidad
src/api/services/media.service.tsServicio API para upload movil
src/hooks/queries/use-media.tsHook de React Query para media
app/(app)/(host)/gallery.tsxPantalla de galería (anfitrion)
app/(app)/(guest)/gallery-guest.tsxPantalla de galería (invitado)
app/(app)/(guest)/interact.tsxAudio guestbook del invitado

nvito-pwa (Progressive Web App)

ArchivoResponsabilidad
src/lib/api/services/media.service.tsServicio API via BFF proxy
src/hooks/use-media-recorder.tsWrapper de MediaRecorder API
src/app/(app)/(host)/gallery/page.tsxGalería del anfitrion
src/app/(app)/(guest)/gallery-guest/page.tsxGalería del invitado
Esta pagina fue util?