This document provides a comprehensive, FAANG-grade architectural analysis of the Agora Frontend repository built with Next.js and React. It details how the web application functions internally, how it interacts with the broader distributed system, and provides a step-by-step rebuilding guide.
The agora-frontend provides the visual interface, real-time client state management, and media-capture bridge for the Agora real-time debate platform. Where the Go Gateway handles multiplexing and the Python Engine processes intelligence, the Next.js frontend physically connects the human speaker to the cloud.
User Microphone (MediaRecorder API) → [ THIS NEXT.JS FRONTEND ] → WebSocket (BinaryBlobs) → Go Gateway → STT
Go Gateway (TTS Audio) → WebSocket (BinaryBlobs) → [ THIS NEXT.JS FRONTEND ] → User Speakers (Web Audio API)
- Real-time DOM Rendering: Subscribes dynamically to inbound Websocket JSON text tokens (
AI_TOKEN) and progressively displays them (Typewriter effect) via React state architectures. - Streaming Audio Ingress: Captures local device microphones and streams 250ms byte chunks through the active WSS connection without storing locally.
- Sequential Audio Egress: Receives and decodes inbound arrays of binary audio buffers. Uses the Web Audio API to create a queue, ensuring back-to-back AI voice playback without audio clipping.
- Session Authentication Hooks: Connects strictly to Supabase Auth. Appends JWT bearer signatures on both HTTPS API calls and WebSocket connection URLs.
- Inputs: User Interaction (Clicks, Microphones, Routing Context).
- Outputs: Authenticated JWT tokens, Rest POST payloads, live Websocket events.
middleware.ts: Next.js middleware trapping all ingress edge-requests. Confirms@supabase/ssrcontexts to block unauthenticated views to private routes.package.json/components.json: Node dependencies and component metadata.
page.tsx: Marketing landing page (/).auth/: Native login/signup layout routes redirecting tokens.debate/setup/: Configuration route posting debate schemas (Motion, Side, Format).debate/[matchId]/: The Live Arena. Contains thepage.tsxUI displaying the transcript and audio control dashboard.results/: End-of-match page callingGET /api/v1/debates/{id}/results.
arenaStore.ts: Global application state manager (Zustand). Encapsulates the entire complexity of the WebSocket connection and JSON buffer mapping ensuring React unmounts do not sever the active debate.
lib/api.ts: Consolidated fetch actions interacting directly withprocess.env.NEXT_PUBLIC_API_BASE_URL(The Go proxy!).lib/supabase/: Web clients executing JWT retrievals.
ui/: Standardized, tailwind-styled atom components (Buttons, Badges).providers/: Context wrappers (specificallyAuthProvider) syncing layout logic with Supabase verification.
The application adopts a Thick Client / Decoupled Socket pattern:
- Zustand Over Local State: The WebSocket lifecycle is disconnected from the React Component Lifecycle. By elevating the
socketandaudioQueueinstances to the ZustanduseArenaStore, navigating away from a UI element doesn't arbitrarily abort a live streaming connection or drop audio chunks mid-sentence. - Server-Side Routing / Client-Side Streaming: Uses SSR (Server Side Rendering) where beneficial (auth and dashboard pages) while isolating the
<LiveArenaPage>strictly to Client Components ("use client") due to dependencies on the browser's nativenavigator.mediaDevicescontext scope. - Framer-Motion Transitions: Employs mathematically modeled spring animations to shift the Document Object Model fluidly as AI text generates instead of allowing raw CSS heights to snap violently.
- Authenticated user clicks "Start Match" on
/debate/setup. lib/api.tsPOSTs to/api/v1/matches.- User is routed dynamically to
/debate/[matchId]. useArenaStore.connect()is fired withinuseEffect, passing the match ID and JWT.- Socket
onopenautomatically publishes{"action": "START_MATCH"}into the websocket immediately.
- Store intercepts incoming string.
JSON.parseidentifies{"event": "AI_TOKEN", "text": "word"}. - Store mutates state:
aiBufferedText += "word". - React evaluates state change and injects DOM updates natively inside the Framer Motion shimmer blocks.
- Concurrently, the WebSocket yields a
Blob. - The
onmessagehook extracts the.arrayBuffer()and pushes it toaudioQueue. - Store triggers
processAudioQueue(), which leverageswindow.AudioContextto construct buffer playback sequential links to system speakers.
- User clicks the Mic Button (
toggleMic()). - Frontend triggers browser hardware prompt
navigator.mediaDevices.getUserMedia. - Initiates
MediaRecordertargetingaudio/webm. Streams data availability at 250ms chunks (recorder.start(250)). - Forwards exact chunks recursively over the socket via
socket.send(e.data). - Human issues POI via "Offer POI" button. Triggers
window.promptand fires JSON{"action": "POI_OFFERED", "text": "..."}.
- User taps "End Turn".
MediaRecorderhalts track processes natively to free hardware binding. - Websocket blasts
{"action": "END_TURN"}JSON command back to the Go Gateway tracking service.
Strict Interface Typings (arenaStore.ts):
export interface ArenaEvent {
event: EventType;
speaker?: "ai" | "human";
text?: string;
}State flows perfectly top-down. The React UI never commands the state machine directly without using encapsulated actions (sendEvent, addTranscriptEntry).
The transcript: TranscriptEntry[] array maintains full history for rendering, while aiBufferedText specifically scopes the volatile real-time generation text.
Web Audio Context Processing: The web browser needs a rigorous audio pipeline because chunked AI voice pieces overlap. The store uses linked promises:
processAudioQueue: () => {
// Escape if busy or empty
if (state.isPlayingAudio || state.audioQueue.length === 0) return;
set({ isPlayingAudio: true });
// Decode and route directly to speakers
audioCtx.decodeAudioData(buffer, (decodedData) => {
// ...
source.onended = () => { // Pop array, recursive chain call
set((s) => ({ audioQueue: s.slice(1), isPlayingAudio: false }));
get().processAudioQueue();
};
source.start();
})
}This is a FAANG-grade approach. It guarantees smooth TTS synthesis playback to the user, completely decoupling Network-Time from Play-Time.
- Go Gateway API: Targets
NEXT_PUBLIC_API_BASE_URL/api/v1/...for standard REST payloads. Triggers Nginx/Gateway rules correctly linking out to the backend. - Go Gateway WSS: Targets
NEXT_PUBLIC_WS_BASE_URL/ws/livebridging directly to the handler engine. - Supabase Edge Services: Validates users natively at edge-level inside
middleware.tswithout incurring costly application re-renders.
The most critical interaction handler:
navigator.mediaDeviceschecks browser policies (Requires HTTPS locally or production!).- Initializing
MediaRecorderwithmimeType: "audio/webm"strictly standardizes encoder structures avoiding Deepgram confusion. - The hook
recorder.ondataavailableacts as an event trap. It executes continuously in the background parsinge.databinaries directly to the raw binary websocket. - The hook stops all parent tracks precisely upon manual termination, protecting user privacy limits.
- Zustand over Context: Scaling contexts for 10ms-delta updates crushes React performance. Zustand writes to isolated store memory outside the React loop, binding via custom hooks exactly where needed.
- Next.js Server-Side Middleware: Traps token anomalies centrally before Next.js even begins shipping massive Javascript bundles to users.
- Graceful DOM Unmounting:
useEffectcleanup hook functions correctly executedisconnect()mitigating zombie connections dangling on Chrome instances.
- Bottlenecks/Risks:
- The Web Audio context API occasionally enters a suspended state on browser auto-play policies (Safari heavily restricts audio until the DOM receives a valid physical click event natively triggering context). If a match starts on AI's turn immediately, Safari might legally silence the audio buffers.
- The
MediaRecorderutilizesaudio/webmstrictly. iOS Safari notoriously lacks native generic WebM encoding capabilities inside theMediaRecorderscope without specific shims, which might break ingestion on iPhones.
- Error Handling: Store errors (
[Arena] Unknown message:) are logged lightly viaconsole.warnbut do not bubble up into visual toast notifications. If an Audio block decode structurally fails, error recovery is brute-forceaudioQueue.slice(1).
If you need to replicate this repository's behavior from a blank NextJS directory:
npx create-next-app@latest- Configure Tailwind CSS and add
framer-motion,zustand,lucide-react, and@supabase/ssr.
- Configure
lib/supabasecontext fetcher. - Build
middleware.tsat the root intercepting invalid cookies. - Build
providers/AuthProviderexposing the.getUser()globally to all deep sub-routes.
- Build
store/arenaStore.tsdefining JSON payload interfaces. - Scaffold WebSocket handlers mapping arrays of events against local properties (
transcript,audioQueue). - Deploy the recursive audio queue extraction model using
audioCtx.decodeAudioData.
- Bind
.envdefinitions mapping Go reverse-proxies. - Implement synchronous REST fetch triggers inside
lib/api.ts(ex.createMatch).
- Scaffold
app/debate/[matchId]/page.tsx. Provide heavy Tailwindabsoluteblobs mapping blurred background gradients. - Bind transcript arrays referencing
framer-motion<motion.div>objects enabling layout springs when arrays grow. - Deploy
MediaRecorderAPIs intercepting user clicks insidetoggleMic(). - Polish with loading states testing component responsiveness across desktop and mobile structures.