<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Krish Anand</title>
    <description>The latest articles on DEV Community by Krish Anand (@officialkrish).</description>
    <link>https://dev.to/officialkrish</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4038162%2F6d20907c-584a-4336-bf31-283cd0311e63.jpg</url>
      <title>DEV Community: Krish Anand</title>
      <link>https://dev.to/officialkrish</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/officialkrish"/>
    <language>en</language>
    <item>
      <title>Weave: Building a Production Video Conferencing Platform with E2E Encrypted Local Recording</title>
      <dc:creator>Krish Anand</dc:creator>
      <pubDate>Wed, 22 Jul 2026 11:59:35 +0000</pubDate>
      <link>https://dev.to/officialkrish/weave-building-a-production-video-conferencing-platform-with-e2e-encrypted-local-recording-30ii</link>
      <guid>https://dev.to/officialkrish/weave-building-a-production-video-conferencing-platform-with-e2e-encrypted-local-recording-30ii</guid>
      <description>&lt;h2&gt;
  
  
  &lt;strong&gt;A comprehensive system design walkthrough — HLD + LLD&lt;/strong&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Weave is an open-source video conferencing platform that solves a fundamental problem: &lt;strong&gt;internet quality should never compromise recording quality&lt;/strong&gt;. Unlike Zoom or Google Meet, which record server-side (and lose quality on bad connections), Weave records &lt;strong&gt;locally on each participant's device&lt;/strong&gt; in encrypted 60-second chunks, then intelligently merges and processes them in the cloud.&lt;/p&gt;

&lt;p&gt;The platform includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real-time video conferencing (powered by Jitsi Meet)&lt;/li&gt;
&lt;li&gt;Local chunk-based recording with AES-256-GCM encryption&lt;/li&gt;
&lt;li&gt;Per-meeting Content Encryption Keys wrapped with RSA-OAEP-256&lt;/li&gt;
&lt;li&gt;An in-browser video editor with timeline, transitions, overlays, and effects&lt;/li&gt;
&lt;li&gt;Multi-camera (multicam) editing with speaker detection and auto-cut&lt;/li&gt;
&lt;li&gt;HLS adaptive streaming with 4 quality profiles&lt;/li&gt;
&lt;li&gt;Scheduling with Google Calendar, Slack, Discord integrations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Tech Stack:&lt;/strong&gt; React 19, Express 5, PostgreSQL, Redis, FFmpeg, Bun, Kubernetes, ArgoCD&lt;/p&gt;




&lt;h2&gt;
  
  
  High-Level Design (HLD)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  System Architecture
&lt;/h3&gt;

&lt;p&gt;Weave follows a &lt;strong&gt;microservices architecture&lt;/strong&gt; within a monorepo (Turborepo). Six services communicate via HTTP REST, WebSocket, and Redis-backed asynchronous job queues.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────────────────────────────────────────────────┐
│                          CLIENT (React + Vite)                          │
│  Dashboard │ Live Meeting (Jitsi) │ Video Editor │ Recording Viewer      │
└────────────┬─────────────────────────────────────────────────────────────┘
             │ HTTPS                               │ WebSocket
             ▼                                     ▼
┌────────────────────────────┐    ┌──────────────────────────────┐
│    BACKEND (Express 5)     │    │   WS-RELAYER (Bun WebSocket) │
│  Port 3000                 │    │   Port 9093                  │
│  REST API ─ Auth           │    │  ─ Chat messaging            │
│  ─ Meetings ─ Recording    │    │  ─ Participant presence      │
│  ─ Editor ─ Notifications  │    │  ─ Recording state events    │
│  ─ GitHub ─ Google OAuth   │    │  ─ Typing indicators         │
└──────┬─────────────────────┘    └──────────┬───────────────────┘
       │                                     │
       ▼                                     ▼
┌──────────────────────────────────────────────────────────────────────┐
│                           INFRASTRUCTURE                             │
│  ┌──────────┐  ┌────────────────────┐  ┌──────────────────────────┐  │
│  │PostgreSQL│  │ Redis              │  │ S3-Compatible Storage    │  │
│  │(NeonDB)  │  │ ─ Job Queues (5)   │  │ (AWS S3 / GCS)          │  │
│  │Prisma    │  │ ─ Rate Limiting     │  │ ─ Encrypted chunks      │  │
│  │14 Models │  │ ─ Chat History     │  │ ─ Final recordings       │  │
│  │15 Enums  │  │ ─ CEK Storage     │  │ ─ HLS streams            │  │
│  └──────────┘  └────────────────────┘  │ ─ Editor assets          │  │
│                                         └──────────────────────────┘  │
└──────────────────────────────────────────────────────────────────────┘
       │
       ┌──────────────────┬──────────────────────┐
       ▼                  ▼                      ▼
┌──────────────┐ ┌───────────────┐ ┌──────────────────────┐
│Merger-Worker │ │Editor-Worker  │ │   Transcoder          │
│(Bun + FFmpeg)│ │(Bun + FFmpeg) │ │(Bun + FFmpeg)         │
│              │ │              │ │                      │
│Decrypts &amp;amp;    │ │Renders video │ │Converts MP4 → HLS    │
│merges chunks │ │editor exports│ │360p/480p/720p/1080p  │
│→ per-user    │ │with effects, │ │+ poster + sprites    │
│videos → grid │ │transitions,  │ │+ VTT thumbnails     │
│video         │ │overlays      │ │                      │
└──────────────┘ └───────────────┘ └──────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Redis Queue Architecture
&lt;/h3&gt;

&lt;p&gt;Five job queues power asynchronous processing. All workers use &lt;strong&gt;Redis Lists&lt;/strong&gt; with &lt;code&gt;BLPOP&lt;/code&gt; for blocking dequeue and &lt;code&gt;RPUSH&lt;/code&gt; for enqueue.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Queue&lt;/th&gt;
&lt;th&gt;Producer&lt;/th&gt;
&lt;th&gt;Consumer&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ProcessVideo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Backend (on meeting end)&lt;/td&gt;
&lt;td&gt;Merger-Worker&lt;/td&gt;
&lt;td&gt;Decrypt &amp;amp; merge chunks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TranscodeVideo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Merger-Worker, Editor-Worker&lt;/td&gt;
&lt;td&gt;Transcoder&lt;/td&gt;
&lt;td&gt;HLS conversion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;EditorRender&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Backend (on export)&lt;/td&gt;
&lt;td&gt;Editor-Worker&lt;/td&gt;
&lt;td&gt;Render video exports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Notifications&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All services&lt;/td&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Deliver user notifications&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MeetingInvitations&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Send meeting invites&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Deployment Architecture
&lt;/h3&gt;

&lt;p&gt;Weave runs on &lt;strong&gt;Kubernetes&lt;/strong&gt; managed via &lt;strong&gt;ArgoCD&lt;/strong&gt; (GitOps). All services are deployed in the &lt;code&gt;weave&lt;/code&gt; namespace.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                         Internet
                            │
                    ┌───────┴────────┐
                    │  NGINX Ingress │ (cert-manager TLS)
                    └───────┬────────┘
                            │
         ┌──────────────────┼──────────────────┐
         ▼                  ▼                  ▼
   weave.krishlabs.tech  api.weave...     wss.weave...
   ┌──────────────┐  ┌──────────────┐  ┌──────────────┐
   │  Frontend    │  │  Backend     │  │  WS-Relayer  │
   │  (React SPA) │  │  Express 5   │  │  Bun WS      │
   │  Pod: 1      │  │  Pod: 1      │  │  Pod: 1      │
   │  CPU: 300m   │  │  CPU: 500m   │  │  CPU: 300m   │
   │  RAM: 256Mi  │  │  RAM: 512Mi  │  │  RAM: 256Mi  │
   └──────────────┘  └──────────────┘  └──────────────┘

   ┌──────────────┐  ┌──────────────┐  ┌──────────────┐
   │Merger-Worker │  │Editor-Worker │  │  Transcoder  │
   │  Pod: 1      │  │  Pod: 1      │  │  Pod: 1      │
   │  CPU: 512m   │  │  CPU: 512m   │  │  CPU: 512m   │
   │  RAM: 1Gi    │  │  RAM: 1Gi    │  │  RAM: 1Gi    │
   │  Vol: 20Gi   │  │  Vol: 20Gi   │  │  Vol: 20Gi   │
   └──────────────┘  └──────────────┘  └──────────────┘

   ┌──────────────────┐
   │  Redis (in-cluster) │
   │  Pod: 1             │
   │  CPU: 100m, RAM: 128Mi │
   └──────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;External Dependencies:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NeonDB&lt;/strong&gt; (PostgreSQL) — managed database on AWS via Neon&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3-compatible storage&lt;/strong&gt; — production bucket on krishlabs.tech&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let's Encrypt&lt;/strong&gt; — TLS certs via cert-manager&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Core Data Flow: The Recording Pipeline
&lt;/h2&gt;

&lt;p&gt;This is the heart of Weave's innovation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. MEETING START
   Client generates 256-bit Content Encryption Key (CEK)
   Fetches server's RSA-4096 public key
   Wraps CEK with RSA-OAEP-256
   Stores wrapped CEK → POST /keys/meeting/:id/wrapped-cek
   Redis stores it (7-day TTL)

2. EVERY 60 SECONDS (during meeting)
   Capture MediaStream → MediaRecorder → WebM chunk
   Generate 96-bit IV (deterministic: SHA-256 of userId:seqNum)
   Encrypt: AES-256-GCM(chunk, CEK, IV)
   Upload: POST /upload-chunk
     └─ Backend stores to S3 + upserts MediaChunk record

3. MEETING ENDS
   Backend finalizes → RPUSH "ProcessVideo" { meetingId }

4. MERGER WORKER (BLPOP "ProcessVideo")
   Download all chunks from S3 (organized by participant)
   Decrypt each chunk:
     ├─ Fetch wrapped CEK from Redis
     ├─ Unwrap with server RSA private key
     └─ AES-GCM-decrypt(ciphertext, CEK, IV, authTag)
   Per user: concatenate chunks → single MP4
   Upload per-user video → S3 (participants/{userId}/merged.mp4)
   Speaker analysis via FFmpeg silencedetect
   Composite grid layout → final grid video
   Upload final → S3 (final/meeting_grid_recording.mp4)
   RPUSH "TranscodeVideo" { meetingId, finalPath }

5. TRANSCODER (BLPOP "TranscodeVideo")
   Download final MP4
   Single-pass multi-profile encode:
     ├─ 360p (640x360, 800kbps)
     ├─ 480p (854x480, 1000kbps)
     ├─ 720p (1280x720, 2.8Mbps)
     └─ 1080p (1920x1080, 5Mbps)
   Generate master.m3u8 playlist
   Generate poster.jpg + sprites.jpg + thumbnails.vtt
   Upload HLS artifacts → S3
   Report READY → Backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Low-Level Design (LLD)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Encryption Protocol
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────────────────────────────────────────┐
│                     Encryption Architecture                      │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  CEK (256-bit random) ──── RSA-OAEP-256 ────► Server Public Key │
│       │                                                          │
│       ▼                                                          │
│  Chunk ──► AES-256-GCM (CEK, IV) ──► Ciphertext + Auth Tag      │
│       │                                  │                       │
│       │                                  ▼                       │
│       │                           Upload to S3 + Metadata        │
│       │                                                          │
│  IV = first 12 bytes of SHA-256(userId:sequenceNumber)           │
│  Auth Tag = 128 bits (appended to ciphertext)                    │
│                                                                  │
│  Stored per chunk (MediaChunk table):                            │
│    - isEncrypted: true                                           │
│    - encryptionAlgorithm: "AES-GCM"                             │
│    - encryptionIv: base64(iv)                                    │
│    - encryptionTagBits: 128                                     │
│                                                                  │
│  Wrapped CEK stored in Redis:                                    │
│    Key: meeting:wrapped-cek:{meetingId}:{participantId}          │
│    TTL: 7 days                                                   │
└──────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Database Schema (Core Models)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌───────────────┐     ┌──────────────────┐     ┌──────────────────┐
│     User      │     │     Meeting      │     │  MediaChunk      │
├───────────────┤     ├──────────────────┤     ├──────────────────┤
│ id (PK)       │────&amp;gt;│ id (PK)          │────&amp;gt;│ id (PK)          │
│ email (UQ)    │     │ roomId (UQ)      │     │ meetingId (FK)   │
│ password      │     │ userId (FK)      │     │ uploaderUserId   │
│ googleId (UQ) │     │ passcode         │     │ sequenceNumber   │
│ githubToken   │     │ recordingState   │     │ bucketLink       │
│ tier          │     │ isEnded          │     │ isEncrypted      │
│ isVerified    │     │ startedAt        │     │ encryptionAlgorithm
│              │     │ endedAt          │     │ encryptionIv     │
└───────────────┘     └──────────────────┘     └──────────────────┘
       │                       │
       │                       │
       ▼                       ▼
┌───────────────┐     ┌──────────────────┐     ┌──────────────────┐
│ Notification  │     │FinalRecording    │     │ParticipantSource │
├───────────────┤     ├──────────────────┤     ├──────────────────┤
│ id (PK)       │     │ id (PK)          │     │ id (PK)          │
│ userId (FK)   │     │ meetingId (FK,UQ)│     │ meetingId (FK)   │
│ type (Enum)   │     │ videoLink        │     │ participantId    │
│ message       │     │ version          │     │ videoUrl         │
│ metadata (JSON)│     │ visibleToEmails[]│     │ durationMs       │
│ isRead        │     │ audioLink        │     │ fileSizeBytes    │
└───────────────┘     └──────────────────┘     └──────────────────┘

┌───────────────────────────────── Editor Domain ─────────────────────────────────┐
│                                                                                  │
│  ┌────────────────┐    ┌──────────────┐    ┌────────────────┐    ┌─────────────┐│
│  │ EditorProject  │───&amp;gt;│ EditorTrack  │───&amp;gt;│  EditorClip    │    │EditorAsset  ││
│  ├────────────────┤    ├──────────────┤    ├────────────────┤    ├─────────────┤│
│  │ id (PK)       │    │ id (PK)      │    │ id (PK)        │    │ id (PK)     ││
│  │ ownerId (FK)  │    │ projectId(FK)│    │ trackId (FK)   │    │ projectId   ││
│  │ meetingId (FK)│    │ type (Enum)  │    │ sourceAssetId  │    │ participantKey│
│  │ sourceMode    │    │ order (UQ)   │    │ timelineStartMs│    │ url          │
│  │ status        │    │ visible      │    │ durationMs     │    │ type (Enum)  │
│  │ durationMs    │    │ muted        │    │ sourceStartMs  │    │ waveformUrl  │
│  │ fps/width/hgt │    │ participantKey│   │ metadata(JSON) │    └─────────────┘
│  └────────────────┘    └──────────────┘    └────────────────┘                   │
│         │                                                                       │
│         ▼                                                                       │
│  ┌────────────────┐    ┌──────────────┐    ┌────────────────┐                   │
│  │ EditorOverlay  │    │ ExportJob    │    │  MulticamLayout│                   │
│  ├────────────────┤    ├──────────────┤    ├────────────────┤                   │
│  │ projectId (FK) │    │ projectId    │    │ projectId (FK) │                   │
│  │ type (Enum)    │    │ status (Enum)│    │ name           │                   │
│  │ content (JSON) │    │ progress     │    │ viewMode (Enum)│                   │
│  │ timelineStartMs│    │ outputUrl    │    │ rows/cols      │                   │
│  │ transform(JSON)│    │ error        │    │ segments[]     │                   │
│  │ style (JSON)   │    └──────────────┘    └────────────────┘                   │
│  │ animation (JSN)│                                                             │
│  └────────────────┘                                                             │
└──────────────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key Enums (15 total):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;RecordingState&lt;/code&gt;: &lt;code&gt;IDLE → RECORDING → UPLOADING → PROCESSING → READY | FAILED&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ExportStatus&lt;/code&gt;: &lt;code&gt;QUEUED → PROCESSING → DONE | FAILED&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SourceMode&lt;/code&gt;: &lt;code&gt;FINAL | MULTITRACK&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UserTier&lt;/code&gt;: &lt;code&gt;FREE | PRO | ENTERPRISE | ADMIN&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ViewMode&lt;/code&gt;: &lt;code&gt;GRID | SINGLE | PIP | CUSTOM&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Video Editor Architecture (LLD)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────────────────────────────────────────────────────────────────────┐
│                        Editor Architecture (Frontend)                       │
│                                                                            │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌───────────────────┐ │
│  │ useEditor   │  │usePlayback  │  │useTrack     │  │useEditorHistory   │ │
│  │ Project     │  │State        │  │Operations   │  │(Undo/Redo, 100    │ │
│  │(Auto-save   │  │(Play/Pause  │  │(CRUD clips, │  │ snapshots)        │ │
│  │ 1s debounce)│  │ Seek,       │  │ split,      │  │                   │ │
│  │             │  │ Source↔Time)│  │ transitions)│  │                   │ │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘  └───────────────────┘ │
│         └────────────────┼────────────────┘                                │
│                          ▼                                                  │
│  ┌──────────────────────────────────────────────────────────────────┐      │
│  │                         Editor.tsx                               │      │
│  │  ┌─────────────┐  ┌──────────────┐  ┌─────────────────────────┐ │      │
│  │  │ Timeline    │  │ CanvasPlayer │  │    EditorPanel          │ │      │
│  │  │ (Horizontal │  │ (request     │  │  ┌─────────────────┐   │ │      │
│  │  │  scrollable │  │  Animation   │  │  │ Tabs: Text,     │   │ │      │
│  │  │  tracks)    │  │  Frame loop) │  │  │ Transitions,   │   │ │      │
│  │  │             │  │              │  │  │ Presets,       │   │ │      │
│  │  │ Ruler       │  │ &amp;lt;video&amp;gt; →    │  │  │ Effects,       │   │ │      │
│  │  │ Track lanes │  │ &amp;lt;canvas&amp;gt;     │  │  │ Transform,     │   │ │      │
│  │  │ Clip blocks │  │  compositing │  │  │ Multicam       │   │ │      │
│  │  │ Transition  │  │  + overlays  │  │  └─────────────────┘   │ │      │
│  │  │ indicators  │  └──────────────┘  └─────────────────────────┘ │      │
│  │  └─────────────┘                                                │      │
│  └──────────────────────────────────────────────────────────────────┘      │
│                                                                            │
│  Canvas Rendering Pipeline (useCanvasVideo + RenderLoop):                  │
│    requestAnimationFrame                                                   │
│      ├─ Read current video frame from hidden &amp;lt;video&amp;gt; element              │
│      ├─ Apply transform (stretch, offset, trim)                           │
│      ├─ Render transition between clips (xfade, slide, wipe, etc.)       │
│      ├─ Composite overlays (text with font/shadow/animation)              │
│      └─ Draw to visible &amp;lt;canvas&amp;gt;                                          │
└────────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Canvas Rendering Pipeline:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;requestAnimationFrame
  ├─ Read current frame from hidden &amp;lt;video&amp;gt; element
  ├─ Apply transform (stretch, offset, trim enforcement)
  ├─ Compute active transition between clips
  │    └─ Use TransitionRenderer for 33+ types (fade, slide, wipe, blur, zoom, cube, etc.)
  ├─ Composite overlays (TEXT type with animations: fade, slide, typewriter, bounce)
  └─ Draw to visible &amp;lt;canvas&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Editor Worker (FFmpeg) Render Pipeline:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;processRenderJob()
  ├─ Collect render clips from project data
  ├─ Download source assets from S3 (content-addressable cache)
  ├─ For each clip:
  │    ├─ Apply trim + scale
  │    ├─ Apply effects (blur, color grade, chroma key, speed ramp)
  │    ├─ Apply preset (shake, glitch, VHS, cinematic-bars)
  │    └─ Generate ffmpeg filter graph
  ├─ Concatenate clips (with xfade transitions)
  ├─ Burn text overlays (drawtext with styling)
  ├─ Mix audio (volume ducking, adelay, amix)
  ├─ Upload to S3
  ├─ Update DB (ExportJob → DONE)
  └─ RPUSH "TranscodeVideo"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Multicam Architecture (V2 Design)
&lt;/h3&gt;

&lt;p&gt;Multicam is Weave's V2 feature enabling true participant-level multi-camera editing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────────────────┐
│                         Multicam Pipeline                               │
│                                                                         │
│  Meeting Recording (raw chunks)                                         │
│         │                                                               │
│         ▼                                                               │
│  Merger Worker:                                                          │
│    ├─ Reconstruct per-participant MP4s from chunks                      │
│    ├─ Upload to: participants/{userId}/merged.mp4                       │
│    ├─ Speaker Analysis:                                                 │
│    │    ├─ ffmpeg silencedetect per participant                         │
│    │    ├─ Cross-reference energy peaks every 500ms window             │
│    │    └─ Store SpeakerTimeline records                                │
│    └─ Also produce standard grid video (backward compatible)            │
│                                                                         │
│  POST /api/v1/multicam/init                                             │
│    ├─ Create EditorProject with sourceMode = MULTITRACK                 │
│    ├─ Seed N video tracks (one per participant)                         │
│    ├─ Create ParticipantSource, MulticamLayout, Framing, Priority       │
│    └─ Return projectId                                                  │
│                                                                         │
│  Frontend Editor (Multicam mode)                                        │
│    ├─ SpeakerLane: color-coded who-spoke-when bars                      │
│    ├─ Program/Cut track: user's camera switch decisions                 │
│    ├─ Participant tracks: per-person video tracks with hide/toggle      │
│    ├─ PiP/Split/Grid layout presets                                    │
│    ├─ Reframe handles: per-participant crop + zoom                      │
│    ├─ Auto-cut suggestions from speaker analysis                       │
│    └─ Camera Priority Panel: drag-sort priority                         │
│                                                                         │
│  Editor Worker (multicam render):                                       │
│    ├─ Resolve program segments (user cuts or auto-follow speaker)       │
│    ├─ For each segment:                                                 │
│    │    ├─ Single: crop+scale active speaker fullscreen                │
│    │    ├─ PiP: overlay inset thumbnail on scaled main                 │
│    │    ├─ Split: hstack side-by-side (50/50 or 70/30)                │
│    │    └─ Grid: xstack N-up grid layout                               │
│    ├─ Burn speaker labels (drawtext)                                   │
│    ├─ Replace missing sources with placeholder (color + drawtext)      │
│    ├─ Concatenate segments + apply overlays + mix audio               │
│    └─ Promote + queue transcode                                        │
└─────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. FFmpeg Filter Graph Examples
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Single clip with effects:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="s2"&gt;"
  [0:v]trim=start=10:end=20,setpts=PTS-STARTPTS,
       scale=1920:1080,
       eq=brightness=0.1:contrast=1.2:saturation=1.1,
       gblur=sigma=2[v];
  [0:a]atrim=start=10:end=20,asetpts=PTS-STARTPTS[a]"&lt;/span&gt;
&lt;span class="nt"&gt;-map&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;v] &lt;span class="nt"&gt;-map&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;a] output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;PiP layout (multicam):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; main.mp4 &lt;span class="nt"&gt;-i&lt;/span&gt; pip.mp4 &lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="s2"&gt;"
  [0:v]crop=iw*1:ih*1,scale=1920:1080[main];
  [1:v]crop=iw*0.5:ih*0.5:iw*0.25:0,scale=480:270[pip];
  [main][pip]overlay=W-w-20:H-h-20,
       drawtext=text='Speaker':x=100:y=H-60:fontsize=24:fontcolor=white[out]"&lt;/span&gt;
&lt;span class="nt"&gt;-map&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;out] &lt;span class="nt"&gt;-map&lt;/span&gt; 0:a &lt;span class="nt"&gt;-map&lt;/span&gt; 1:a output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Grid layout (2x2):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; a.mp4 &lt;span class="nt"&gt;-i&lt;/span&gt; b.mp4 &lt;span class="nt"&gt;-i&lt;/span&gt; c.mp4 &lt;span class="nt"&gt;-i&lt;/span&gt; d.mp4 &lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="s2"&gt;"
  [0:v]scale=960:540[0v]; [1:v]scale=960:540[1v];
  [2:v]scale=960:540[2v]; [3:v]scale=960:540[3v];
  [0v][1v]hstack=inputs=2[row1];
  [2v][3v]hstack=inputs=2[row2];
  [row1][row2]vstack=inputs=2,
  amix=inputs=4"&lt;/span&gt;
output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. API Endpoints (Key Routes)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Authentication
  POST /api/v1/user/signup                    # Register
  POST /api/v1/user/login                     # Login
  POST /api/v1/user/verify-email              # Verify 6-digit code
  GET  /api/v1/user/me                        # Current user

Meetings
  POST /api/v1/meeting/create                 # Create with passcode
  POST /api/v1/meeting/join/:id               # Join by roomId
  POST /api/v1/meeting/end/:id                # Host end meeting
  POST /api/v1/meeting/create/schedule        # Schedule meeting
  GET  /api/v1/meeting/get/:id                # Meeting details

Recording
  POST /api/v1/upload-chunk                   # Upload encrypted chunk
  POST /api/v1/recording/start/:id            # Host start recording
  POST /api/v1/recording/stop/:id             # Host stop recording
  GET  /api/v1/recording/status/:id           # Poll recording state
  GET  /api/v1/recording/page/:id             # Full recording page

Editor
  POST /api/v1/editor/projects                # Create editor project
  GET  /api/v1/editor/projects/:id            # Get project with tracks
  PUT  /api/v1/editor/projects/:id            # Save project state
  POST /api/v1/editor/projects/:id/exports    # Queue export job
  GET  /api/v1/editor/exports/:jobId/stream   # SSE export progress

Multicam
  POST /api/v1/multicam/init                  # Create multicam project
  GET  /api/v1/multicam/projects/:id          # Full multicam manifest
  PUT  /api/v1/multicam/projects/:id/framings # Update crop/zoom
  PUT  /api/v1/multicam/projects/:id/priorities# Camera priority
  POST /api/v1/multicam/projects/:id/auto-cut # Generate cuts

Worker (internal)
  POST /api/v1/worker/recording-status/:id    # Worker → Backend callback

Encryption
  GET  /api/v1/keys/public                    # Server RSA public key (JWK)
  POST /api/v1/keys/meeting/:id/wrapped-cek   # Store wrapped CEK

Integrations
  GET  /api/v1/google/auth/url                # Google OAuth URL
  GET  /api/v1/github                         # GitHub OAuth redirect
  POST /api/v1/github/create/issue            # Create GitHub issue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  7. Key API Contracts
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Upload Chunk (the most critical endpoint):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /api/v1/upload-chunk
Headers: Authorization: Bearer &amp;lt;jwt&amp;gt;
Body: multipart/form-data
  - file: binary (min 48 bytes)
  - meetingId: string
  - participantId: string
  - sequenceNumber: number
  - startedAt: ISO datetime
  - durationMs: number
  - sourceMimeType: string
  - encryptionAlgorithm?: "AES-GCM"
  - encryptionIv?: string (base64)
  - encryptionTagBits?: 128

Response: 201 { id, bucketLink }
Rate Limit: 600 req/min per user
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Worker Recording Status (inter-service):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/api/v&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;/worker/recording-status/:meetingId&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Headers:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;x-worker-token:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;worker-service-jwt&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Body:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;status:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PROCESSING"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"READY"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FAILED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;finalPath?:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;version?:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;On&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;READY:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;updates&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Meeting.recordingState,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;creates&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;FinalRecording,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="err"&gt;sends&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;RECORDING_READY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;notification&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;On&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;FAILED:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;sets&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;recordingState&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;FAILED,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;sends&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;RECORDING_FAILED&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;notification&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Design Decisions &amp;amp; Tradeoffs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Rationale&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Local recording in chunks&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Eliminates quality loss from bad internet; enables E2E encryption&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AES-256-GCM per chunk&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Authenticated encryption prevents tampering; deterministic IV avoids nonce reuse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RSA-OAEP key wrapping&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Only the server can unwrap CEKs; clients never see the private key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Redis Lists for queues&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Simple, reliable, no extra dependency (compared to Bull/SQS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Per-participant video reconstruction&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Enables true multicam editing from raw sources instead of merged grid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FFmpeg for all video processing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Battle-tested, supports every filter we need, static binaries for portability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Single-pass multi-profile encode&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Scales to 4 HLS profiles in one FFmpeg pass instead of 4 sequential encodes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Canvas-based editor preview&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Full control over rendering pipeline; supports effects/transitions/overlays in real-time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Weave demonstrates how to build a production-grade video conferencing platform with a focus on recording quality and post-production editing. The key architectural takeaways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Client-side encryption&lt;/strong&gt; before upload ensures privacy even from the platform provider&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chunk-based recording&lt;/strong&gt; decouples recording quality from network quality&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis-backed job queues&lt;/strong&gt; enable reliable async processing of CPU-intensive video tasks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-participant source management&lt;/strong&gt; enables powerful multicam editing scenarios&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FFmpeg as a service&lt;/strong&gt; provides a flexible video processing pipeline without custom C++ code.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Weave is open source. &lt;a href="https://github.com/Official-Krish/weave" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>opensource</category>
      <category>security</category>
      <category>streaming</category>
    </item>
    <item>
      <title>Building QuantNest: A Workflow Engine for Automated Trading</title>
      <dc:creator>Krish Anand</dc:creator>
      <pubDate>Tue, 21 Jul 2026 06:55:22 +0000</pubDate>
      <link>https://dev.to/officialkrish/building-quantnest-a-workflow-engine-for-automated-trading-49im</link>
      <guid>https://dev.to/officialkrish/building-quantnest-a-workflow-engine-for-automated-trading-49im</guid>
      <description>&lt;h2&gt;
  
  
  The One-Line Hook
&lt;/h2&gt;

&lt;p&gt;QuantNest is a drag-and-drop workflow engine where non-programmers wire up trading strategies as directed graphs, and a poll-based executor evaluates triggers, routes through conditional branches, and executes broker/notification actions — all without writing a single line of code.&lt;/p&gt;

&lt;p&gt;The hard problem wasn't the workflow execution, or the visual builder, or even the AI integration. It was making all three work together reliably under the constraint that trading actions have financial consequences and &lt;strong&gt;exactly-once execution matters&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Constraints &amp;amp; Requirements
&lt;/h2&gt;

&lt;p&gt;I started with a few hard requirements:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No persistent connection to the database.&lt;/strong&gt; The executor polls on an interval. This keeps the architecture simple — no WebSocket server, no long-lived cursor, no replica-set tailing. If the executor crashes, it just restarts and picks up on the next poll cycle.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Exactly-once semantics for trade actions.&lt;/strong&gt; If the executor fires a buy order, crashes after the broker confirms but before persisting the result, and restarts — it should not buy again. This ruled out naive at-least-once polling.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Conditional branching must be user-comprehensible.&lt;/strong&gt; Users need to see why a particular branch was taken. Every execution must be traceable — trigger evaluation, branch decision, node status, final outcome.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Token auth that survives concurrent requests.&lt;/strong&gt; The frontend SPA makes multiple simultaneous API calls. If two 401s race and both try to refresh the token, only one should succeed, and the other should reuse the result.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Model-agnostic AI.&lt;/strong&gt; Users bring their own API keys. The platform should route to Gemini, OpenAI, or Anthropic without caring which one is configured.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These constraints shaped every major design decision.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyfcxq22mrhvu0l5wos8i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyfcxq22mrhvu0l5wos8i.png" alt=" " width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The executor is the heart of the system. It runs an infinite loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wait 2000ms + backoff
  → fetch active workflows from MongoDB
  → for each, evaluate trigger condition
  → if triggered, lock workflow (Redis SET NX PX 5000)
  → execute graph recursively
  → persist execution trace
  → release lock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The backoff is intentionally linear (+250ms per empty poll, capped at 10s). Exponential felt wrong here — you want gradual backoff during quiet periods but fast recovery when conditions change. A trader's price trigger at 2s resolution needs to catch the breakout, not be sleeping in a 30-second backoff.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Design Decisions (and the tradeoffs I wrestled with)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Poll-based vs Event-driven
&lt;/h3&gt;

&lt;p&gt;This was the first real decision. Event-driven (change streams, WebSocket, push) gives lower latency. But it adds a lot of complexity — reconnection logic, at-least-once delivery guarantees, ordering concerns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why I chose polling:&lt;/strong&gt; Simplicity wins for an MVP. The poll loop is 63 lines of code. It handles crashes trivially (restart and re-poll). The linear backoff means quiet periods don't waste CPU, but active periods get fast response.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tradeoff:&lt;/strong&gt; Minimum trigger latency is 2 seconds. For a 1-minute candle strategy this is irrelevant. For a tick-level scalper it's unacceptable. At scale I'd add a push channel for time-sensitive workflows alongside the poll loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I didn't expect:&lt;/strong&gt; The empty-poll backoff interacts badly with workflows that trigger rarely. A workflow that fires once a day sees the poll interval drift to 10s after ~30 empty cycles. The fix was resetting the backoff counter after the first successful execution of the day, not on every poll cycle. I caught this when a user's market-open trigger fired 8 seconds late.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Token Rotation with Family Tracking
&lt;/h3&gt;

&lt;p&gt;The auth design: 15-minute JWT access tokens + 7-day refresh tokens stored as SHA-256 hashes in MongoDB. Each refresh token has a &lt;code&gt;familyId&lt;/code&gt;. When rotating, the old token is revoked and a new one is created in the same family. &lt;strong&gt;If a revoked token is presented, the entire family is revoked&lt;/strong&gt; — this detects stolen token replay.&lt;/p&gt;

&lt;p&gt;The frontend has a semaphore-based queue for concurrent refresh:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;isRefreshing stops 401 from creating new refresh calls
  → if already refreshing, queue the pending request
  → on success, replay the queue
  → on failure, clear auth and redirect to login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why I chose this:&lt;/strong&gt; Because race conditions during token refresh are a real problem in SPAs. Without the queue, multiple failed requests cause cascading refreshes that lock out the user.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tradeoff:&lt;/strong&gt; The queue introduces memory pressure if many requests arrive while refresh is in-flight. In practice, this doesn't happen — the refresh window is ~200ms locally, and most SPAs don't fire 100 requests in that window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What broke in production:&lt;/strong&gt; The first version didn't have the queue. Two parallel API calls would both see 401, both try to refresh, the first would succeed, the second would fail because the first refresh rotated the token invalidating the second. The user would get logged out from a perfectly valid session. The queue + &lt;code&gt;_retry&lt;/code&gt; flag fixed this, but it took two deployment cycles to get right. The lesson: &lt;strong&gt;always assume concurrent requests will race&lt;/strong&gt;, even on a single-threaded browser event loop.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Conditional Branching as Source Handles
&lt;/h3&gt;

&lt;p&gt;Nodes with conditions (&lt;code&gt;conditional-trigger&lt;/code&gt;, &lt;code&gt;if&lt;/code&gt;, &lt;code&gt;recheck&lt;/code&gt;) expose two output handles: &lt;code&gt;true&lt;/code&gt; and &lt;code&gt;false&lt;/code&gt;. Edges connected to the &lt;code&gt;true&lt;/code&gt; handle are followed when the condition evaluates to true, and vice versa.&lt;/p&gt;

&lt;p&gt;The executor resolves branches at runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolveConditionalEdges&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;sourceNode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outgoingEdges&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;evaluatedCondition&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;outgoingEdges&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;edge&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sourceHandle&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;evaluatedCondition&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sourceHandle&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;false&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;evaluatedCondition&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// non-conditional edges pass through&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why I chose this:&lt;/strong&gt; It maps directly to the visual representation. Users see two labeled ports on the node. They drag edges from the port that matches their intent. No complex rule editor, no expression language to learn.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tradeoff:&lt;/strong&gt; This limits conditions to binary (true/false). Multi-way branching requires chaining &lt;code&gt;if&lt;/code&gt; nodes. For trading strategies this is rarely an issue — most branching is "if condition met, buy; else wait." But for general-purpose automation it would be limiting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I'd do differently:&lt;/strong&gt; Store the condition expression in the edge metadata instead of the source handle string. This would allow future extension to switch-case patterns without changing the node model.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Redis Locks for Execution Cooldown
&lt;/h3&gt;

&lt;p&gt;Every workflow execution acquires a Redis lock with 5-second TTL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;SET lock:exec:&amp;lt;workflowId&amp;gt; &amp;lt;instanceId&amp;gt; NX PX 5000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the lock can't be acquired, the execution is skipped. This prevents the same workflow from being evaluated by two poll cycles simultaneously (e.g., if a trigger evaluation takes longer than the poll interval).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why I chose this over MongoDB atomic updates:&lt;/strong&gt; Redis locks are cheaper (in-memory vs disk write) and have natural TTL expiry. If the executor crashes mid-execution, the lock self-releases in 5 seconds. MongoDB would require a manual cleanup job or a TTL index on a lock document.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tradeoff:&lt;/strong&gt; Distributed locks require all instances to agree on the Redis instance. If Redis goes down, all executions skip (fail-safe, which is correct for trading). Adding a second Redis for high availability would double the infra. For a single-instance deployment this is fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What bit me:&lt;/strong&gt; The lock release used &lt;code&gt;GETDEL&lt;/code&gt; with ownership verification — you can only release a lock you own. But a slow GC pause on the executor could cause the lock to expire, then get acquired by a second instance, then the first instance resumes and releases the second instance's lock. The fix: &lt;strong&gt;locks should be treated as advisory, not authoritative.&lt;/strong&gt; The execution checks the lock state at the start but doesn't enforce it during the run. Double-execution is prevented by idempotency keys, not locks.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. AI Provider Strategy Pattern
&lt;/h3&gt;

&lt;p&gt;The AI builder accepts natural language and produces structured workflow graphs. Three providers implement the same interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;StrategyPlannerProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;models&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AiModelDescriptor&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nf"&gt;generatePlan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;StrategyBuilderResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A resolver picks the right provider based on the user's model selection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolvePlannerProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requestedModel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// finds the provider that owns the requested model&lt;/span&gt;
  &lt;span class="c1"&gt;// falls back to a default if no match&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why I chose this:&lt;/strong&gt; Users bring their own API keys. Some prefer OpenAI, some Gemini, some Claude. The strategy pattern keeps the routing logic in one place. Adding a new provider is implementing one interface and registering it in the array.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tradeoff:&lt;/strong&gt; Each provider returns JSON in slightly different formats. The response parsing (Zod validation) has to be permissive enough to handle variance but strict enough to reject malformed graphs. A single provider with a well-defined output schema would be simpler but doesn't let users choose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What surprised me:&lt;/strong&gt; Gemini (&lt;code&gt;gemini-2.5-flash&lt;/code&gt;) is consistently 2-3x faster than Claude Opus for the same prompt. The quality difference for simple strategy generation is negligible. The strategy pattern makes this optimization invisible to the user — they pick a model, and the routing happens transparently.&lt;/p&gt;




&lt;h2&gt;
  
  
  Error Handling by the Numbers
&lt;/h2&gt;

&lt;p&gt;One of the most boring but important parts of the system: every API endpoint has explicit error paths. I counted them while writing this post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;121 &lt;code&gt;res.status(4xx/5xx)&lt;/code&gt; calls&lt;/strong&gt; across 6 route files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;11 Mongoose models&lt;/strong&gt; with &lt;strong&gt;9 indexes&lt;/strong&gt; &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;16 non-retryable error keywords&lt;/strong&gt; that prevent infinite retry loops (auth errors, validation errors, missing configuration)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3 error boundary layers&lt;/strong&gt;: Zod input validation, service-level errors, catch-all 500s&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern is consistent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;SomeSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safeParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;someOperation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isKnownError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Internal server error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every route follows this. It's repetitive but it means no unhandled rejection crashes the server, and every error returns a structured response.&lt;/p&gt;




&lt;h2&gt;
  
  
  Benchmarks (Real Numbers, Not Marketing)
&lt;/h2&gt;

&lt;p&gt;I ran these against the running system with Docker MongoDB + Redis on a Mac ARM laptop:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;p50&lt;/th&gt;
&lt;th&gt;p95&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;API request (no DB)&lt;/td&gt;
&lt;td&gt;0.7ms&lt;/td&gt;
&lt;td&gt;1.8ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Burst (50 concurrent)&lt;/td&gt;
&lt;td&gt;1.9ms&lt;/td&gt;
&lt;td&gt;17ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signin (bcrypt + JWT)&lt;/td&gt;
&lt;td&gt;89ms&lt;/td&gt;
&lt;td&gt;104ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concurrent signins (1000 users)&lt;/td&gt;
&lt;td&gt;8.4s&lt;/td&gt;
&lt;td&gt;14.6s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redis SET&lt;/td&gt;
&lt;td&gt;3.7ms&lt;/td&gt;
&lt;td&gt;5.3ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redis GET&lt;/td&gt;
&lt;td&gt;3.5ms&lt;/td&gt;
&lt;td&gt;4.1ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The signin latency is dominated by bcrypt (cost factor 10). Under 1000 concurrent requests, bcrypt serializes on the CPU and latency climbs to 8 seconds. &lt;strong&gt;The bottleneck is intentional security, not the application.&lt;/strong&gt; Switching to Argon2 or reducing the cost factor would improve throughput at the expense of password cracking resistance.&lt;/p&gt;

&lt;p&gt;The API throughput (non-DB routes) is ~1000 req/s before rate limiting kicks in. The limiting factor is the event loop, not the database.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Would I Do Differently at Scale?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. CDC Instead of Polling
&lt;/h3&gt;

&lt;p&gt;At 10,000+ active workflows, polling every 2 seconds becomes wasteful. I'd add MongoDB Change Streams for real-time trigger evaluation, falling back to polling for cold-start recovery. The poll loop would shift to a "dead letter" role — catch workflows that change streams missed.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Shard Workflows by Trigger Type
&lt;/h3&gt;

&lt;p&gt;Price triggers need sub-second evaluation. Timer triggers are fine with 5-second resolution. Report-generation triggers can wait 30 seconds. Currently all workflows share one poll interval. Sharding by trigger type would let each poller run at its optimal frequency.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Replace bcrypt with Argon2id and Add a Session Cache
&lt;/h3&gt;

&lt;p&gt;bcrypt at cost 10 gives 90ms per comparison. Under 1000 concurrent signins, this becomes 8 seconds of wall-clock time. Argon2id is hardware-resistant and faster on modern CPUs. Combined with a Redis session cache (O(1) lookup after first authentication), the majority of requests would bypass the hash entirely.&lt;/p&gt;

&lt;p&gt;Currently there's no session cache — every authenticated request verifies the JWT signature (fast, ~1µs), but token refresh hits MongoDB with a &lt;code&gt;findOne({tokenHash})&lt;/code&gt; which is O(log n). Adding &lt;code&gt;redisSet("session:&amp;lt;tokenHash&amp;gt;", userData, 3600)&lt;/code&gt; during token creation would make refreshes O(1) and reduce DB load.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Formalize the Circuit Breaker
&lt;/h3&gt;

&lt;p&gt;Right now, consecutive failure thresholds (3 failures → warn, 5 failures → pause) are hardcoded in the execution service. I'd extract this into a formal circuit breaker with half-open state, configurable thresholds per action type, and exponential reset timeouts. Trading actions should have tighter thresholds than notification actions.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Webhook Gateway
&lt;/h3&gt;

&lt;p&gt;The system currently has no webhook endpoint. For real-time broker callbacks (Zerodha order fills, Groww position updates), a webhook gateway with Redis-backed idempotency keys would replace the current polling-based state checking. The idempotency middleware already exists — it just needs a route to attach to.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Polling is underrated.&lt;/strong&gt; Everyone reaches for WebSockets and event-driven architecture. A well-tuned poll loop with linear backoff handles 90% of use cases with 10% of the complexity.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rate limiters are the first thing that breaks in benchmarks.&lt;/strong&gt; I spent more time fighting &lt;code&gt;express-rate-limit&lt;/code&gt; during load testing than writing the actual benchmark scripts. Plan your rate limit strategy before you need it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Bcrypt doesn't scale.&lt;/strong&gt; 1000 concurrent signins on a single core brings the server to its knees. If you expect high auth throughput, cache sessions aggressively or use a faster hash.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Family-based token rotation catches real attacks.&lt;/strong&gt; In testing, a revoked token was presented within minutes of rotation. The family revocation killed it. Without family tracking, a stolen refresh token would stay valid until the user signs out.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The 61µs AI pipeline overhead is a rounding error.&lt;/strong&gt; The actual latency is 100% determined by the AI provider. Don't optimize your prompt building pipeline — optimize your model selection.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Check it out: &lt;a href="https://quantnest.krishlabs.tech" rel="noopener noreferrer"&gt;QuantNest&lt;/a&gt;&lt;br&gt;
Found a bug or have an idea? &lt;a href="https://github.com/Official-Krish/QuantNest/issues/new" rel="noopener noreferrer"&gt;Open an issue&lt;/a&gt;.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How I Built an AI Interview Platform That Survives Redis Outages</title>
      <dc:creator>Krish Anand</dc:creator>
      <pubDate>Mon, 20 Jul 2026 12:38:18 +0000</pubDate>
      <link>https://dev.to/officialkrish/how-i-built-an-ai-interview-platform-that-survives-redis-outages-4h1e</link>
      <guid>https://dev.to/officialkrish/how-i-built-an-ai-interview-platform-that-survives-redis-outages-4h1e</guid>
      <description>&lt;p&gt;Real‑time AI voice interviews and 100 % availability even when Redis, the email provider, or the AI itself goes down.&lt;/p&gt;




&lt;h2&gt;
  
  
  The One‑Line Hook
&lt;/h2&gt;

&lt;p&gt;Evalio runs live AI‑powered voice interviews that adapt to the candidate, remember every session, and score across six dimensions — all while the backend is designed to &lt;strong&gt;never return a 500&lt;/strong&gt;, no matter which dependency fails.&lt;/p&gt;




&lt;h2&gt;
  
  
  Constraints That Shaped Every Tradeoff
&lt;/h2&gt;

&lt;p&gt;Before I wrote a single line of business logic, I locked in four non‑negotiables:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Constraint&lt;/th&gt;
&lt;th&gt;Why it mattered&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Real‑time audio latency &amp;lt; 2 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A voice interview with lag feels broken. Candidates leave.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;100 % availability under dependency failure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Users trust me with their practice time. Losing a session because an email API errored is a betrayal of that trust.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ship fast, alone&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;I'm a single developer. I can't afford Kafka clusters, service meshes, or six microservices. Every abstraction must earn its keep.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;End‑to‑end type safety&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No &lt;code&gt;undefined is not a function&lt;/code&gt; at 2 AM because the DB schema and the frontend types drifted apart. The compiler catches everything.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Architecture (The 30‑Second Tour)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8txn1xlz6xawct4b34av.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8txn1xlz6xawct4b34av.png" alt="High Level Design" width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two servers, one monorepo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser → nginx
            ├── HTTP  :3000  (Elysia)  → Auth, CRUD, Evaluation
            └── WS    :8080  (ws)      → Audio stream, Gemini relay
                                              → Gemini 2.5 Flash Live API
Redis   → Cache, Queue, Rate‑limiting
Postgres → Sessions, Turns, Users, Resumes
Resend  → Email notifications
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why two servers instead of one?&lt;/strong&gt; Elysia supports WebSocket on the same port. I deliberately separated them because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP requests are short (milliseconds). WS connections live 30–60 minutes. A traffic spike on one shouldn't starve the other.&lt;/li&gt;
&lt;li&gt;They scale independently. When the user base grows, the WS server needs more memory per connection; the HTTP server needs more CPU. Different profiles, different scaling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tradeoff is operational complexity — two ports, two health checks, two services in the Kubernetes manifest. Worth it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Design Decisions
&lt;/h2&gt;

&lt;p&gt;This is the heart of the article. Every choice I made was a tradeoff. Here's what I chose and why.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Redis over PostgreSQL for the Queue
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Why:&lt;/strong&gt; Real‑time interviews need a queue. When all 4 concurrent slots are full, the next candidate waits and sees their position update live. Redis sorted sets give me O(log N) push/pop and O(1) position lookups. The position is pushed to the waiting client via WebSocket — instant feedback, no polling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tradeoff:&lt;/strong&gt; Redis is in‑memory and stateful. If it restarts, the queue vanishes. PostgreSQL would survive a restart but its pub/sub model adds latency and polling complexity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigation that saved me:&lt;/strong&gt; The queue isn't essential for correctness — it's a UX nicety. When Redis fails, &lt;code&gt;dequeueNext()&lt;/code&gt; returns &lt;code&gt;null&lt;/code&gt;, &lt;code&gt;getPosition()&lt;/code&gt; returns &lt;code&gt;0&lt;/code&gt;, and the system treats every session as instantly ready. No candidate is blocked by a queue outage. This isn't a hack — it's a deliberate design: &lt;strong&gt;a queue that causes errors is worse than no queue at all.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Circuit Breaker for Cache (50 Lines, 100 % Availability)
&lt;/h3&gt;

&lt;p&gt;The cache middleware wraps route handlers with get/set Redis. If Redis is slow or down, every request blocks and eventually 500s. That's unacceptable.&lt;/p&gt;

&lt;p&gt;The fix is a textbook circuit breaker living in 50 lines at &lt;code&gt;apps/backend/src/lib/cache.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3 failures → circuit OPEN → all cache ops return null → DB fallback
15 s later  → circuit HALF‑OPEN → one probe request
Probe succeeds → circuit CLOSED → caching resumes
Probe fails    → back to OPEN, wait another 15 s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The mistake I almost made:&lt;/strong&gt; The original &lt;code&gt;isReady()&lt;/code&gt; method didn't delegate to &lt;code&gt;canAttempt()&lt;/code&gt;. When the circuit was open, it never probed — the system stayed degraded forever. Found it during benchmark testing. A bug that would have looked like a design failure in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result verified by benchmarks:&lt;/strong&gt; During a simulated Redis outage, the API maintained &lt;strong&gt;100 % availability&lt;/strong&gt; with no error rate increase. The only cost was slightly higher DB load (our queries average 1.70 ms, so even direct DB hits are fast).&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Graceful Degradation at Every Layer
&lt;/h3&gt;

&lt;p&gt;Three independent failure modes, three different recovery patterns:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Fails How&lt;/th&gt;
&lt;th&gt;Fallback&lt;/th&gt;
&lt;th&gt;Recovery&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Redis&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Timeout / connection refused&lt;/td&gt;
&lt;td&gt;Cache returns null → DB query&lt;/td&gt;
&lt;td&gt;15 s circuit breaker probe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Email (Resend)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5xx / network error&lt;/td&gt;
&lt;td&gt;Write to &lt;code&gt;PendingEmail&lt;/code&gt; table&lt;/td&gt;
&lt;td&gt;Exponential backoff: 5 s → 25 s → 125 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gemini AI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;API error&lt;/td&gt;
&lt;td&gt;Mark session FAILED, save partial progress&lt;/td&gt;
&lt;td&gt;Candidate retries manually&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pattern is consistent: &lt;strong&gt;never crash, never lose data, always have a forward path.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cache fallback&lt;/strong&gt;: The &lt;code&gt;cached()&lt;/code&gt; middleware catches Redis errors, returns &lt;code&gt;null&lt;/code&gt;, and the handler queries PostgreSQL directly. The caller never sees a failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email buffer&lt;/strong&gt;: If Resend is down, the email is inserted into a &lt;code&gt;PendingEmail&lt;/code&gt; table. A background job (&lt;code&gt;flushPendingEmails&lt;/code&gt;) picks it up later with exponential backoff. &lt;strong&gt;100 % delivery rate guaranteed&lt;/strong&gt;, even during email provider outages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session preservation&lt;/strong&gt;: If Gemini errors mid‑interview, the session is marked FAILED but all turns and scores are saved. The candidate picks up where they left off.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. End‑to‑End Types: Prisma → Elysia → Eden → React
&lt;/h3&gt;

&lt;p&gt;This is the single biggest productivity win for a solo developer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Prisma schema → TypeScript types
     ↓
Elysia route  → uses Prisma types in request/response
     ↓
Eden Treaty   → auto‑generates type‑safe client
     ↓
React page    → imports Eden client, gets full autocomplete
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If I rename a field in the Prisma schema, the compiler catches every broken reference across all four packages before I even run the tests. No runtime surprises. No "why is this undefined" debugging sessions at midnight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tradeoff:&lt;/strong&gt; Tight coupling between backend and frontend types. In a monorepo with exactly one frontend, this is a feature. If I ever need a public API for third‑party consumers, I'd split the contract into OpenAPI/Swagger.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Prompt Layering over Monolithic Prompts
&lt;/h3&gt;

&lt;p&gt;The AI's system prompt is assembled dynamically from 10 independent layers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Objective + Candidate History + Company Context
+ Role Context + Interview Style + Interaction Depth
+ Resume + GitHub + Job Description + Evaluation Dimensions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why:&lt;/strong&gt; A single 3000‑word system prompt is unmaintainable. You can't A/B test "Role" separately from "Style" when they're in one blob. With layers, I can tweak the &lt;code&gt;roleContext.ts&lt;/code&gt; file and know exactly what changed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Impact weighting&lt;/strong&gt; (measured empirically):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Role context: ~60 % of the interview quality&lt;/li&gt;
&lt;li&gt;Interview style: ~25 %&lt;/li&gt;
&lt;li&gt;Company context: ~10 %&lt;/li&gt;
&lt;li&gt;Depth level: ~5 %&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Tradeoff:&lt;/strong&gt; ~200 lines of assembly logic instead of a single string. Worth every line for debuggability.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Gemini 2.5 Flash over GPT‑4o for Voice
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Why:&lt;/strong&gt; Gemini's API natively supports bidirectional audio streaming. The browser mic → WebSocket → Gemini → audio response pipeline completes in ~500 ms. GPT‑4o requires a three‑stage pipeline (STT → LLM → TTS), which adds 1.5–2 s of latency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tradeoff:&lt;/strong&gt; GPT‑4o gives better‑quality answers on complex technical questions. But in a voice interview, latency destroys the experience faster than imperfect answers. A 2‑second delay makes the AI feel disengaged; a 500 ms delay feels like a real person thinking.&lt;/p&gt;




&lt;h2&gt;
  
  
  Benchmarks That Validate the Design
&lt;/h2&gt;

&lt;p&gt;Numbers are meaningless without context. Here's what each benchmark proves:&lt;/p&gt;

&lt;h3&gt;
  
  
  Database Performance
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query&lt;/th&gt;
&lt;th&gt;Total Time&lt;/th&gt;
&lt;th&gt;Scan Type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Interview list (user + date)&lt;/td&gt;
&lt;td&gt;5.13 ms&lt;/td&gt;
&lt;td&gt;Index Scan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate‑limit count (7‑day window)&lt;/td&gt;
&lt;td&gt;0.93 ms&lt;/td&gt;
&lt;td&gt;Index Scan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Score trend (last 5 sessions)&lt;/td&gt;
&lt;td&gt;0.29 ms&lt;/td&gt;
&lt;td&gt;Index Scan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interview detail (with joins)&lt;/td&gt;
&lt;td&gt;1.16 ms&lt;/td&gt;
&lt;td&gt;Index Scan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refresh‑token lookup&lt;/td&gt;
&lt;td&gt;1.01 ms&lt;/td&gt;
&lt;td&gt;Index Scan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Average&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.70 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;What this proves:&lt;/strong&gt; The composite indexes on &lt;code&gt;InterviewSession(userId, createdAt)&lt;/code&gt; and &lt;code&gt;(userId, status, createdAt)&lt;/code&gt; eliminated sequential scans. Without them, these queries would take 50–200 ms at 10k rows. Estimated improvement: 29–117×.&lt;/p&gt;

&lt;h3&gt;
  
  
  API Throughput
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concurrency&lt;/th&gt;
&lt;th&gt;Throughput&lt;/th&gt;
&lt;th&gt;Avg Latency&lt;/th&gt;
&lt;th&gt;P95&lt;/th&gt;
&lt;th&gt;Errors&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10×&lt;/td&gt;
&lt;td&gt;19,190 req/s&lt;/td&gt;
&lt;td&gt;0.4 ms&lt;/td&gt;
&lt;td&gt;1.2 ms&lt;/td&gt;
&lt;td&gt;0 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25×&lt;/td&gt;
&lt;td&gt;19,355 req/s&lt;/td&gt;
&lt;td&gt;0.9 ms&lt;/td&gt;
&lt;td&gt;1.9 ms&lt;/td&gt;
&lt;td&gt;0 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50×&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;23,318 req/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.4 ms&lt;/td&gt;
&lt;td&gt;5.0 ms&lt;/td&gt;
&lt;td&gt;0 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100×&lt;/td&gt;
&lt;td&gt;16,262 req/s&lt;/td&gt;
&lt;td&gt;3.9 ms&lt;/td&gt;
&lt;td&gt;6.6 ms&lt;/td&gt;
&lt;td&gt;0 %&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;What this proves:&lt;/strong&gt; The system saturates around 50 concurrent connections on an M3 MacBook. Beyond that, throughput drops slightly due to Bun's event loop pressure. But 23 k req/s is well above any realistic load for a bootstrapped SaaS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Circuit Breaker &amp;amp; Degradation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Circuit breaker&lt;/strong&gt;: 3 failures → open → 15 s → half‑open → recovery. Verified end‑to‑end.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Degradation&lt;/strong&gt;: Redis outage → 100 % API availability. Email outage → 0 % delivery loss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audio latency&lt;/strong&gt;: ~500 ms end‑to‑end (estimate, measured locally without production network conditions).&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What I'd Do Differently at Scale
&lt;/h2&gt;

&lt;p&gt;The current design works for a solo‑dev bootstrapped product. If I woke up tomorrow with 10 000 concurrent users, here's what would break first and how I'd fix it:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Replace Redis Queue with Kafka (or PostgreSQL &lt;code&gt;SKIP LOCKED&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Redis sorted sets work well until a node restarts mid‑interview and the queue state disappears. At scale:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kafka&lt;/strong&gt;: Durable, replayable, partition‑aware. Overkill today, necessary when queue state becomes a correctness requirement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL &lt;code&gt;SKIP LOCKED&lt;/code&gt;&lt;/strong&gt;: Simpler, no new infrastructure, but polling adds latency for position updates.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Split the WebSocket Server into Its Own Autoscaling Group
&lt;/h3&gt;

&lt;p&gt;Currently both servers live in one Kubernetes deployment. WS connections pin memory and CPU — an interview session consuming 200 MB for audio buffers shouldn't compete with HTTP requests for the same resources.&lt;/p&gt;

&lt;p&gt;At scale: separate &lt;code&gt;Deployment&lt;/code&gt; for &lt;code&gt;evalio-backend-ws&lt;/code&gt; with its own HPA based on active connections, while the HTTP server scales on request rate.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. CDN for Audio Response Caching
&lt;/h3&gt;

&lt;p&gt;Every audio chunk from Gemini is streamed through the WS to the client. If I cached common responses (greetings, explanations) on a CDN, I could reduce Gemini API calls by an estimated 20–30 %.&lt;/p&gt;

&lt;p&gt;The tricky part: audio responses are ephemeral and session‑specific. Only deterministic, non‑personalized chunks can be cached.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Shard by User ID
&lt;/h3&gt;

&lt;p&gt;The current queue is a single sorted set. With 10 k concurrent users, &lt;code&gt;ZRANGE&lt;/code&gt; operations on a set with millions of entries become noticeable. Sharding by &lt;code&gt;userId % SHARD_COUNT&lt;/code&gt; would distribute load across N Redis instances.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Real Monitoring (Not Just Logs)
&lt;/h3&gt;

&lt;p&gt;Today I rely on circuit breaker logs and manual health checks. At scale, I'd add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prometheus metrics&lt;/strong&gt; for cache hit ratio, queue depth, circuit breaker state&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alerts&lt;/strong&gt; for circuit‑breaker open events and email buffer growth&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tracing&lt;/strong&gt; across the audio pipeline to pinpoint latency sources&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A cache that 500s is worse than no cache.&lt;/strong&gt; The circuit breaker is 50 lines of code that made Redis a non‑critical dependency instead of a single point of failure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Graceful degradation isn't optional.&lt;/strong&gt; Three fallback mechanisms — cache, queue, email — cover every external dependency. None of them are complex. All of them prevent user‑visible errors.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Type‑safety is a force multiplier for solo devs.&lt;/strong&gt; The Prisma → Elysia → Eden pipeline caught dozens of refactoring mistakes before they reached production. I'd trade a few percentage points of runtime performance for compile‑time safety any day.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Benchmarks exposed a bug that would have been catastrophic.&lt;/strong&gt; The &lt;code&gt;isReady()&lt;/code&gt; → &lt;code&gt;canAttempt()&lt;/code&gt; delegation bug was invisible during development. Only the benchmark's failure‑injection test revealed it. Always stress‑test your failure modes.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Built with Bun, Elysia, React 19, PostgreSQL, Redis, and Gemini 2.5 Flash. Deployed on Kubernetes via ArgoCD.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Check it out: &lt;a href="https://evalio.krishlabs.tech/" rel="noopener noreferrer"&gt;Evalio&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Found a bug or have an idea? &lt;a href="https://github.com/Official-Krish/Evalio/issues/new" rel="noopener noreferrer"&gt;Open an issue&lt;/a&gt;.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
