Skip to content

Mahakisore7/Agora-Frontend

Repository files navigation

Agora

Agora — Web Client

The real-time debate arena. A voice-first, streaming interface where a human debates a competitive AI opponent under formal parliamentary rules, with WUDC-style adjudication delivered live.


Next.js React TypeScript Tailwind CSS Supabase Status




Architecture · User Flow · Engineering Decisions · WebSocket Protocol · Getting Started


Table of Contents

  1. Overview
  2. Product Capabilities
  3. Product Tour
  4. Architecture
  5. User Flow
  6. Application Routes
  7. Engineering Decisions
  8. State Management
  9. Real-Time Audio Pipeline
  10. WebSocket Protocol
  11. Authentication
  12. Project Structure
  13. Tech Stack
  14. Getting Started
  15. Environment Variables
  16. Development
  17. Deployment
  18. Browser Compatibility
  19. Troubleshooting
  20. Contributing

Overview

Agora is a competitive parliamentary debate platform. The product allows a user to:

  • Compete in Asian Parliamentary (AP) or British Parliamentary (BP) formats against an AI opponent that respects WUDC role duties.
  • Speak naturally into their microphone with sub-second transcription.
  • Receive streaming, voice-synthesized responses from the AI in real time.
  • Get tournament-grade adjudication after each match — clash extraction, weighted matrix, WUDC pillar scoring, and per-speaker grading with verbatim quotes.

This repository is the web client. It owns the visual interface, the real-time client state, and the bridge between browser media APIs and the platform's backend.

Where this service sits

Agora is composed of three services. This repository is the user-facing one.

Service Responsibility Stack
agora-frontend (this repo) Browser UI, WebSocket lifecycle, microphone capture, audio playback queue Next.js, React, TypeScript, Zustand
agora-gateway WebSocket broker, STT/TTS multiplexer, reverse proxy, Redis state mutator Go, Gorilla, Redis
agora-ai-engine Four-phase debater, five-phase adjudicator, RAG, persistence Python, FastAPI, LangChain, pgvector

The client talks exclusively to the gateway. The gateway talks to the AI engine through Redis and a reverse HTTP proxy.


Product Capabilities

Live voice streams. Audio is captured at 250 ms slices via MediaRecorder, transmitted as binary frames over an authenticated WebSocket, and transcribed by Deepgram with confidence metrics surfaced to the UI.

Adaptive AI opponents. The AI's skill level is configurable per match across three independent levers — retrieval depth, memory drop probability, and persona temperature. The client surfaces these as Beginner, Intermediate, and Advanced.

Two tournament formats. Asian Parliamentary (six speakers) and British Parliamentary (eight speakers, four teams) are first-class. Role enums, speaker schedules, and team labels are enforced end-to-end.

Sequential audio playback. Multiple TTS chunks arrive concurrently but are queued and played serially through the Web Audio API. No clipping, no overlap, no perceptible gaps between sentence chunks.

Streaming token UI. Each AI token is appended to the transcript as it arrives. Framer Motion handles layout shifts so the DOM grows fluidly without jank or scroll-snap artifacts.

Persistent real-time state. The WebSocket connection and the audio queue live outside the React component lifecycle. Page navigation, component unmounts, and tab focus changes do not sever an active debate.

Edge-level authentication. Supabase JWTs are validated by Next.js middleware on the Edge Network before any client bundle is shipped to unauthenticated requesters.

Full match history and analytics. Users can review every match, drill into per-speaker breakdowns, and track win rate, average score, and best score across both formats.


Product Tour

Landing
Landing
Sign In
Sign in
Dashboard
Dashboard
Match Configuration
Setup
Case Preparation
Case Prep
Arena — Idle
Arena Idle
Arena — Streaming Speech
AI Speaking
Arena — Active Speaker
Arena Streaming
Adjudication In Progress
Adjudication
Results — Verdict
Verdict
Results — Pillars and Matrix
WCM Pillars
Results — Speaker Grades
Speakers
Match History
History
Profile
Profile

Architecture

System topology

graph TD
    subgraph Browser["Browser — this repository"]
        UI[Next.js App Router]
        Z[Zustand store]
        AC[AudioContext + queue]
        MR[MediaRecorder · 250 ms]
        UI <--> Z
        Z <--> AC
        Z <--> MR
    end

    subgraph Edge["Edge"]
        MW[middleware.ts<br/>Supabase JWT gate]
    end

    subgraph Backend["Microservices"]
        GW[Go Gateway · :8080]
        AI[Python AI Engine · :8000]
        REDIS[(Redis Pub/Sub)]
        DB[(Postgres + pgvector)]
        GW <--> REDIS
        AI <--> REDIS
        AI <--> DB
    end

    subgraph External["External services"]
        SB[(Supabase Auth)]
        DG[Deepgram STT/TTS]
        GROQ[Groq · LLaMA-3]
        COH[Cohere Embeddings]
    end

    Browser <-->|WSS · binary + JSON| GW
    Browser <-->|HTTPS · REST| GW
    Browser --> MW
    MW <--> SB
    GW <-->|WSS| DG
    GW -->|HTTPS| DG
    AI <-->|HTTPS| GROQ
    AI <-->|HTTPS| COH
Loading

Client topology in one sentence

A thick React client owns the WebSocket and the Web Audio queue in a Zustand store, server components handle non-real-time data fetching, and Edge middleware gates authentication before any bundle leaves the CDN.

Production Deployment & Cloud Architecture

In the live production environment, the system is distributed across multiple hosting tiers to optimize for real-time performance, low latency, and secure streaming:

Deployment Architecture

1. Frontend Layer (Vercel Cloud)

  • Host: Deployed serverlessly at https://agora-frontend-alpha.vercel.app.
  • Role: Delivers the responsive, responsive web interface, handles Client state (Zustand), captures human microphone audio via browser MediaRecorder, schedules TTS playback buffers sequentially using the Web Audio API, and interacts directly with Supabase Cloud for user sign-in/sign-up sessions.

2. Real-Time Routing & Gateways (AWS EC2 VM)

  • Host: AWS EC2 instance running Amazon Linux 2023 (16.171.42.39.nip.io).
  • Nginx Reverse Proxy: Serves as the SSL/TLS termination gate (ports 80/443). Cryptographically decrypts incoming secure HTTPS/WSS traffic using a Let's Encrypt authority certificate, proxying connection queries locally to the Go gateway on http://localhost:8080.
  • Go Gateway (Port 8080): A highly concurrent reverse proxy terminating long-lived WebSocket connections, validating Supabase JWT tokens, multiplexing binary audio slices to Deepgram, and coordinating Redis message routing.
  • Python AI Engine (Port 8000): FastAPI server orchestrating the 4-phase debate agent, LangChain/Groq LLaMA models, pgvector searches, and WUDC adjudication.
  • Redis Event Broker: Active in a Docker container acting as a Pub/Sub queue to stream token arrays instantaneously between the AI Engine and Go Gateway.

3. Persistence & Auth Tier (Supabase Cloud)

  • Host: PostgreSQL + pgvector databases deployed in AWS region ap-southeast-2 (Sydney).
  • Role: Handles secure Supabase OAuth and stores tables containing debates, match configurations, speaker grades, and case-prep embeddings. Connects to backend containers via the dedicated, highly stable production pooler host (aws-1-ap-southeast-2.pooler.supabase.com).

User Flow

flowchart TD
    A[Landing Page] --> B{Authenticated?}
    B -->|No| C[Sign In / Sign Up]
    C -->|OAuth or Email| D[Supabase Auth]
    D --> E[Dashboard]
    B -->|Yes| E
    E --> F[Match Configuration]
    F --> F1[1. Choose format · AP or BP]
    F1 --> F2[2. Pick speaker role]
    F2 --> F3[3. Enter or generate motion]
    F3 --> F4[4. Choose AI difficulty]
    F4 --> G[Case Preparation]
    G --> G1[Review AI-generated brief]
    G1 --> H[Live Arena]
    H --> H1[Tap to speak · MediaRecorder]
    H1 --> H2[AI thinks · 4-phase RAG]
    H2 --> H3[AI streams text + voice]
    H3 --> H4[Optional POI exchange]
    H4 --> H5[End turn · timing captured]
    H5 -->|More speakers| H1
    H5 -->|All complete| I[Adjudication]
    I --> I1[1. Clashes]
    I1 --> I2[2. Weighted Matrix]
    I2 --> I3[3. WUDC Pillars]
    I3 --> I4[4. Speakers]
    I4 --> I5[5. Verdict]
    I5 --> J[Results]
    J --> K{Next?}
    K -->|Debate again| F
    K -->|Match history| L[History]
    K -->|Dashboard| E
Loading

Application Routes

Route Render Auth Purpose
/ Client Public Landing — product overview, calls to action
/auth/login Client Public Email, Google, GitHub authentication
/auth/signup Client Public Account creation with email confirmation
/auth/callback Route Handler OAuth code-to-session exchange
/dashboard Server Required Stats, recent matches, format breakdown
/debate/setup Client Required Four-step match configuration
/debate/[matchId]/prep Client Required AI-generated case-prep brief
/debate/[matchId] Client Required Live Arena — voice and streaming UI
/results/[matchId] Client Required Adjudication, WCM, pillars, speakers
/history Server Required All debates with AP/BP filters
/profile Client Required User profile, avatar upload

Engineering Decisions

The following are the non-trivial technical decisions made while building this client. Each is framed as a problem encountered and the constraint that drove the solution.

1. WebSocket and audio must survive React's lifecycle

Problem. React components mount and unmount on navigation, state changes, and Suspense boundaries. A naive implementation that stored the WebSocket in component state would tear down the connection every time the user opened a modal, switched tabs, or triggered a hot-reload. Audio chunks in flight would be lost.

Solution. The WebSocket reference and audio queue live in a Zustand store, with the active AudioBufferSourceNode and AudioContext hoisted to module scope. Zustand subscribes outside React's reconciliation, so updates fire without re-rendering the tree, and the singletons are immune to component unmount cascades.

Implementation. src/store/arenaStore.ts

// Module-scope singletons — outlive any component
let _audioCtx: AudioContext | null = null;
let _activeSource: AudioBufferSourceNode | null = null;
let _activeSourceStartTime: number = 0;
let _totalTurnAudioPlayed: number = 0;

2. Sequential playback for concurrent TTS chunks

Problem. The gateway sends TTS audio at sentence boundaries. For a long AI speech, ten or more audio chunks arrive within seconds. Playing them as they arrive produces an audio collision — sentence one and sentence three overlap, producing garbled speech.

Solution. Maintain an ArrayBuffer[] queue. When a chunk arrives, push and trigger processAudioQueue(). The processor decodes a single buffer through AudioContext.decodeAudioData, creates a fresh BufferSource, sets onended to recursively process the next chunk, and only then calls source.start(). Playback is strictly serial, gapless, and reorder-safe.

Implementation.

processAudioQueue: () => {
  if (state.isPlayingAudio || state.audioQueue.length === 0) return;
  set({ isPlayingAudio: true });

  audioCtx.decodeAudioData(buffer, (decoded) => {
    const source = audioCtx.createBufferSource();
    source.buffer = decoded;
    source.connect(audioCtx.destination);
    source.onended = () => {
      _totalTurnAudioPlayed += decoded.duration;
      set((s) => ({ audioQueue: s.audioQueue.slice(1), isPlayingAudio: false }));
      get().processAudioQueue();
    };
    source.start();
  });
};

3. The browser owns speech-timing measurement

Problem. AI speech duration is a metric that affects scoring and analytics. The Go gateway forwards audio bytes; the Python engine generates the text. Neither knows precisely when audio actually played in the user's browser, because network latency and TTS round-trip times are non-deterministic.

Solution. The browser measures everything. The frontend captures audio.onplay() and audio.onended() timestamps and includes them in the END_TURN event sent to the gateway. The gateway forwards the timing untouched. The AI engine persists exactly what the frontend reported.

Rationale. The browser is the only system that knows the moment audio hardware actually produced sound. Measuring anywhere else introduces network and clock-skew error.

4. Edge authentication before bundle ship

Problem. A naive Next.js app ships its JavaScript bundle first and authenticates inside React after hydration. This means unauthenticated users have already downloaded the protected client code.

Solution. middleware.ts runs on the Edge Network. Supabase JWT cookies are validated against the auth project before any bundle is served. Unauthenticated requesters to protected routes are redirected to /auth/login at the edge.

Implementation. middleware.ts uses @supabase/ssr (not @supabase/supabase-js) to handle cookies safely in the Edge runtime.

5. Streaming token UI without layout thrash

Problem. Streaming a debate speech token-by-token means hundreds of DOM mutations per second. CSS height transitions snap. auto-height containers produce flickering scroll. Naive autoscroll fights user scroll position.

Solution. Each transcript entry is a Framer Motion component. Layout shifts are interpolated by spring physics (stiffness: 200, damping: 20) so growth animates smoothly. The transcript scroll container uses controlled programmatic scroll only when the user is already at the bottom — manual scrollback is respected.

6. Audio decoupled from non-serializable Zustand state

Problem. Zustand expects serializable state. AudioContext and AudioBufferSourceNode are mutable, non-serializable browser singletons. Reducing them into Zustand state produces stale closures and broken onended callbacks.

Solution. Audio primitives live at module scope outside Zustand. The store exposes a small typed API (processAudioQueue, stopAllAudio, pauseAudio, resumeAudio, skipAiSpeech, getAudioProgress) that mutates these singletons under controlled conditions. The store still holds the queue itself, which is serializable, for the UI to observe.

7. Deterministic auto end-turn

Problem. When does an AI turn actually end? When tokens stop arriving? When audio finishes playing? When the engine sends a completion event? Each on its own is racy.

Solution. All four conditions must be true simultaneously:

currentSpeaker === "ai"
&& aiThoughtComplete === true
&& pendingAudioBlobs === 0
&& audioQueue.length === 0
&& !isPlayingAudio

When the predicate holds, the client sends END_TURN with measured timing. This is the only place in the codebase that ends an AI turn — eliminating duplicate END_TURN emissions and stale-event races.


State Management

The arena is governed by a single Zustand store at src/store/arenaStore.ts.

State shape

interface ArenaState {
  // Connection
  socket: WebSocket | null;
  connected: boolean;
  matchId: string | null;

  // Speaker tracking
  currentSpeaker: "ai" | "human" | null;
  currentSpeakerRole: string | null;

  // Streaming buffers (committed to transcript on TURN_STARTED)
  aiBufferedText: string;
  humanBufferedText: string;
  aiThoughtComplete: boolean;
  transcript: TranscriptEntry[];

  // Audio queue
  audioQueue: ArrayBuffer[];
  isPlayingAudio: boolean;
  isAudioPaused: boolean;
  audioProgress: number;       // 0..1 progress within current chunk
  audioChunkDuration: number;
  pendingAudioBlobs: number;   // FileReader operations in flight

  // Match lifecycle
  isMatchComplete: boolean;
  verdict: ArenaEvent | null;
  adjudicationComplete: boolean;
  adjudicationMessage: string | null;

  // Timing (forwarded to gateway in END_TURN)
  aiSpeechStartTime: number | null;
  humanTurnStartTime: number | null;
}

Event handler map

Inbound event Action
AI_TOKEN Append text to aiBufferedText
HUMAN_TRANSCRIPT_CHUNK Append text to humanBufferedText
HUMAN_TRANSCRIPT Flush humanBufferedText to transcript
TURN_STARTED Commit both buffers to transcript, set new speaker, reset audio counters
AI_THOUGHT_COMPLETE Set aiThoughtComplete = true, evaluate checkAiTurnComplete()
MATCH_COMPLETE Set isMatchComplete, capture verdict
ADJUDICATION_STARTED Update adjudication message
ADJUDICATION_COMPLETE Set adjudicationComplete, trigger redirect
Blob (binary) FileReader → push ArrayBuffer to queue → processAudioQueue()

Real-Time Audio Pipeline

Ingress — Human Speaking

sequenceDiagram
    participant Mic as Microphone
    participant MR as MediaRecorder
    participant WS as WebSocket
    participant GW as Go Gateway
    participant DG as Deepgram STT

    Note over MR: getUserMedia({ audio: true })
    Note over MR: new MediaRecorder(stream, { mimeType: "audio/webm" })
    Note over MR: recorder.start(250)

    loop Every 250 ms
        Mic->>MR: audio buffer
        MR->>WS: ondataavailable, send(blob)
        WS->>GW: BinaryMessage
        GW->>DG: forward stream
        DG-->>GW: transcript + confidence
        GW->>WS: HUMAN_TRANSCRIPT_CHUNK
        WS->>MR: render in UI
    end

    Note over MR: User taps End Turn
    MR->>WS: { action: STOP_MIC }
    MR->>WS: { action: END_TURN, human_speech_*_utc }
    Note over MR: stream.getTracks().forEach(t => t.stop())
Loading

Egress — AI Speaking

sequenceDiagram
    participant AI as Python AI Engine
    participant R as Redis
    participant GW as Go Gateway
    participant TTS as Deepgram TTS
    participant WS as WebSocket
    participant Z as Zustand
    participant Spk as Speakers

    loop Each generated token
        AI->>R: PUBLISH AI_TOKEN word
        R->>GW: subscriber notify
        GW->>WS: forward AI_TOKEN event
        WS->>Z: appendAiToken(word)
        Z->>WS: trigger DOM update
    end

    Note over GW: aiBuffer accumulates until . ? !
    GW->>TTS: TextToSpeech(sentence)
    TTS-->>GW: PCM 24 kHz binary
    GW->>WS: BinaryMessage(audio)
    WS->>Z: queue ArrayBuffer
    Z->>Z: processAudioQueue()
    Z->>Spk: source.start()

    AI->>R: PUBLISH AI_THOUGHT_COMPLETE
    R->>GW: subscriber notify
    GW->>WS: forward
    WS->>Z: aiThoughtComplete = true

    Note over Z: onended chain ends
    Z->>WS: { action: END_TURN, ai_speech_*_utc }
Loading

WebSocket Protocol

Endpoint

wss://{NEXT_PUBLIC_WS_BASE_URL}/ws/live?match_id={matchId}&token={JWT}

Client → Server

Direction Event Payload
JSON START_MATCH { "action": "START_MATCH" }
JSON STOP_MIC { "action": "STOP_MIC" }
JSON END_TURN { "action": "END_TURN", "human_speech_start_time_utc": ..., "human_speech_end_time_utc": ..., "human_speech_duration_ms": ... }
JSON POI_OFFERED { "action": "POI_OFFERED", "text": "..." }
Binary Audio chunk Blob (250 ms audio/webm)

Server → Client

Direction Event Payload
JSON TURN_STARTED { event, speaker: "ai" | "human", role, side, turn_index }
JSON AI_TOKEN { event, text }
JSON AI_THOUGHT_COMPLETE { event }
JSON HUMAN_TRANSCRIPT_CHUNK { event, text, confidence }
JSON HUMAN_TRANSCRIPT { event, text }
JSON POI_ACCEPTED / POI_DECLINED { event }
JSON MATCH_COMPLETE { event, match_id, message }
JSON ADJUDICATION_STARTED { event }
JSON ADJUDICATION_COMPLETE { event, verdict, gov_total_score, opp_total_score, ... }
JSON ADJUDICATION_ERROR / AI_ERROR { event, error_message }
Binary TTS audio PCM 24 kHz linear16

Authentication

Provider

Supabase Auth with three sign-in methods: email and password, Google OAuth, and GitHub OAuth.

Edge gate

middleware.ts runs on every request matching the protected matcher. It uses @supabase/ssr (the cookie-safe SSR client) to validate the session.

Protected routes:        /dashboard, /debate/*, /results/*, /history, /profile
Reverse-protected:       /auth/login, /auth/signup  (signed-in users are redirected away)

Token flow

Browser
   │
   ├── Supabase Auth ── JWT
   │
   ├── Auth cookie via @supabase/ssr
   │
   ├── REST:  Authorization: Bearer {JWT}
   │
   └── WS:    ?token={JWT}

Project Structure

agora-frontend/
├── assets/                          README screenshots
├── public/                          Static assets
├── middleware.ts                    Edge auth gate (Supabase SSR)
├── next.config.ts
├── components.json                  shadcn/ui config
│
├── src/
│   ├── app/                         Next.js App Router
│   │   ├── layout.tsx               Root layout, AuthProvider, Toaster
│   │   ├── page.tsx                 Landing
│   │   ├── auth/
│   │   │   ├── login/page.tsx
│   │   │   ├── signup/page.tsx
│   │   │   └── callback/route.ts    OAuth exchange
│   │   ├── dashboard/page.tsx       Server component, stats and matches
│   │   ├── debate/
│   │   │   ├── setup/page.tsx       Four-step config form
│   │   │   └── [matchId]/
│   │   │       ├── page.tsx         Live Arena (client)
│   │   │       └── prep/page.tsx    Case-prep review
│   │   ├── results/[matchId]/page.tsx
│   │   ├── history/page.tsx
│   │   └── profile/page.tsx
│   │
│   ├── store/
│   │   └── arenaStore.ts            WebSocket + audio queue state
│   │
│   ├── lib/
│   │   ├── api.ts                   REST client + role enums
│   │   ├── supabase/
│   │   │   ├── client.ts            createBrowserClient
│   │   │   └── server.ts            createServerClient (cookies)
│   │   └── utils.ts                 cn() helpers
│   │
│   ├── components/
│   │   ├── providers/
│   │   │   └── AuthProvider.tsx     Session context, onAuthStateChange
│   │   └── ui/                      shadcn/ui primitives
│   │
│   ├── hooks/                       Custom hooks
│   └── types/                       Shared TypeScript types
│
├── package.json
├── tsconfig.json
├── eslint.config.mjs
└── postcss.config.mjs

Tech Stack

Framework

Technology Version Purpose
Next.js 16.2.3 App Router, Server Components, Edge Middleware
React 19.2.4 Concurrent rendering
TypeScript 5 Strict typing through socket events and API responses

State and data

Technology Purpose
Zustand 5 Out-of-React state for socket and audio
@supabase/ssr Cookie-safe SSR auth client
@supabase/supabase-js Browser client for OAuth and storage

User interface

Technology Purpose
Tailwind CSS 4 Utility-first styling
shadcn/ui Radix-based, copy-in component primitives
Framer Motion 12 Spring-modeled layout transitions
Lucide React Iconography
Sonner Toast notifications
next-themes Dark-mode handling

Browser APIs

API Purpose
MediaRecorder 250 ms audio/webm chunked microphone capture
AudioContext decodeAudioData + BufferSource playback chain
WebSocket Bidirectional binary and JSON channel to gateway

Getting Started

Prerequisites

  • Node.js 18 or later (LTS recommended)
  • npm, yarn, or pnpm
  • A running Go gateway on localhost:8080
  • A running Python AI engine on localhost:8000
  • A Supabase project (URL and anon key)

Install

git clone <repository-url>
cd agora-frontend
npm install

Configure

cp .env.example .env.local
# Edit .env.local with your credentials

Run

npm run dev
# Open http://localhost:3000

Environment Variables

Create .env.local from .env.example:

# Supabase
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOi...

# Backend
NEXT_PUBLIC_API_BASE_URL=http://localhost:8080
NEXT_PUBLIC_WS_BASE_URL=ws://localhost:8080

# Public site URL (used for OAuth redirect)
NEXT_PUBLIC_SITE_URL=http://localhost:3000

All NEXT_PUBLIC_* variables are exposed to the browser. Do not place service-role keys here.


Development

Available scripts

Command Effect
npm run dev Start Next.js dev server with HMR
npm run build Production build with type-check
npm start Run the production server
npm run lint ESLint via eslint-config-next

Code conventions

  • Default to server components. Switch to "use client" only when browser APIs or event handlers require it.
  • All WebSocket events pass through the discriminated union ArenaEvent in arenaStore.ts.
  • Compose Tailwind classes through cn() from lib/utils.ts (clsx + tailwind-merge).
  • Prefer Framer Motion components over CSS transitions for any layout that animates during streaming.

Adding a new WebSocket event

  1. Extend the EventType union in src/store/arenaStore.ts.
  2. Add the optional field to ArenaEvent.
  3. Add a case in the socket.onmessage handler.
  4. Wire the UI consumer via useArenaStore(state => state.<field>).

Deployment

The application is a standard Next.js project and runs anywhere Next.js runs. The recommended target is Vercel, since middleware.ts is designed to execute on the Edge runtime.

Vercel (Production)

The frontend is live at https://agora-frontend-alpha.vercel.app. To deploy your own frontend instance:

  1. Push your repository to GitHub.
  2. Import the project in Vercel.
  3. Configure target Environment Variables in Project Settings → Environment Variables:
    • NEXT_PUBLIC_SUPABASE_URL: Your Supabase project URL.
    • NEXT_PUBLIC_SUPABASE_ANON_KEY: Your Supabase anonymous key.
    • NEXT_PUBLIC_API_BASE_URL: The secure production API backend (https://16.171.42.39.nip.io).
    • NEXT_PUBLIC_WS_BASE_URL: The secure production WebSocket backend (wss://16.171.42.39.nip.io).
    • NEXT_PUBLIC_SITE_URL: Your Vercel frontend domain (https://your-app.vercel.app).
  4. Ensure the backend domain uses HTTPS. Browsers enforce strict security constraints on raw microphone capture (getUserMedia) and Web Audio APIs, blocking them on insecure HTTP origins (except localhost).

Important

Mixed Content Blocks Resolved: In early deployments, the frontend experienced Failed to generate motion errors due to "Mixed Content" security rules. Because the frontend was served over secure HTTPS on Vercel, modern browsers strictly blocked API requests and WebSocket upgrades to the EC2 backend's unencrypted IP address (http://16.171.42.39:8080).

We resolved this production blocker by:

  1. Setting up a dynamic DNS hostname utilizing the nip.io wildcard mapping to map 16.171.42.39 to 16.171.42.39.nip.io.
  2. Placing an Nginx proxy on the EC2 machine listening on Ports 80 and 443.
  3. Running Certbot to generate and auto-renew a valid, browser-trusted Let's Encrypt SSL Certificate for the hostname.
  4. Pointing Vercel's environment variables to the new secure https:// and wss:// dynamic domain gateways.

Self-hosted

npm run build
npm start

If the application is fronted by a reverse proxy, ensure the proxy forwards the standard Next.js routes — including the middleware-protected routes and the OAuth callback at /auth/callback. No special WebSocket handling is required at the frontend tier; the WebSocket connection terminates at the Go gateway, not at this service.


Browser Compatibility

Capability Chrome Firefox Safari Edge
MediaRecorder (audio/webm) Yes Yes iOS lacks WebM container; an MP4 shim is required for production Yes
AudioContext Yes Yes Requires a user gesture before resume Yes
WebSocket Yes Yes Yes Yes
Supabase OAuth cookies Yes Yes Yes Yes

Safari autoplay policy keeps the AudioContext suspended until a user gesture. The Arena's microphone button is the trusted gesture that unlocks playback.


Troubleshooting

Microphone permission is denied

Browsers block getUserMedia over plain HTTP except on localhost. Develop on localhost:3000 and deploy on HTTPS.

AI tokens stream but no audio plays

The AudioContext is suspended (Safari autoplay policy). Click the microphone or any user-gesture button — the next processAudioQueue() call will resume the context.

Session is dropped on localhost reload

Supabase cookies are flagged SameSite=Lax and Secure in some configurations. Local HTTP browsers may discard them on hard reload. Deploy to HTTPS to resolve.

WebSocket connects then closes immediately
  • Confirm the Go gateway is reachable at NEXT_PUBLIC_WS_BASE_URL.
  • Verify the JWT in the query string is well-formed.
  • Open DevTools, Network, WS frame: inspect the close code (1006 indicates no auth, 1011 indicates a backend crash).
Audio chunks play out of order or overlap

The recursive onended chain serializes playback. If overlap occurs, the queue was mutated outside Zustand. Audit any component that touches audioQueue directly and confirm it routes through processAudioQueue().


Contributing

Branch strategy

main             production
develop          integration
feature/<name>   features
bugfix/<id>      bug fixes

Commit convention

feat: add waveform visualizer
fix: resolve audio overlap on rapid POI
docs: update WebSocket protocol table
refactor: extract audio queue into module-scope

Pull request checklist

  • npm run lint passes
  • npm run build succeeds
  • No console.log statements remain
  • Screenshots attached for UI changes

Further Reading


Built with ⚡ by the Agora team

Report a bug · Request a feature · Watch the demo

About

The real-time debate arena. A voice-first, streaming interface where a human debates a competitive AI opponent under formal parliamentary rules, with WUDC-style adjudication delivered live.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors