← Back to Blog
Technical Deep Dive December 2, 2025 9 min read

Live Events Optimization: 24-Hour Persistence and Time-Travel Replay

How a 6am coding session transformed pretty-but-useless event streaming into an actual intelligence tool—with IndexedDB persistence, multi-speed replay, and seamless session continuity.

The Problem: Pretty But Useless

Three days ago, we shipped Live Universe Events—real-time streaming of EVE Frontier blockchain activity via Cloudflare Durable Objects and WebSockets. Smart gate links, tribe formations, structure deployments, and fuel deliveries scrolling across the bottom of the map in a satisfying ticker. Visual halos and flashes highlighting where the action was happening.

It looked great. Users loved the aesthetic. But there was a fundamental problem: it was completely useless.

The Reality Check: Unless you happened to be staring at your browser at the exact moment something interesting happened, you'd miss it entirely. Refresh the page? Gone. Switch tabs for 10 minutes? Gone. Come back from making coffee? All that activity—gone.

Sure, there was a History panel where you could scroll through events. But who's realistically going to click through 500 entries one by one, trying to find the one interesting gate link that happened while they were AFK?

The ticker was a visualisation of activity, not an intelligence tool. And that wasn't good enough.

The 6am Epiphany

Sometimes the best features come from personal frustration. Woke up at 6am—two hours before I needed to—because it was bugging me. The architecture was solid (Durable Objects, WebSocket hibernation, event filtering), but the user experience had a critical gap: no persistence.

Two questions crystallized the solution:

  1. What if events survived a browser refresh?
  2. What if you could replay what you missed—at any speed?

90 minutes later, both were in production.

Solution 1: IndexedDB Persistence

Browser localStorage has a ~5MB limit and is synchronous (blocks the main thread). For potentially thousands of events with full metadata, we needed something more capable.

IndexedDB is the browser's native NoSQL database—asynchronous, transactional, and can handle hundreds of megabytes. Perfect for event storage.

The Lazy Loading Challenge

The naive approach would be: wait for IndexedDB to load, then show the UI. But that creates a noticeable delay on page load, and users might think something's broken.

Instead, we implemented lazy loading with seamless merging:

  1. Page loads instantly—WebSocket connects, events start arriving
  2. IndexedDB loads in background—no blocking, no loading indicators
  3. Events merge seamlessly—when DB finishes loading, historical events merge with newly-arrived events
  4. User never notices—history just "appears" to grow as you use the page
// Simplified lazy loading pattern
useEffect(() => {
  // Start SSE immediately - don't wait for IndexedDB
  connectToEventStream();
  
  // Load history in background
  loadAllEvents().then(storedEvents => {
    // Merge with any events that arrived while loading
    setEventHistory(prev => mergeAndDedupe(prev, storedEvents));
  });
}, []);

24-Hour Rolling Window

We considered various retention strategies:

Strategy Pros Cons
Fixed count (500 per type) Predictable memory Loses temporal context
7-day window Long history Potentially huge storage
24-hour window Relevant context, reasonable size

24 hours hit the sweet spot. At EVE Frontier's typical activity rate (~80,000 events/day), that's approximately 48MB—well under browser quotas, and enough to see "what happened overnight" without storing ancient history nobody cares about.

~48 MB
Maximum storage for 24 hours of events
(~600 bytes × 80,000 events)

Automatic Cleanup

Events older than 24 hours get pruned automatically:

Solution 2: Time-Travel Replay

Persistence solved the "events disappear on refresh" problem. But reading through a list of 500 events is still tedious. What if you could watch what happened?

Multi-Speed Playback

Real-time replay would take hours. Nobody has time for that. So we built speed controls:

10× 50× 100× 500× Max

At 50×, an hour of activity plays back in ~72 seconds. At 500×, it's under 8 seconds. "Max" fires events as fast as the browser can render them.

Time-Proportional Playback

Here's the key insight: events aren't evenly distributed. There are bursts of activity (someone links 10 gates in quick succession) and quiet periods (3am downtime). If we played events at fixed intervals, we'd lose that temporal texture.

Instead, replay preserves the relative timing:

// Time-proportional playback
const realTimeGapMs = nextEvent.timestamp - currentEvent.timestamp;
const scaledDelayMs = Math.min(5000, Math.max(10, realTimeGapMs / speed));
await sleep(scaledDelayMs);

A burst of 10 events that happened in 2 seconds still feels like a burst during replay. A 30-minute quiet period compresses to seconds but still feels like a pause. The rhythm of activity is preserved.

Visual Integration

During replay, events trigger the same visual effects as live events:

You can literally watch the EVE Frontier universe evolve—see gate networks form, structures deploy, tribes grow—in accelerated time.

Implementation Details

IndexedDB Schema

The database structure is intentionally simple:

// Database: ef_event_history
// Store: events
{
  id: auto-increment,
  eventType: string,      // 'gate_linked', 'structure_online', etc.
  timestamp: number,      // Unix ms - indexed for time-based queries
  text: string,           // Formatted display text
  systemId: number,       // For clickable system links
  systemName: string,     // Resolved name
  event: {                // Raw event data for replay
    type, system_id, data, timestamp
  }
}

Two indexes enable efficient queries:

Graceful Degradation

IndexedDB isn't available in all contexts (private browsing, some mobile browsers, storage pressure). The system handles this gracefully:

export async function initEventHistoryDB(): Promise<IDBDatabase | null> {
  if (!window.indexedDB) {
    console.warn('[eventHistoryDB] Not available, using memory-only');
    return null;
  }
  // ... initialization
}

If IndexedDB fails, events still work—they just don't persist across sessions. No errors, no broken UI, just reduced functionality.

Event Counter Consistency

A subtle UX issue emerged during testing: the event counter next to "LIVE" showed events received this session, but the History panel showed all persisted events. After a refresh, these numbers would diverge confusingly.

The fix was simple but important: calculate the counter from total history rather than WebSocket count:

// Before: only counts this session's WebSocket events
const { eventCount } = useUniverseEvents({ ... });

// After: counts all events in history (persisted + live)
const totalEventCount = useMemo(() => {
  let count = 0;
  eventHistory.forEach(items => count += items.length);
  return count;
}, [eventHistory]);

Now the counter reflects reality: "6 events available" means 6 events you can actually see and replay.

The User Experience

Here's what the optimized flow looks like:

  1. First visit—events arrive live, build up in history, persist to IndexedDB in background
  2. Leave and return—page loads instantly, WebSocket reconnects, IndexedDB history merges in
  3. Check History—see all events from the last 24 hours, not just current session
  4. Click Replay—watch events play back at your chosen speed, halos lighting up the map
  5. Close laptop overnight—come back next day, events from yesterday are still there
The Transformation: What was a "looks cool but I'll never use it" feature became a genuine intelligence tool. You can now answer questions like "what happened in my region while I was asleep?" or "show me all the gate activity from the last 6 hours."

Performance Characteristics

Metric Value
Page load impact None (lazy loading)
IndexedDB load time ~50-200ms for full history
Event save latency <5ms (async, non-blocking)
Storage per event ~600 bytes
Max storage (24h) ~48MB
Replay overhead Negligible (reuses existing render path)

What's Next

With persistence and replay in place, several enhancements become possible:

The foundation is now solid. Events persist, replay works, and the architecture can support whatever intelligence features make sense next.

Lessons Learned

A few takeaways from this 90-minute sprint:

  1. Pretty isn't enough—if users can't act on information, it's decoration
  2. Lazy loading is underrated—async DB loading with seamless merging feels magical
  3. Preserve temporal texture—time-proportional replay is more informative than fixed-speed
  4. Graceful degradation—always have a fallback when browser APIs might fail
  5. Fix UX inconsistencies immediately—mismatched counters erode trust

Sometimes the best coding sessions happen at 6am when you can't sleep because something's bugging you. Now the EVE Frontier map doesn't just show you what's happening—it shows you what you missed.