The Morphine Vision
A few weeks ago, I had shoulder surgery. The kind where they give you the good painkillers.
I was lying in recovery, half-conscious, phone in hand, doomscrolling through news apps like always. Ukraine. Gaza. Taiwan tensions. Tariffs. Protests somewhere. It all blurred together, an endless vertical feed of headlines competing for my drugged-up attention.
And then I had... a vision.
Not a religious experience. Just morphine doing morphine things. But I saw it clearly: a globe. Not a feed. A 3D spinning globe where events appeared as glowing dots. Red for military. Blue for diplomacy. Severity determined the glow intensity, not engagement metrics. No "celebrities did what?" anywhere in sight.
Like Civilization V, but for real news.
I wrote it down haphazardly in my notes app, one-handed: "war room news app globe civ style" and passed out.
When I woke up properly, I thought: wait, that's actually not a terrible idea.
Why I Needed This
I'd been getting into geopolitics, like many people lately, because the world has gotten... loud. Tariff wars, shifting alliances, actual wars. Keeping up was exhausting.
Here's what drove me nuts:
News apps are designed to keep you scrolling, not informed. "Celebrity X Did Y" gets the same visual weight as "Tanks Crossing Border." No hierarchy of significance.
Same story, 47 headlines. When something big happens, Reuters, BBC, AP, Al Jazeera, and CNN all report on it, each as a separate item. That's noise, not information.
No spatial context. I'd read about events in places I couldn't immediately visualize. Where exactly IS Gabon? How close is that to other recent events?
Too much work to get the basics. I didn't want to dive into articles just to find out who put tariffs on who. I wanted breadth, quick info, fast.
So I spent my recovery time building the thing.
Week 1: The War Room
Day 1-2: Getting the globe to render
Started with Next.js 16, React 19, and Mapbox GL for the 3D globe. I wanted that dark, atmospheric "war room" aesthetic, fog effects, star fields, glowing markers.
My first bug was embarrassingly basic. The globe just... didn't render. Blank screen.
After an hour: the parent container had height: 0. All children were position: absolute, and absolute children don't contribute to parent height.
Classic CSS. Modern frameworks don't save you from the fundamentals.
By end of day 2, I had a spinning 3D globe with colored dots. Events loaded from a local JSON file, 17 hand-crafted "fake" events. Looked cool. Also completely useless because the data was stale the moment I created it.
Day 3-4: The AI pipeline (a.k.a. burning money)
Static data wasn't going to cut it. I needed real news, enriched with AI to extract coordinates, categories (MILITARY, DIPLOMACY, ECONOMY, UNREST), severity on a 1-10 scale, and fallout predictions.
I built a Python worker that fetches ~150 articles from news sources, runs each through GPT to extract structured data, then groups related articles into single "incidents." When Reuters, BBC, and AP all report on the same missile strike, you see ONE event with three sources not three pins on the map.
This worked beautifully.
Then I woke up to insufficient_quota errors. Burned through my OpenAI credits in 3 days.
Day 5: Gemini migration
Migrating from OpenAI to Google Gemini took about a day. The APIs are similar enough that it was mostly find-and-replace.
The silver lining: I discovered a hybrid model strategy. Gemini 2.5 Flash-Lite for high-volume article classification (pennies per thousand). Gemini 2.5 Flash for synthesis and user-facing briefings (needs better reasoning). Cut costs by ~40-50%.
AI apps have a hidden cost cliff. Monitor your usage BEFORE you hit the wall.
The geocoding disaster
Here's a fun one: the smaller AI model was placing events in the completely wrong hemisphere.
"Al Udeid Air Base, Qatar" was showing up in Georgia, USA. "Middle East" was appearing in Nova Scotia, Canada.
Asking an LLM to do geopolitical analysis AND coordinate generation in one prompt is too much. It's like asking someone to write poetry while doing mental math.
The fix was a 3-tier geocoding system:
- Dictionary lookup first – I compiled 263 known geopolitical hotspots (Tehran, Gaza, Kyiv, etc.) with exact coordinates. No API call needed.
- Redis cache second – If we've geocoded "Natanz Nuclear Facility" before, use the cached result.
- Focused LLM call last – Only for truly unknown locations, with a dedicated single-task prompt.
Now Qatar is actually in Qatar.
Week 2: Everything broke
Mobile
I showed the desktop version to a friend. First question: "Does it work on my phone?"
It did not.
You can't just shrink a dashboard. A sidebar and hover tooltips don't work on a 390px screen. I needed a complete rethink.
I designed a three-phase bottom sheet I called the "Pilot's View":
- Scanner Mode (30%) – Quick headline scan, like a ticker
- Pilot Mode (55%) – Full event details with swipe navigation
- Analyst Mode (95%) – AI briefing chat interface
You're scanning for signals, then zooming in, then getting the full intelligence briefing.
Then iOS Safari entered the chat.
Fighting iOS Safari
This was my first time building a web app with mobile in mind. I had no idea what I was getting into.
100vh includes the browser chrome on iOS, so your content hides behind the Safari toolbar. Touch gestures conflict with native scroll. The keyboard opening doesn't consistently push content up.
I ended up building a custom useViewportHeight hook that calculates ACTUAL visible pixels. Native touch handlers that intercept gestures before the browser does. A whole system for detecting when the keyboard opens vs. closes.
None of this is in any tutorial. I found out by breaking things.
RSS > API
I was proud of my NewsAPI integration. Professional. Modern. API-first.
Then my first user (shout out to my mum) told me the events felt stale.
Turns out NewsAPI's free tier has a 15-60 minute delay. For a "real-time" dashboard, that's not real-time. That's yesterday's news.
So I went back to basics. RSS feeds.
23 feeds from major outlets, BBC, Al Jazeera, Guardian, Deutsche Welle, NPR, CNN, Sky News, NY Times, Washington Post, South China Morning Post, Times of Israel, and more. Free, unlimited, and actually real-time.
NewsAPI became a backup. RSS is the primary.
Sometimes the "old" technology is the right choice. RSS has been around for decades because it works.
The 901K Redis commands disaster
The morning after launch, I checked my Upstash Redis dashboard.
901,000 commands in 2 days.
The free tier gives you 10,000/day. I was at 144,000 per day per browser tab.
What happened
I'd built a "reactions" feature where users vote on events (Critical / Market Impact / Noise). Each event's reaction counts were stored in Redis.
My polling code looked innocent:
useEffect(() => {
const interval = setInterval(() => {
fetchReactions(); // One Redis call per visible event
}, 30000); // Every 30 seconds
}, []);
The math:
- 50 visible events × 30-second polling = 6,000 commands/hour
- Per browser tab
- Leave 3 tabs open for 2 days = 864,000 commands
Oops.
The fix
Switched to a single API endpoint that returns ALL reactions at once. Cache it on the edge. No polling just refresh when the tab gets focus.
99.9% reduction in Redis commands.
My first implementation was terrible. But I only discovered HOW terrible because I shipped and monitored.
The $0/month stack
This whole thing runs on free tiers:
| Service | What it does |
|---|---|
| Vercel | Frontend hosting |
| GitHub Actions | Worker cron (every 15 min) |
| Cloudflare R2 | Event data storage |
| Upstash Redis | Rate limiting, reactions |
| Gemini | AI enrichment & briefings |
| Tavily | Web search for the briefing feature |
Total monthly cost: ~$0
Won't scale to millions of users. But for a side project it's perfect and it forced good architecture decisions. Aggressive caching, batch operations, edge computing. Constraints breed creativity or whatever.
What I'd do differently
Mobile first. I designed desktop first and "made it responsive." Wrong move. The mobile experience needed completely different interaction patterns. If I'd started there, I'd have saved a week.
SSR from the start. The main page is entirely client-rendered. Retrofitting SSR into a codebase that assumed client-only is painful. Think about SEO early.
Cache more aggressively. Every API call, every computation, ask "does this need to be fresh?" My Redis disaster happened because I was polling data that barely changed.
Write tests. I'm at ~11,000 lines of TypeScript and 1,300 lines of Python with zero tests. It's fine until you break something during a refactor and spend an hour debugging what a test would have caught in seconds.
The secret weapon: the development log
Best decision I made: keeping a detailed plan.md file.
Every bug, every design decision -> documented. It's now 4,896 lines and still growing.
Here's an actual entry from debugging why clicking events wasn't working:
Root Cause: Classic stale closure bug with Mapbox event listeners:
- When the map initializes, click handlers are registered ONCE
- These handlers capture
onSingleEventClickin their closures- When
filteredEventschanges, the handler still has the OLD callback- Solution: Refs for click callbacks, updated in effects
That's the kind of context that's impossible to reconstruct a week later.
The log serves as a debugging aid ("Wait, didn't I solve this before?"), a way to onboard my future self after a break, and as it turns out -> article material. This post wouldn't exist without it.
It ships
Realpolitik is live at realpolitik.world.
There are bugs I know about and probably bugs I don't. But every morning now, I open the globe, check what's new, and feel slightly less overwhelmed by the state of the world.
Red dots pulse in conflict zones. Blue dots appear at diplomatic summits. I tap one, get a summary, see what the AI thinks happens next.
No doomscrolling. No engagement-optimized fear. Just: here's what's happening, here's where, here's why it matters.
That was the goal.
Try it: realpolitik.world
Code: github.com/iamjameskeane/realpolitik
Build the thing your drugged-up self scribbled in the notes app. It might actually work.








Top comments (0)