Component-Based Architecture
VoxDiff uses a component model where every feature is a standalone module with declared dependencies. Components are initialized in strict order, ensuring lower-level components (database, encryption) are available before higher-level ones (comparison, tier management) depend on them.
Initialization Layers
Layer 1: Foundation → Encryption, Utils
Layer 2: Database → DB, DB_Tables
Layer 3: Core Features → Settings, Cache, Tier_Manager, Email_Verify, Stripe, Rate_Limiter
Layer 4: LLM → LLM_Manager, Prompts
Layer 5: Comparison → Comparison (core feature)
Layer 6: Application → Persona, Chat
Layer 7: UI → Shortcodes, Assets, AJAX
Each layer depends on layers below it. This guarantees:
- Database is available before tier manager tries to query user_tiers
- Encryption is available before storage components encrypt data
- LLM_Manager is available before judge LLM runs
Database Schema: Multi-Tenant Isolation
All custom tables use the voxdiff_ prefix and include user_id (or session_id for anonymous) as a foreign key.
Comparison Tables
CREATE TABLE wp_voxdiff_comparisons (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
share_uuid VARCHAR(36) UNIQUE,
global_system_prompt LONGTEXT,
temperature DECIMAL(3,2),
max_tokens SMALLINT UNSIGNED,
top_p DECIMAL(3,2),
turn_count TINYINT UNSIGNED,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES wp_users(ID) ON DELETE CASCADE,
KEY (user_id),
UNIQUE KEY (share_uuid)
);
CREATE TABLE wp_voxdiff_comparison_lanes (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
comparison_id BIGINT UNSIGNED NOT NULL,
lane_index TINYINT UNSIGNED,
provider VARCHAR(32),
model VARCHAR(64),
label VARCHAR(128),
FOREIGN KEY (comparison_id) REFERENCES wp_voxdiff_comparisons(id) ON DELETE CASCADE,
KEY (comparison_id)
);
CREATE TABLE wp_voxdiff_comparison_turns (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
lane_id BIGINT UNSIGNED NOT NULL,
turn_number TINYINT UNSIGNED,
role VARCHAR(16),
content LONGTEXT,
input_tokens INT UNSIGNED,
output_tokens INT UNSIGNED,
latency_ms INT UNSIGNED,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (lane_id) REFERENCES wp_voxdiff_comparison_lanes(id) ON DELETE CASCADE,
KEY (lane_id, turn_number)
);
CREATE TABLE wp_voxdiff_comparison_analysis (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
comparison_id BIGINT UNSIGNED NOT NULL,
turn_number TINYINT UNSIGNED,
analysis_json LONGTEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (comparison_id) REFERENCES wp_voxdiff_comparisons(id) ON DELETE CASCADE,
KEY (comparison_id, turn_number)
);
Multi-tenancy via foreign keys:
- A user can only access comparisons where
user_id = their_id - Each comparison has lanes; each lane has turns
- No query can leak another user’s data (FK constraints + authorization checks)
Tier & Token Tables
CREATE TABLE wp_voxdiff_user_tiers (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL UNIQUE,
tier ENUM('red_cup','open_bar','cash_bar','run_a_tab') DEFAULT 'open_bar',
token_balance BIGINT UNSIGNED DEFAULT 0,
tokens_allotted BIGINT UNSIGNED DEFAULT 1000000,
period_start DATETIME,
period_end DATETIME,
stripe_customer_id VARCHAR(64),
stripe_subscription_id VARCHAR(64),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES wp_users(ID) ON DELETE CASCADE,
KEY (user_id),
KEY (tier),
KEY (stripe_customer),
KEY (stripe_subscription)
);
CREATE TABLE wp_voxdiff_anonymous_tiers (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
session_id VARCHAR(64) NOT NULL UNIQUE,
ip_address VARCHAR(45) NOT NULL,
token_balance BIGINT UNSIGNED DEFAULT 1000000,
tokens_consumed BIGINT UNSIGNED DEFAULT 0,
first_seen DATETIME DEFAULT CURRENT_TIMESTAMP,
last_active DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY (session_id),
KEY (ip_address),
KEY (last_active)
);
CREATE TABLE wp_voxdiff_token_ledger (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT UNSIGNED SIGNED,
session_id VARCHAR(64),
event_type ENUM('grant','debit','refund','admin_adjustment','expiry'),
tier ENUM('red_cup','open_bar','cash_bar','run_a_tab'),
tokens_delta BIGINT SIGNED,
balance_after BIGINT UNSIGNED,
reference_type VARCHAR(50),
reference_id VARCHAR(64),
note TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
KEY (user_id),
KEY (session_id),
KEY (event_type),
KEY (created_at),
KEY (reference_type, reference_id)
);
CREATE TABLE wp_voxdiff_admin_overrides (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
override_tier ENUM('red_cup','open_bar','cash_bar','run_a_tab'),
bonus_tokens BIGINT UNSIGNED DEFAULT 0,
exempt_rate_limits TINYINT(1) DEFAULT 0,
note TEXT,
set_by BIGINT UNSIGNED NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES wp_users(ID) ON DELETE CASCADE,
FOREIGN KEY (set_by) REFERENCES wp_users(ID) ON DELETE CASCADE,
KEY (user_id)
);
Tier Resolution Logic
When a request arrives, the server determines the user’s effective tier in this order:
public function resolve_tier(?int $user_id, ?string $session_id): string {
// Step 1: Anonymous user?
if (!$user_id) {
return 'red_cup'; // Session-scoped, no account
}
// Step 2: Admin override?
$override = $this->get_admin_override($user_id);
if ($override && $override->override_tier) {
return $override->override_tier;
}
// Step 3: Email verified?
$is_verified = get_user_meta($user_id, 'voxdiff_email_verified', true);
if (!$is_verified) {
return 'red_cup'; // Registered but unverified
}
// Step 4: Check tier_manager row (may have subscription or cash bar)
$tier_row = $this->get_user_tier($user_id);
if (!$tier_row) {
return 'open_bar'; // Default for verified users with no other tier
}
// Step 5: Subscription active?
if ($tier_row->tier === 'run_a_tab' && $this->is_subscription_active($tier_row)) {
return 'run_a_tab';
}
// Step 6: Cash bar tokens remaining?
if ($tier_row->tier === 'cash_bar' && $tier_row->token_balance > 0) {
return 'cash_bar';
}
// Step 7: Default to open_bar if verified
return 'open_bar';
}
The result is the user’s effective tier, which gates feature access.
Comparison Creation Gate Sequence
When voxdiff_comparison_create AJAX is called:
public function handle_comparison_create() {
// 1. Verify nonce
if (!wp_verify_nonce($_POST['comparison_nonce'], 'voxdiff_comparison')) {
wp_send_json_error(['code' => 'invalid_nonce']);
}
// 2. Resolve identity
$user_id = get_current_user_id();
$session_id = $user_id ? null : $this->get_or_create_session_id();
// 3. Resolve tier
$tier = $this->tier_manager->resolve_tier($user_id, $session_id);
// 4. Check rate limit
if (!$this->rate_limiter->check_rate_limit($tier, 'comparison')) {
wp_send_json_error(['code' => 'rate_limit_exceeded'], 429);
}
// 5. Validate lane count
$lane_count = count($_POST['lanes']);
$max_lanes = $this->tier_manager->max_lanes($tier);
if ($lane_count > $max_lanes) {
wp_send_json_error(['code' => 'lane_limit_exceeded', 'max_lanes' => $max_lanes]);
}
// 6. Validate max_tokens (tier ceiling)
$max_tokens = (int)$_POST['max_tokens'];
$tier_ceiling = $this->tier_manager->max_tokens_ceiling($tier);
$max_tokens = min($max_tokens, $tier_ceiling);
// 7. Validate model access (premium models)
foreach ($_POST['lanes'] as $lane) {
if ($this->is_premium_model($lane['model'])) {
if (!$this->tier_manager->can_use_premium($tier)) {
wp_send_json_error(['code' => 'premium_model_restricted']);
}
}
}
// 8. Validate persona min_tier requirements
foreach ($_POST['lanes'] as $lane) {
if (!empty($lane['persona_id'])) {
$persona = $this->persona->get($lane['persona_id']);
if (!$this->tier_manager->tier_meets($tier, $persona->min_tier)) {
wp_send_json_error(['code' => 'persona_tier_restricted']);
}
}
}
// 9. Estimate tokens & check balance
$estimate = $this->tier_manager->estimate_request_tokens(
$_POST['prompt'],
$lane_count,
$max_tokens,
$_POST['system_prompt'] ?? ''
);
$balance = $this->tier_manager->get_balance($user_id, $session_id);
if ($balance < $estimate) {
wp_send_json_error(['code' => 'insufficient_tokens', 'balance' => $balance], 402);
}
// 10. Create session in DB
$comparison_id = $this->comparison->create_session(
$user_id,
$_POST['lanes'],
['temperature' => $_POST['temperature'], 'max_tokens' => $max_tokens]
);
// 11. Generate stream tokens (one per lane, one-time use)
$stream_tokens = [];
foreach ($lanes as $lane) {
$token = $this->generate_stream_token($comparison_id, $lane['id']);
$stream_tokens[$lane['id']] = $token;
}
// 12. Return success
wp_send_json_success([
'comparison_id' => $comparison_id,
'lanes' => $lanes,
'stream_tokens' => $stream_tokens
]);
}
Each step must pass before the next one runs. If any check fails, an error is returned immediately.
Streaming Architecture: SSE (Server-Sent Events)
Stream Endpoint
includes/stream-comparison.php is a lightweight PHP file that:
- Boots WordPress minimally (
require wp-load.php) - Validates the stream token
- Calls the LLM provider
- Emits SSE events
<?php
// Boot WordPress
define('SHORTINIT', false);
require_once dirname(__DIR__, 4) . '/wp-load.php';
// Validate stream token
$token = $_GET['stream_token'] ?? '';
$comparison_id = (int)$_GET['comparison_id'] ?? 0;
$lane_id = (int)$_GET['lane_id'] ?? 0;
// Recompute HMAC and check transient
if (!$this->validate_stream_token($token, $comparison_id, $lane_id)) {
header('HTTP/1.1 403 Forbidden');
exit;
}
// Delete transient (one-time use)
delete_transient("voxdiff_stream_{$comparison_id}_{$lane_id}");
// Fetch lane configuration
$lane = $this->db->query("SELECT * FROM voxdiff_comparison_lanes WHERE id = %d", $lane_id);
$comparison = $this->db->query("SELECT * FROM voxdiff_comparisons WHERE id = %d", $comparison_id);
// Fetch conversation history for this lane
$history = $this->comparison->get_lane_history($lane_id);
// Prepare system prompt
$system_prompt = $comparison->global_system_prompt;
// Set SSE headers
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no'); // Disable nginx buffering
// Call LLM with streaming callback
$usage = $this->llm_manager->call_provider_streaming(
$lane->provider,
$lane->model,
$history,
[
'temperature' => $comparison->temperature,
'max_tokens' => $comparison->max_tokens,
'system' => $system_prompt
],
function($chunk) {
// Emit chunk as SSE event
echo "data: " . json_encode(['chunk' => $chunk]) . "nn";
flush();
}
);
// Emit final event with token counts
echo "data: " . json_encode([
'done' => true,
'input_tokens' => $usage['input_tokens'],
'output_tokens' => $usage['output_tokens'],
'latency_ms' => $usage['latency_ms']
]) . "nn";
flush();
Why a separate file?
- WordPress AJAX doesn’t support SSE natively
stream-comparison.phpcan set SSE headers without WordPress interfering- No nonce needed (stream tokens are one-time HMAC tokens)
- Minimal boot (no plugins, no admin, just WordPress core)
Stream Token Validation
Stream tokens are generated before streaming starts and validated on arrival:
// Generate (in comparison_create handler)
$token_data = "{$comparison_id}:{$lane_id}:{$turn_number}";
$token = hash_hmac('sha256', $token_data, VOXDIFF_STREAM_SECRET);
set_transient("voxdiff_stream_{$comparison_id}_{$lane_id}", $token, 60); // 60-second expiry
// Validate (in stream-comparison.php)
public function validate_stream_token(string $provided_token, int $comparison_id, int $lane_id): bool {
$token_data = "{$comparison_id}:{$lane_id}:{$turn_number}";
$expected_token = hash_hmac('sha256', $token_data, VOXDIFF_STREAM_SECRET);
// Fetch transient (returns false if expired)
$stored = get_transient("voxdiff_stream_{$comparison_id}_{$lane_id}");
return hash_equals($stored, $provided_token); // Constant-time comparison
}
Benefits:
- No nonce parameter (EventSource can’t send POST bodies or custom headers)
- One-time use (transient deleted after validation)
- Time-limited (60-second expiry)
- Can’t be reused for other lanes
Token Adapter Pattern
Each LLM provider has unique SSE chunk formats. The adapter pattern normalizes these:
LLM_Adapter_Interface
interface LLM_Adapter_Interface {
// Format user messages into provider-specific format
public function format_messages(array $messages): array;
// Parse an SSE chunk and extract tokens/text
public function parse_sse_chunk(string $line): ?array; // Returns {text, input_tokens, output_tokens}
// Extract usage from final response
public function extract_usage(array $response): array; // Returns {input_tokens, output_tokens}
}
OpenAI_Adapter
class OpenAI_Adapter implements LLM_Adapter_Interface {
public function format_messages(array $messages): array {
// OpenAI: role = 'system' | 'user' | 'assistant'
return array_map(fn($msg) => [
'role' => $msg['role'],
'content' => $msg['content']
], $messages);
}
public function parse_sse_chunk(string $line): ?array {
if ($line === 'data: [DONE]') {
return null; // Stream finished
}
$data = json_decode(substr($line, 6), true); // Strip "data: " prefix
if ($data['choices'][0]['delta']['content'] ?? null) {
return ['text' => $data['choices'][0]['delta']['content']];
}
if ($data['usage'] ?? null) {
return [
'input_tokens' => $data['usage']['prompt_tokens'],
'output_tokens' => $data['usage']['completion_tokens']
];
}
return null;
}
}
Anthropic_Adapter
class Anthropic_Adapter implements LLM_Adapter_Interface {
public function format_messages(array $messages): array {
// Anthropic: system message separate from messages array
// Messages are only role='user' | role='assistant'
return array_filter($messages, fn($msg) => $msg['role'] !== 'system');
}
public function parse_sse_chunk(string $line): ?array {
$data = json_decode(substr($line, 6), true);
if ($data['type'] === 'content_block_delta') {
return ['text' => $data['delta']['text'] ?? ''];
}
if ($data['type'] === 'message_start') {
return ['input_tokens' => $data['message']['usage']['input_tokens']];
}
if ($data['type'] === 'message_delta') {
return ['output_tokens' => $data['delta']['usage']['output_tokens']];
}
return null;
}
}
Generic_LLM_Provider
class Generic_LLM_Provider extends Base_LLM_Provider {
public function call_streaming(
string $model,
array $messages,
array $options,
callable $on_chunk
): array {
// Get adapter for this provider (Anthropic, OpenAI, Gemini, etc.)
$adapter = $this->get_adapter();
// Format messages using adapter
$formatted_messages = $adapter->format_messages($messages);
// Prepare payload (including system message if applicable)
$payload = $this->build_payload($formatted_messages, $options, $adapter);
// Prepare HTTP client
$client = new GuzzleHttpClient();
$response = $client->post($this->api_base_url, [
'stream' => true,
'json' => $payload,
'headers' => $this->build_headers()
]);
// Stream and accumulate tokens
$body = $response->getBody();
$input_tokens = 0;
$output_tokens = 0;
while (!$body->eof()) {
$line = $body->read(1024);
$parsed = $adapter->parse_sse_chunk($line);
if ($parsed === null) {
break; // Stream finished
}
if (is_string($parsed)) {
// Text chunk
($on_chunk)($parsed);
} else if (is_array($parsed)) {
// Token data
$input_tokens += $parsed['input_tokens'] ?? 0;
$output_tokens += $parsed['output_tokens'] ?? 0;
}
}
return [
'content' => $assembled_text,
'input_tokens' => $input_tokens,
'output_tokens' => $output_tokens,
'latency_ms' => $elapsed_ms
];
}
}
Debit Handler: Atomic Token Accounting
After voxdiff_comparison_save_turn receives the final lane results and judge analysis:
public function handle_comparison_save_turn() {
// ... validation ...
// Aggregate tokens from all lanes + judge
$total_input = array_sum(array_column($_POST['lane_results'], 'input_tokens'));
$total_output = array_sum(array_column($_POST['lane_results'], 'output_tokens'));
$judge_input = $analysis['judge_input_tokens'] ?? 0;
$judge_output = $analysis['judge_output_tokens'] ?? 0;
$total_tokens = $total_input + $total_output + $judge_input + $judge_output;
// Atomic debit
$success = $this->tier_manager->debit_tokens(
$user_id,
$session_id,
$total_tokens,
'comparison_turn',
"{$comparison_id}_{$turn_number}"
);
if (!$success) {
// Balance went negative (race condition or estimation miss)
// Turn is already saved, account floors at 0
voxdiff_log("Token debit failed post-save for user {$user_id}", 'error');
}
// Log ledger entry
$this->tier_manager->log_ledger(
event_type: 'debit',
user_id: $user_id,
session_id: $session_id,
tokens_delta: -$total_tokens,
reference_type: 'comparison_turn',
reference_id: "{$comparison_id}_{$turn_number}"
);
// Return success with new balance
$new_balance = $this->tier_manager->get_balance($user_id, $session_id);
wp_send_json_success([
'turn_number' => $turn_number,
'analysis' => $analysis,
'tokens_debited' => $total_tokens,
'token_balance' => $new_balance
]);
}
Judge LLM Invocation
After all lanes finish streaming, the judge runs:
public function run_judge(int $comparison_id, int $turn_number, string $user_message, array $lane_results): ?array {
// Build judge prompt
$prompt = "You are an impartial judge...nn";
$prompt .= "USER PROMPT: $user_messagenn";
$prompt .= "RESPONSES:n";
foreach ($lane_results as $result) {
$prompt .= "--- {$result['lane_label']} ({$result['provider']}/{$result['model']}) ---n";
$prompt .= $result['content'] . "nn";
}
// Get judge persona/model from settings
$judge_config = get_option('voxdiff_judge_config', []);
$judge_provider = $judge_config['provider'] ?? 'openai';
$judge_model = $judge_config['model'] ?? 'gpt-4o-mini';
// Call judge (non-streaming, collect full response)
$response = $this->llm_manager->call_provider_streaming(
$judge_provider,
$judge_model,
[['role' => 'user', 'content' => $prompt]],
['temperature' => 0.0, 'max_tokens' => 400],
function() {} // Discard chunks; just collect full response
);
// Parse judge JSON
$analysis = json_decode($response['content'], true);
if (!$analysis) {
voxdiff_log("Judge response was not valid JSON", 'error');
return null;
}
// Save analysis
$this->comparison->save_analysis($comparison_id, $turn_number, $analysis);
// Return analysis with token metadata
$analysis['_judge_tokens'] = $response['input_tokens'] + $response['output_tokens'];
return $analysis;
}
Frontend Integration: JavaScript
ComparisonSession Class
class ComparisonSession {
constructor(config) {
this.config = config;
this.comparisonId = null;
this.lanes = [];
this.totals = { inputTokens: 0, outputTokens: 0, energyWh: 0, costUsd: 0 };
}
async create(laneConfigs, globalSettings) {
const response = await fetch(this.config.ajaxurl, {
method: 'POST',
body: new URLSearchParams({
action: 'voxdiff_comparison_create',
comparison_nonce: this.config.nonces.comparison,
lanes: JSON.stringify(laneConfigs),
settings: JSON.stringify(globalSettings)
})
});
const data = await response.json();
this.comparisonId = data.data.comparison_id;
this.lanes = data.data.lanes;
return data;
}
async sendTurn(userMessage) {
// Open N EventSources in parallel
const streams = this.lanes.map(lane =>
this.streamLane(lane.lane_id, userMessage)
);
const results = await Promise.all(streams);
return results;
}
streamLane(laneId, userMessage) {
return new Promise((resolve, reject) => {
const url = new URL(this.config.streamUrl);
url.searchParams.set('comparison_id', this.comparisonId);
url.searchParams.set('lane_id', laneId);
url.searchParams.set('turn_number', this.currentTurn);
url.searchParams.set('stream_token', this.streamTokens[laneId]);
const es = new EventSource(url.toString());
let assembled = '';
let startTime = Date.now();
es.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.chunk) {
assembled += data.chunk;
this.updateUI(laneId, assembled); // Live update
}
if (data.done) {
es.close();
const elapsed = Date.now() - startTime;
resolve({
lane_id: laneId,
content: assembled,
input_tokens: data.input_tokens,
output_tokens: data.output_tokens,
latency_ms: elapsed
});
}
};
es.onerror = (e) => {
es.close();
reject(e);
};
});
}
async saveTurn(turnNumber, userMessage, laneResults) {
const response = await fetch(this.config.ajaxurl, {
method: 'POST',
body: new URLSearchParams({
action: 'voxdiff_comparison_save_turn',
comparison_nonce: this.config.nonces.comparison,
comparison_id: this.comparisonId,
turn_number: turnNumber,
user_message: userMessage,
lane_results: JSON.stringify(laneResults)
})
});
const data = await response.json();
return data; // Includes analysis and new token balance
}
}
Rate Limiting: Sliding Window
Rate limits are enforced per-user per-tier using a sliding window:
public function check_rate_limit(string $tier, string $action): bool {
$user_id = get_current_user_id();
$cache_key = "voxdiff_rate_limit_{$user_id}_{$action}";
$limit = $this->limits[$tier][$action] ?? 100; // Default limit
$window = 3600; // 1 hour
$count = wp_cache_get($cache_key) ?? 0;
if ($count >= $limit) {
return false;
}
wp_cache_set($cache_key, $count + 1, '', $window);
return true;
}
Cache backend (Redis or WP_Object_Cache) automatically expires entries after the window.
Security: Nonce & CSRF Prevention
All AJAX actions use WordPress nonces. Stream tokens use HMAC.
// AJAX request (standard nonce)
wp_verify_nonce($_POST['comparison_nonce'], 'voxdiff_comparison')
// SSE stream (one-time HMAC token)
hash_equals(
hash_hmac('sha256', "{$comparison_id}:{$lane_id}:{$turn_number}", VOXDIFF_STREAM_SECRET),
$_GET['stream_token']
)
Next Steps for Developers
- Review
core/Plugin.phpfor initialization order - Study
core/Tier_Manager.phpfor tier resolution and token accounting - Examine
includes/Ajax.phpfor gate sequence examples - Check
core/Providers/Generic_LLM_Provider.phpfor streaming implementation