Reconocimiento Facial — Documentación Técnica
**Fecha:** Noviembre 2026 (actualizado)
Reconocimiento Facial — Documentación Técnica
Sistema "Ver Personas" — Postea.la
Fecha: Noviembre 2026 (actualizado)
Estado: ✅ Completamente en producción
Autor: Equipo Postea.la
Planes: Profesional y Platinum
✅ En producción (Septiembre 2026)
- Pipeline completo: IndexFaces al subir cada foto (DetectFaces eliminado — redundante)
- Overlay de caras en photo-detail con nombres y navegación
PersonPhotosComponent: página por invitado rediseñada — header centrado, tabs con badge dorado, grid 3 columnasPersonsListComponent: lista de personas con tabs Identificadas/Sin identificar, avatares con crop de cara via CSS bboxevent_face_personspara persistirpersonIdpor evento (ya no se usanamecomo identificador)- Saludos en tabla
commentsconcontent_type = 'PERSON' face_search_cacheeliminada (diseño defectuoso — datos obsoletos durante eventos activos)- Conteo de fotos con
COUNT(DISTINCT photo_id)y filtrois_active=1 - Perfil de persona basado en BD — no llama a Rekognition en
GET /persons/{faceId} totalPersonsincluido en el response deGET /persons/{faceId}- Collation
utf8mb4_0900_ai_cienevent_face_persons— fix para evitar error en JOIN conphotos
✅ Cambios Nov 2026
personIdreemplaza anamecomo identificador de persona — toda cara nueva recibe unFacePersonvacío al indexarse, conpersonIdasignado inmediatamente. Ya no se usaname != nullpara determinar si una persona es "conocida".FacePersontabla nueva — almacena perfil de la persona (nombre, cargo, empresa, ciudad, LinkedIn, Instagram). Se crea vacía y se llena cuando el invitado edita su perfil.event_face_persons.person_id— referencia aface_persons.id. Antes tenía camponameinline.- Deduplicación al indexar —
SearchFacescorre dentro deindexFacesInPhoto. Si la cara es similar a una ya registrada en el evento, se reutiliza el mismopersonIden vez de crear uno nuevo. SearchFacesse salta en la primera foto — sitotalFacesIndexed == 0antes de esta foto, no hay similares posibles.DetectFaceseliminado — era redundante.IndexFacesya devuelve bounding boxes enFaceRecord.face().boundingBox().- Todos los logs de pago marcados con
💰 REKOGNITION_PAID— para auditoría y control de costos. deletePhotodesindexa de Rekognition — al borrar una foto, losfaceIds que quedan sin fotos se eliminan de la colección AWS conDeleteFaces, evitando matches fantasma.
1. Arquitectura General
Stack tecnológico
| Componente | Tecnología |
|---|---|
| Detección de rostros | AWS Rekognition DetectFaces |
| Indexación | AWS Rekognition IndexFaces |
| Búsqueda / similitud | AWS Rekognition SearchFacesByImage / SearchFaces |
| Almacenamiento imágenes | AWS S3 us-east-1 (bucket bucketvideosposteala-us) |
| Rekognition region | us-east-1 — mismo bucket para evitar cross-region |
| Base de datos | MariaDB (local Docker) / MariaDB (EC2 producción) |
| Backend | Spring Boot 3 + @Async + CompletableFuture |
Diagrama de flujo — subida de foto
Foto subida por invitado
│
▼
AsyncMediaProcessingService.processOriginalAsync()
│
├── [IA moderación activa] ──→ RekognitionService.analyzeImage()
│ └── [REJECTED] ──→ STOP (no indexar)
│
├── Generar thumbnail + compressed
│
└── [faceRecognitionEnabled=true]
│
▼
FaceRecognitionService.indexFacesInPhoto()
│
├── 💰 IndexFaces → registra faceIds en colección, devuelve bbox
├── Guarda en photo_faces (faceId + bbox)
└── [si totalFacesIndexed > 0 antes de esta foto]
│
└── 💰 SearchFaces por cada cara nueva → similarsByFaceId
│
▼
AsyncMediaProcessingService (por cada cara)
├── ¿Ya existe EventFacePerson para este faceId? → skip
├── ¿Hay similar con personId en BD? → reusar personId
└── No: crear FacePerson vacío + EventFacePerson nuevo
Diagrama de flujo — identificar a una persona
Usuario edita perfil (PUT /personas/{personId})
│
▼
Actualiza face_persons (nombre, cargo, empresa, ciudad, linkedin, instagram)
│
└── Todos los faceIds del mismo personId ya están vinculados
via event_face_persons — no requiere propagación adicional
2. Modelo de Datos
2.1 Cambios en tablas existentes
events
ALTER TABLE events ADD COLUMN face_recognition_enabled BOOLEAN NOT NULL DEFAULT FALSE;
photos
ALTER TABLE photos
ADD COLUMN faces_indexed BOOLEAN NOT NULL DEFAULT FALSE,
ADD COLUMN faces_count INT NOT NULL DEFAULT 0;
comments
El campo content_type es VARCHAR(255). Se agregó el valor 'PERSON' para saludos a invitados.
No requiere migración — es varchar, no enum.
2.2 Nuevas tablas
event_face_collections
Una colección Rekognition por evento. Se crea al activar el feature.
CREATE TABLE event_face_collections (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
event_id BIGINT NOT NULL UNIQUE,
rekognition_collection_id VARCHAR(100) NOT NULL, -- "postea-{eventSlug}"
is_active BOOLEAN NOT NULL DEFAULT TRUE,
total_faces_indexed INT NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL,
deleted_at TIMESTAMP NULL
);
photo_faces
Cada rostro detectado en cada foto. Almacena bounding box para overlay en frontend.
CREATE TABLE photo_faces (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
photo_id BIGINT NOT NULL,
event_id BIGINT NOT NULL,
rekognition_face_id VARCHAR(100) NOT NULL,
confidence FLOAT NOT NULL,
bbox_left FLOAT NOT NULL, -- valores 0.0–1.0
bbox_top FLOAT NOT NULL,
bbox_width FLOAT NOT NULL,
bbox_height FLOAT NOT NULL,
created_at TIMESTAMP NOT NULL,
INDEX idx_photo_faces_photo_id (photo_id),
INDEX idx_photo_faces_event_id (event_id),
INDEX idx_photo_faces_face_id (rekognition_face_id)
);
event_face_persons
Mapea rekognition_face_id → person_id por evento. Cada cara indexada tiene exactamente un registro. Múltiples caras de la misma persona apuntan al mismo person_id.
CREATE TABLE event_face_persons (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
event_id BIGINT NOT NULL,
rekognition_face_id VARCHAR(100) NOT NULL,
person_id BIGINT NULL, -- FK a face_persons.id
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_event_face (event_id, rekognition_face_id),
INDEX idx_efp_event_id (event_id),
INDEX idx_efp_face_id (rekognition_face_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
face_persons (nueva — Nov 2026)
Perfil de una persona en un evento. Se crea vacío al indexar la primera cara y se llena cuando el invitado edita su perfil.
CREATE TABLE face_persons (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
event_id BIGINT NOT NULL,
name VARCHAR(100) NULL,
job_title VARCHAR(100) NULL,
company VARCHAR(100) NULL,
city VARCHAR(100) NULL,
linkedin_url VARCHAR(255) NULL,
instagram_url VARCHAR(255) NULL,
profile_photo_url VARCHAR(500) NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_fp_event_id (event_id)
);
Modelo de identidad (Nov 2026):
- 1 persona → 1
FacePersonconpersonId - 1 persona en N fotos → N
faceIds distintos de Rekognition - N
EventFacePersonapuntan al mismopersonId - Para identificar si una cara es "conocida": verificar si hay
EventFacePersonconpersonIdque tenga similar en el evento — NO usarname != null
3. Entidades JPA
EventFaceCollection
@Entity @Table(name = "event_face_collections")
public class EventFaceCollection {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
@Column(name = "event_id", nullable = false, unique = true) private Long eventId;
@Column(name = "rekognition_collection_id") private String rekognitionCollectionId;
@Column(name = "is_active") private Boolean isActive = true;
@Column(name = "total_faces_indexed") private Integer totalFacesIndexed = 0;
@Column(name = "created_at") private Instant createdAt;
@Column(name = "deleted_at") private Instant deletedAt;
}
PhotoFace
@Entity @Table(name = "photo_faces")
public class PhotoFace {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
@Column(name = "photo_id") private Long photoId;
@Column(name = "event_id") private Long eventId;
@Column(name = "rekognition_face_id") private String rekognitionFaceId;
@Column(name = "confidence") private Float confidence;
@Column(name = "bbox_left") private Float bboxLeft;
@Column(name = "bbox_top") private Float bboxTop;
@Column(name = "bbox_width") private Float bboxWidth;
@Column(name = "bbox_height") private Float bboxHeight;
@Column(name = "created_at") private Instant createdAt;
}
EventFacePerson
@Entity @Table(name = "event_face_persons")
public class EventFacePerson {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
@Column(name = "event_id") private Long eventId;
@Column(name = "rekognition_face_id") private String rekognitionFaceId;
@Column(name = "name") private String name;
@Column(name = "named_at") private Instant namedAt;
@Column(name = "named_by") private String namedBy;
@Column(name = "profile_photo_id") private Long profilePhotoId;
@Column(name = "created_at") private Instant createdAt;
@PrePersist void prePersist() { if (createdAt == null) createdAt = Instant.now(); }
}
4. Servicios
4.1 FaceRecognitionService
Métodos públicos
// Crear colección al activar el feature en un evento
EventFaceCollection createCollectionForEvent(String eventSlug, Long eventId);
// Indexar rostros de una foto recién subida
FaceIndexResult indexFacesInPhoto(Long photoId, String s3Key, Long eventId);
// Buscar fotos por imagen subida (selfie)
List<Long> searchByUpload(byte[] imageBytes, String eventSlug, Long eventId);
// Buscar fotos por faceId seleccionado — devuelve photoIds de esa persona
List<Long> searchByFaceId(String faceId, String eventSlug, Long eventId);
// Devuelve solo los faceIds similares (para propagación de nombres)
List<String> searchSimilarFaceIds(String faceId, String eventSlug, Long eventId);
// Bounding boxes de una foto (para overlay frontend)
List<PhotoFaceDTO> getFacesInPhoto(Long photoId);
// Borrar colección al vencer el evento
void deleteCollectionForEvent(String eventSlug);
FaceIndexResult
public record FaceIndexResult(Long photoId, int facesIndexed, List<PhotoFace> faces) {}
Parámetros Rekognition
| Parámetro | Valor | Motivo |
|---|---|---|
MIN_FACE_CONFIDENCE |
80% | Balance precisión/recall |
FACE_MATCH_THRESHOLD |
80% | Agrupa misma persona en distintas fotos |
MAX_FACES_PER_PHOTO |
10 | Límite de caras por foto |
MAX_SEARCH_FACES |
10 | Resultados de búsqueda |
QualityFilter (IndexFaces) |
AUTO |
Rekognition descarta caras borrosas |
4.2 AsyncMediaProcessingService — hook de indexación
if (Boolean.TRUE.equals(event.getFaceRecognitionEnabled())) {
FaceIndexResult faceResult = faceRecognitionService.indexFacesInPhoto(photoId, s3Key, event.getId());
photo.setFacesIndexed(true);
photo.setFacesCount(faceResult.facesIndexed());
photoRepository.save(photo);
// Auto-nombrar: solo si ya hay personas nombradas (evita llamadas innecesarias a Rekognition)
long namedCount = eventFacePersonRepository.countByEventIdAndNameIsNotNull(event.getId());
if (namedCount > 0) {
for (PhotoFace pf : faceResult.faces()) {
List<String> similarFaceIds = faceRecognitionService.searchSimilarFaceIds(
pf.getRekognitionFaceId(), eventSlug, event.getId());
for (String similarFaceId : similarFaceIds) {
// Si el faceId similar tiene nombre, copiarlo al nuevo faceId
eventFacePersonRepository
.findByEventIdAndRekognitionFaceId(event.getId(), similarFaceId)
.filter(e -> e.getName() != null)
.ifPresent(named -> {
// Guardar solo si el nuevo faceId aún no tiene registro
eventFacePersonRepository.findByEventIdAndRekognitionFaceId(
event.getId(), pf.getRekognitionFaceId())
.ifPresentOrElse(
existing -> { /* ya existe, no sobreescribir */ },
() -> eventFacePersonRepository.save(EventFacePerson.builder()
.eventId(event.getId())
.rekognitionFaceId(pf.getRekognitionFaceId())
.name(named.getName())
.namedBy(named.getNamedBy())
.namedAt(Instant.now())
.build())
);
});
}
}
}
}
s3Key se extrae con s3Service.extractKeyFromUrl(originalUrl) — no hardcodeado.
5. Endpoints REST
Base path: /api/events/{slug}/faces
POST /search/select
Body: { faceId: String, searcherName?: String }
→ Busca fotos por faceId
→ Response: FaceSearchResultDTO
GET /photos/{photoId}
→ Bounding boxes + nombre (si identificado) de cada cara en la foto
→ Response: List<PhotoFaceResponseDTO>
GET /persons
→ Lista de todas las personas del evento agrupadas por nombre
→ Response: { identified: PersonSummaryDTO[], unidentified: PersonSummaryDTO[], totalPersons: int }
→ Cada PersonSummaryDTO incluye: faceId, name, profilePhotoUrl, photoCount, bboxLeft/Top/Width/Height
GET /persons/{faceId}
→ Perfil del invitado: nombre, fotos donde aparece (vía BD, no Rekognition)
→ Agrupa todos los faceIds con el mismo nombre para obtener todas las fotos
→ Incluye totalPersons (COUNT DISTINCT faceIds del evento)
→ Response: PersonProfileDTO
PUT /persons/{faceId}/name
Body: { name: String, namedBy?: String }
→ Guarda nombre y propaga a faceIds similares vía SearchFaces
→ Response: { faceId, name, namedBy, alreadyNamed }
GET /persons/{faceId}/comments
→ Saludos del invitado (content_type='PERSON')
→ Response: List<{ id, message, authorName, createdAt }>
POST /persons/{faceId}/comments
Body: { message: String, authorName?: String }
→ Agrega saludo. Auto-crea EventFacePerson si no existe.
→ Response: { id, message, authorName, createdAt }
POST /collection
→ Crea la colección Rekognition del evento
→ Response: { collectionId, eventId, active }
DELETE /collection
→ Borra la colección Rekognition del evento
→ Response: 204 No Content
DTOs principales
// Perfil de invitado
public class PersonProfileDTO {
private Long personId;
private String faceId;
private String name;
private String namedBy;
private Instant namedAt;
private String profilePhotoUrl;
private List<PhotoMiniDTO> photos;
private Integer totalPersons; // COUNT DISTINCT faceIds del evento
public record PhotoMiniDTO(Long photoId, String thumbnailUrl, String compressedUrl) {}
}
// Resumen de persona en lista
public class PersonSummaryDTO {
private String faceId;
private String name;
private String profilePhotoUrl;
private int photoCount;
private Float bboxLeft, bboxTop, bboxWidth, bboxHeight; // para crop de cara en avatar
}
// Cara en foto (para overlay)
public class PhotoFaceResponseDTO {
private String faceId;
private Float confidence;
private Float bboxLeft, bboxTop, bboxWidth, bboxHeight;
private String personName; // null si no identificado
}
6. Frontend
Rutas
/event/:eventId/personas → PersonsListComponent
/event/:eventId/persona/:faceId → PersonPhotosComponent
PersonsListComponent
- Tabs: Identificadas / Sin identificar con contadores
- Avatar circular con crop de cara CSS usando
bboxLeft/Top/Width/Height:faceStyle(p: PersonSummary): object { const pad = 0.30; const scale = 1 / (p.bboxWidth + pad * 2); const offsetX = -(p.bboxLeft - pad) * scale * 100; const offsetY = -(p.bboxTop - pad) * scale * 100; return { position: 'absolute', width: `${scale * 100}%`, top: `${offsetY}%`, left: `${offsetX}%` }; } - Navega a
PersonPhotosComponentpasandototalPersonsviarouter state - CTA "¿Falta alguien?" al fondo → cambia
activeTab = 'unidentified'
PersonPhotosComponent
- Header centrado: avatar 88px, nombre en Playfair Display, botón ✏️, pill dorado "Ver todas las personas [N]"
totalPersonsse lee deprofile.totalPersons(viene del backend), fallback ahistory.state- Panel edición nombre: separado del header, con input full-width + botones Guardar/Cancelar
- Tabs:
Fotos (N)/Saludos (N)— activo con underline dorado#d4a574y badge dorado - Grid fotos: 3 columnas cuadradas
aspect-ratio: 1, gap 2px, paginación de 10 en 10 - Overlay "+N" en última celda del grid si hay más fotos sin cargar
- Saludos: input integrado como pill con botón enviar circular dorado
- Fondo header:
#faf7f4(mismo que la página de personas) - "Invitado desconocido" en letra normal (sin cursiva)
FaceRecognitionService (frontend)
getPersonsListForEvent(eventSlug): Observable<PersonsListResponse>
getPersonProfile(eventSlug, faceId): Observable<PersonProfile> // incluye totalPersons
namePerson(eventSlug, faceId, name, namedBy?): Observable<{name, namedBy, alreadyNamed}>
getPersonComments(eventSlug, faceId): Observable<PersonComment[]>
addPersonComment(eventSlug, faceId, message, authorName): Observable<PersonComment>
getFacesInPhoto(eventSlug, photoId): Observable<PhotoFace[]>
Interfaces clave:
export interface PersonProfile {
personId: number | null;
faceId: string;
name: string | null;
namedBy: string | null;
namedAt: string | null;
profilePhotoUrl: string | null;
photos: { photoId: number; thumbnailUrl: string; compressedUrl: string }[];
totalPersons: number | null;
}
export interface PersonSummary {
faceId: string;
name: string | null;
profilePhotoUrl: string | null;
photoCount: number;
bboxLeft: number | null;
bboxTop: number | null;
bboxWidth: number | null;
bboxHeight: number | null;
}
7. Repositories
// EventFacePersonRepository
Optional<EventFacePerson> findByEventIdAndRekognitionFaceId(Long eventId, String faceId);
List<EventFacePerson> findByEventId(Long eventId);
long countByEventIdAndNameIsNotNull(Long eventId);
// PhotoFaceRepository
List<PhotoFace> findByPhotoId(Long photoId);
List<Long> findPhotoIdsByFaceIdAndEventId(String faceId, Long eventId);
List<Long> findPhotoIdsByFaceIdsAndEventId(List<String> faceIds, Long eventId);
List<PhotoFace> findByPhotoIdInAndEventId(List<Long> photoIds, Long eventId);
int countDistinctFaceIdsByEventId(Long eventId); // para totalPersons en perfil
// Queries nativas (performance crítica)
// Conteo de fotos por faceId (para lista de personas)
@Query("SELECT pf.rekognitionFaceId, COUNT(DISTINCT pf.photoId) FROM PhotoFace ...")
List<Object[]> countPhotosByFaceIdForEvent(Long eventId);
// Conteo de fotos ÚNICO por nombre (evita doble conteo multi-faceId)
@Query(nativeQuery=true, """
SELECT efp.name, COUNT(DISTINCT pf.photo_id)
FROM event_face_persons efp
JOIN photo_faces pf ON pf.rekognition_face_id = efp.rekognition_face_id AND pf.event_id = efp.event_id
JOIN photos p ON p.id = pf.photo_id AND p.is_active = 1
WHERE efp.event_id = :eventId AND efp.name IS NOT NULL
GROUP BY efp.name
""")
List<Object[]> countDistinctPhotosByNameForEvent(Long eventId);
// Bounding box de primera foto por faceId (para avatares con crop)
@Query("SELECT pf.rekognitionFaceId, pf.bboxLeft, pf.bboxTop, pf.bboxWidth, pf.bboxHeight ...")
List<Object[]> findBboxByFaceIdsAndEventId(List<String> faceIds, Long eventId);
8. Configuración AWS
Naming convention de colecciones
postea-{eventSlug}
Ejemplo: postea-boda-cesar-2026
Bucket S3
bucketvideosposteala-us (us-east-1)
BucketOwnerPreferred — ACLs habilitados
9. Costos AWS — Estimación por evento
Escenario: 60 fotos, promedio 3 rostros/foto, 20 búsquedas
| API | Llamadas | Precio unitario | Total (USD) |
|---|---|---|---|
DetectFaces |
eliminado | ||
IndexFaces |
60 | $0.001 | $0.060 |
SearchFaces (dedup al indexar) |
~120* | $0.001 | $0.120 |
SearchFacesByImage (find-me) |
20 | $0.001 | $0.020 |
SearchFaces (by-face desde visor) |
~30 | $0.001 | $0.030 |
| S3 transferencia | ~180MB | $0.09/GB | $0.016 |
| Total | ~$0.25 |
*SearchFaces al indexar: 1 por cara nueva, solo si ya hay caras previas en la colección. Primera foto = 0 SearchFaces. Con 3 caras/foto: foto 1 → 0, foto 2 → 3, foto 3 → 3... ≈ 2 por foto promedio.
Con Profesional (S/79 ≈ $21 USD): costo AWS = ~1.2% del ingreso por evento (antes 1.4% con DetectFaces).
Ver detalles: documentacion-funcional/rekognition_costos.md
10. Estado de implementación
✅ Completado en producción (Nov 2026)
BD
├── ✅ events.face_recognition_enabled
├── ✅ photos.faces_indexed, photos.faces_count
├── ✅ event_face_collections
├── ✅ photo_faces
├── ✅ event_face_persons (person_id en vez de name — Nov 2026)
├── ✅ face_persons (perfil editable — Nov 2026)
└── ❌ face_search_cache ← eliminada (diseño defectuoso)
Backend
├── ✅ FaceRecognitionService:
│ ├── indexFacesInPhoto (sin DetectFaces — Nov 2026)
│ ├── SearchFaces dentro de indexFacesInPhoto (Nov 2026)
│ ├── deleteFacesFromCollection al borrar foto (Nov 2026)
│ ├── searchByFaceId, searchByUpload, findMeByUpload
│ └── todos los logs 💰 REKOGNITION_PAID (Nov 2026)
├── ✅ AsyncMediaProcessingService:
│ ├── indexación al subir foto
│ └── dedup por personId (sin llamadas extra a Rekognition — Nov 2026)
├── ✅ AdminPhotoService: deletePhoto limpia Rekognition (Nov 2026)
└── ✅ FaceRecognitionController:
├── GET /persons — lista con personId
├── GET /persons/{personId} — perfil
├── PUT /persons/{personId} — editar perfil (nombre, cargo, empresa, ciudad, linkedin, instagram)
├── GET /persons/by-face/{faceId} — resolver faceId → personId
├── GET|POST /persons/{personId}/comments
└── POST|DELETE /collection
Frontend
├── ✅ FaceRecognitionService (face-recognition.service.ts)
├── ✅ photo-detail: overlay de caras, navegación a persona por personId
├── ✅ PersonsListComponent: tabs Identificadas/Sin identificar, avatares con crop
├── ✅ PersonPhotosComponent: header, tabs fotos/saludos, grid 3 col
└── ✅ ProPersonProfileEditComponent: form compacto con prefijos inline (Nov 2026)
├── Solo handle en LinkedIn (linkedin.com/in/tu-perfil)
└── Solo @usuario en Instagram
⚠️ Optimizaciones pendientes
- SearchFaces en by-face: si personId ya está en BD, devolver fotos sin ir a Rekognition
(ahorra 1 llamada por cada click en cara desde el visor)
- Umbral mínimo de caras antes de hacer SearchFaces al indexar
(si evento < 5 fotos, prob. de match muy baja)
🚧 Deuda técnica
- Tests unitarios e integración para el sistema de faces
- searchSimilarFaceIds() aún hace llamada extra a Rekognition —
solo se usa desde contextos donde ya tenemos los similares del índex
11. Consideraciones de seguridad
- Los endpoints de personas son públicos — cualquier persona con el slug del evento puede acceder. Intencional: los invitados no tienen cuenta.
- Los
faceIdde Rekognition son identificadores opacos — no contienen datos biométricos exportables. - Al vencer el evento,
deleteCollectionForEventborra la colección en AWS y los registros en BD. - Todos los filtros de BD van por
event_id— no hay cruce de datos entre eventos. - Cada evento tiene su propia colección Rekognition aislada (
postea-{slug}).