Room Export Blueprint — Reproducing Sessions from Exports
Purpose
A room export is a self-contained ZIP archive of an entire video identification session. This blueprint explains exactly what’s inside, how to extract useful data from it, and how to reproduce/replay the session for debugging, auditing, or development purposes.
What Is a Room Export?
A room export captures everything about a single videochat identification session:
- The room page (rendered HTML with embedded data)
- The replay page (interactive session replay)
- Flow pages (workflow step views)
- Video/audio recordings (decrypted from encrypted storage)
- Screenshots (face photos, document scans, etc.)
- Presentation attachments (PDFs shown during call)
data_index.json— structured metadata about the sessionscreenshots.json— screenshot metadatareplaydata.js— full activity timeline for replay
Export ZIP Structure
room-export-{roomId}.zip
├── index.html # Room detail page (static HTML)
├── replay.html # Interactive replay page
├── flow-{flowId}.html # Workflow views (one per flow)
├── replaydata.js # Activity timeline (JS variable)
├── data_index.json # Structured session metadata
├── screenshots.json # Screenshot metadata array
│
├── records/{roomId}/ # Media files
│ ├── {filename}.webm # Video recordings (customer + operator)
│ ├── {filename}.mkv # Alternative codec containers
│ └── {filename}.mp4 # H.264 recordings
│
├── raw/ # Raw MJR files (if exportMjr=true)
│ └── {janus recording files}
│
├── presentation/ # Documents shown during session
│ ├── attachment_{id}.pdf # PDF attachments
│ └── attachments.js # Base64-encoded attachment data
│
├── api/
│ ├── screenshot/{roomId}/ # Screenshot images
│ │ └── {attachmentId}
│ └── emrtd-photo/{roomId}/ # eMRTD passport photos
│ └── {photoId}
│
├── css/ # Stylesheets (for offline viewing)
├── js/ # JavaScript (for offline viewing)
├── img/ # Images and icons
├── font/ # Fonts
└── metronic/ # UI framework assets
Key Data Files
data_index.json — The Rosetta Stone
{
"sourceSystemName": "facekom-nusz",
"exportedAt": 1710758400,
"replayData": {
"room": { /* OperatorRoomData */ },
"activityLog": [ /* Activity[] */ ],
"flowLog": [ /* FlowLogEntry[] */ ],
"customerData": [ /* CustomerDataField[] */ ],
"validationLog": [ /* ValidationEntry[] */ ],
"recognitionLog": [ /* RecognitionResult[] */ ],
"roomSettings": { /* RoomSettings */ },
"portalData": { /* PortalData */ },
"configState": { /* ConfigState */ },
"attachments": [ /* Attachment[] */ ]
},
"customer": {
"id": 123,
"name": "...",
"email": "...",
"phone": "...",
"searchFields": { /* ... */ }
}
}replaydata.js — Activity Timeline
This is the same replayData object but wrapped as a JS variable for the offline replay page:
const replaydata = {
room: { /* room metadata */ },
activityLog: [
{
type: "videochat:join",
from: "operator",
createdAt: 1710758400,
content: { /* event-specific data */ }
},
{
type: "videochat:webrtcStream",
from: "customer",
createdAt: 1710758401,
content: {
videoUrl: "records/123/customer_video.webm",
audioUrl: "records/123/customer_audio.webm"
}
},
{
type: "attachment",
createdAt: 1710758500,
content: {
attachmentUrl: "api/screenshot/123/456",
screenshotCategory: "face_photo"
}
},
{
type: "videochat:flow:state",
content: {
flowLog: [ /* task completions, conditions, results */ ]
}
},
{
type: "videochat:customerData:update",
content: {
customerData: { /* portal field values */ }
}
}
// ... many more activity types
]
}screenshots.json — Screenshot Index
[
{
"screenshotUrl": "api/screenshot/123/456",
"category": "face_photo",
"createdAt": "2026-03-15T10:30:00.000Z"
},
{
"screenshotUrl": "api/screenshot/123/789",
"category": "document_front",
"createdAt": "2026-03-15T10:31:00.000Z"
}
]Activity Types You’ll Find
| Activity Type | What It Records |
|---|---|
videochat:join | Operator/customer joined the room |
videochat:leave | Operator/customer left |
videochat:close | Room was closed |
videochat:webrtcStream | Video/audio recording file paths |
videochat:chat:message | Chat messages exchanged |
videochat:flow:state | Complete workflow execution log |
videochat:customerData:update | Customer data changes during session |
videochat:config:state | Feature configuration snapshot |
videochat:presentation:open | Document presented to customer |
videochat:documents:show | Document shown in call |
attachment | Screenshot captured (face, document, etc.) |
selfService:attachment | Self-service screenshot |
videochat:screenshot:remote | Remote screenshot request |
videochat:selfMute | Mute/unmute events |
videochat:remoteUserHoldMedia | Hold media state |
How to Reproduce a Session
Step 1: Extract and Examine
# Unzip the export
unzip room-export-123.zip -d room-123/
cd room-123/
# View the data index
cat data_index.json | jq .
# List all activities by type
cat data_index.json | jq '.replayData.activityLog | group_by(.type) | map({type: .[0].type, count: length})'
# View customer data
cat data_index.json | jq '.customer'
# View flow execution
cat data_index.json | jq '.replayData.flowLog'Step 2: View the Session Offline
# Open the replay page in a browser (works offline!)
open replay.html
# Or open the room detail page
open index.html
# Or open a specific flow page
open flow-456.htmlOffline Viewing
The export is fully self-contained. All CSS, JS, fonts, and images are included. The
replay.htmlpage will play back the session timeline usingreplaydata.js, showing video recordings and activity events in sequence.
Step 3: Extract Screenshots
# List all screenshots
cat screenshots.json | jq '.[].screenshotUrl'
# Screenshots are in api/screenshot/{roomId}/ as raw image files
ls api/screenshot/123/
# View a specific screenshot
open api/screenshot/123/456Step 4: Extract Video Recordings
# Videos are in records/{roomId}/
ls records/123/
# Play a recording
vlc records/123/customer_video.webm
# Extract audio only
ffmpeg -i records/123/customer_video.webm -vn -acodec copy customer_audio.opusOperator Blackout
If the export was created with
blackoutOperator=true, operator video tracks are replaced with audio-only files (video stripped via ffmpeg). Customer video remains intact.
Step 5: Analyze the Workflow
# Extract the flow log
cat data_index.json | jq '.replayData.flowLog'
# Each flow log entry contains:
# - task name, status (completed/skipped/failed)
# - task data (customer inputs, verification results)
# - conditions evaluated
# - timestamps
# Extract all task results
cat data_index.json | jq '.replayData.activityLog[] | select(.type == "videochat:flow:state") | .content.flowLog[] | {task: .taskName, status: .status, result: .result}'Step 6: Extract Face Recognition Data
# Recognition results are in the activity log
cat data_index.json | jq '.replayData.recognitionLog'
# This includes:
# - Face detection results (bounding boxes, confidence)
# - Face comparison scores (document photo vs live)
# - Liveness detection results
# - MRZ/OCR data from documentsStep 7: Extract Customer Verification Data
# Customer data as captured during the session
cat data_index.json | jq '.replayData.customerData'
# Validation results (identity checks)
cat data_index.json | jq '.replayData.validationLog'
# Portal data (structured customer info)
cat data_index.json | jq '.replayData.portalData'How Room Exports Are Created
Export Flow (Code Path)
sequenceDiagram participant Op as Operator participant API as exportroom.js participant BG as backgroundProcess participant Export as ExportRoomPageService participant Crawl as simplecrawler participant DB as Database participant FS as File System Op->>API: GET /api/supervisor/export/:id API->>API: Check room status (must be closed + converted) API->>API: Check view access (ACL) alt features.roomExportFromFileSystem API->>Export: exportRoomFromFileSystem() Export->>Export: createRoomExportZip() else Background export (default) API->>BG: create BackgroundProcess('roomExport') API-->>Op: redirect to /download/{id} BG->>Export: createRoomExportZip() end Export->>Crawl: crawl room page + replay page Crawl->>Crawl: Fetch HTML, CSS, JS, images, fonts Crawl->>Crawl: Fetch /api/replay-data/{roomId} Crawl->>Crawl: Fix paths for offline viewing Export->>DB: Load MediaFile + Encryption records Export->>FS: Decrypt and copy video files Export->>DB: Load Activities (screenshots) Export->>FS: Copy screenshot files Export->>DB: Load Attachments (presentations) Export->>FS: Decrypt and copy PDF attachments Export->>DB: getReplayData(roomId) Export->>FS: Write data_index.json Export->>FS: Write screenshots.json Note over Export: Hook: roomExport:encryption Note over Export: Hook: roomExport:fileName Note over Export: Hook: roomExport:rabRoutesList Export->>Export: zipDirectory() (optional encryption) Export-->>Op: ZIP file download
Key Services
| Service | File | Purpose |
|---|---|---|
ExportRoomPageService | server/service/ExportRoomPageService.js | Main room export logic |
ExportSelfServiceRoomPageService | server/service/ExportSelfServiceRoomPageService.js | Self-service room export |
ExportImportedRoomPageService | server/service/ExportImportedRoomPageService.js | Re-export of imported rooms |
ExportServiceBase | server/service/ExportServiceBase.js | Base class (video, screenshot, ZIP) |
ExportScreenshotsService | server/service/ExportScreenshotsService.js | Screenshot PDF generation |
ExportRawRoomVideoFilesService | server/service/ExportRawRoomVideoFilesService.js | Raw MJR file export |
RoomExportProcess | server/backgroundProcess/roomExport.process.js | Background process handler |
ImportService | server/service/ImportService.js | Import from legacy databases |
Export Configuration
{
"features": {
"export": true,
"exportMjr": false,
"roomExportFromFileSystem": false,
"roomExportMissingAttachmentsFillEmptyFiles": false
},
"roomExport": {
"exportFlows": true
},
"roomExportUrl": "https://oss-lederera.facekomdev.net"
}Customization Hooks
| Hook | Purpose |
|---|---|
roomExport:encryption | Provide ZIP password + encryption method |
roomExport:fileName | Customize export file name |
roomExport:rabRoutesList | Add custom routes to the crawl whitelist |
roomExport:blackoutMediaTypes | Customize which media types to blackout |
Importing Exported Data (Legacy Migration)
The ImportService allows importing room data from legacy FaceKom databases into the current system. This is used when migrating between deployments.
Import Data Model
erDiagram ImportedCustomer ||--o{ ImportedRoom : has ImportedRoom ||--o{ ImportedFlow : has ImportedFlow ||--o{ ImportedFlowTranslation : has
| Model | Purpose |
|---|---|
ImportedCustomer | Customer record from legacy system |
ImportedRoom | Room record with replayData JSON blob |
ImportedFlow | Workflow data from legacy system |
ImportedFlowTranslation | Flow translations from legacy |
Import Configuration
{
"importData": {
"attachmentDirectory": "/workspace/import_data/attachments",
"recordDirectory": "/workspace/import_data/records/converted",
"db": {
"legacyDB1": "postgresql://dev:dev@localhost:5432/legacy_db_1"
},
"encryption": {
"legacyDB1": "customer-room"
},
"excludedColumns": {
"room": { "legacyDB1": ["roomCodecContainer", "data"] },
"flow": { "legacyDB1": ["cleared"] },
"customer": { "legacyDB1": [] }
}
}
}Import Order (Must Be Sequential)
# The ImportService.autoImport() runs in this order:
1. importCustomers() # ImportedCustomer records
2. importRooms() # ImportedRoom records + replayData blob
3. importReplays() # Replay data enrichment
4. importTranslations() # Flow translations
5. importFlows() # Flow definitionsReproduction Recipes
Recipe 1: Debug a Failed Verification
# 1. Get the export
# 2. Find the flow execution log
cat data_index.json | jq '.replayData.activityLog[] | select(.type == "videochat:flow:state") | .content.flowLog[] | select(.status == "failed")'
# 3. Find what verification step failed and why
# 4. Check the recognition results
cat data_index.json | jq '.replayData.recognitionLog'
# 5. View the screenshots that were captured at that point
cat screenshots.json | jq 'sort_by(.createdAt)'
# 6. Open the replay to watch the exact moment
open replay.htmlRecipe 2: Reproduce a Face Comparison Issue
# 1. Extract face photos
mkdir face_photos
cp api/screenshot/123/* face_photos/
# 2. Find the face comparison activity
cat data_index.json | jq '.replayData.activityLog[] | select(.type | contains("face"))'
# 3. Get the document photo and live photo paths
# 4. Send them to vuer_cv for re-comparison:
curl -X POST https://cv-lederera.facekomdev.net/face/compare \
-F "image1=@face_photos/document_face.jpg" \
-F "image2=@face_photos/live_face.jpg"Recipe 3: Reproduce a Document OCR Issue
# 1. Find document screenshots
cat screenshots.json | jq '.[] | select(.category | contains("document"))'
# 2. Get the document image
cp api/screenshot/123/{document_attachment_id} document.jpg
# 3. Re-run OCR on it:
curl -X POST https://cv-lederera.facekomdev.net/document/ocr \
-F "image=@document.jpg"
# 4. Compare with what was captured in the activity log
cat data_index.json | jq '.replayData.activityLog[] | select(.type | contains("ocr"))'Recipe 4: Check What the Customer Saw
# 1. Open the replay page
open replay.html
# 2. The replay shows the session from the operator's perspective
# 3. To understand customer perspective, check:
cat data_index.json | jq '.replayData.activityLog[] | select(.from == "customer")'
# 4. Customer-initiated events include:
# - videochat:chat:message (from customer)
# - videochat:selfMute
# - videochat:close (if customer disconnected)Recipe 5: Reproduce the Entire Session in Dev
# 1. Extract all customer data
cat data_index.json | jq '.customer' > customer.json
cat data_index.json | jq '.replayData.portalData' > portal.json
# 2. Create a test customer with the same data in dev environment
# 3. Set up the same flow configuration
cat data_index.json | jq '.replayData.configState' > config_state.json
# 4. Start a room and manually replicate the flow steps
# 5. Use the screenshots as reference documents for CV testingGotchas and Warnings
Encrypted Exports
Some deployments encrypt the ZIP via the
roomExport:encryptionhook. You’ll need the password to open it. Check the customization branch for the encryption logic.
Missing Attachments
If
features.roomExportMissingAttachmentsFillEmptyFilesis enabled, missing attachment files are replaced with empty files. Check file sizes!
Bug in importedRoom.js setter
The
setfunction forreplayDatareferencesparsed(undeclared variable) instead ofvalon line 99. This is a bug that may cause import issues. See tech-debt.
Crawler Timeout
The export crawler has a 120-second timeout (
setTimeout(reject, 120 * 1000)). Large rooms with many screenshots may timeout. The background process approach (default) is more reliable.
Self-Signed Certs
The crawler sets
ignoreInvalidSSL = truebecause dev environments use self-signed certificates.
Related
- vuer_oss — Backend service that generates exports
- database-schema — Room, Activity, MediaFile, Encryption models
- vuer_cv — CV service for re-running recognition
- authentication — RAB token system used by the crawler
- infrastructure — Dev environment setup
- FaceKom — Platform overview
- tech-debt — Known bugs in export/import system