CASE · 012024 — ONGOINGSOLOREALTIME · MULTILINGUAL
◦ AZADIFEED · azadifeed.com

A crisis feed. In three languages. In seconds.

Kuratierte, KI-übersetzte Echtzeit-News für die iranische Diaspora. RSS-In, Telegram-Out, volles RTL, unter 50 €/Monat.

§ 01Problem · motivation

Why this exists.

Keine zentrale, mehrsprachige Stelle für verifizierte Breaking News während Protesten, Hinrichtungswellen und Internet-Shutdowns.

Für die iranische Diaspora — und Menschen im Iran selbst — gibt es während Krisenereignissen keine zentrale, mehrsprachige Stelle, die verifizierte Nachrichten in Echtzeit bündelt. Relevante Meldungen verteilen sich über Dutzende persischer Telegram-Kanäle, Twitter-Accounts und westliche Medien mit Verzögerung.

Bestehende Newsportale sind entweder nur auf Farsi (für die zweite Diaspora-Generation nicht lesbar), nur auf Englisch/Deutsch (ohne persischen Kontext) oder reine Aggregatoren ohne Krisen-Klassifikation und Push. AzadiFeed kuratiert RSS-Quellen automatisiert, übersetzt KI-gestützt in EN / DE / FA und pusht Breaking News in Sekunden an Telegram-Abonnenten mit Stadt-Filter.

§ 02Constraints · operating box

The box it had to fit in.

Jede Architektur-Entscheidung musste gegen diese Constraints geprüft werden.
C/01 · OPS
Solo-Dev, nebenberuflich — alles muss ohne Ops-Team wartbar sein.
C/02 · BUDGET
Hard Cap: < 50 €/Monat — Hosting + OpenAI + selbst gehostete MongoDB.
C/03 · LATENCY
RSS-Meldung → Telegram-Push in < 2 Minuten, inkl. Triple-Translation + Crisis-Scoring.
C/04 · LANGUAGES
EN / DE / FA mit vollständigem RTL-Layout — Vazir-Font, mixed LTR in Zitaten/URLs.
C/05 · MODERATION
Auto-Publish nur über Crisis-Schwellwert. Alles darunter in manuelle Queue — False Positives wären politisch heikel.
C/06 · TRAFFIC
Spiky — 95 % der Zeit <100 Visits/h, bei Krisenereignissen 50–100× Spike innerhalb Minuten.
C/07 · PRIVACY
IP-Hashing, 24h-Retention für Crisis-Reports, GDPR-konform für deutsche Diaspora.
C/08 · OFFLINE
PWA mit Service Worker — Nutzer im Iran mit instabiler Verbindung lesen gecachte Meldungen weiter.
§ 03Architecture · system topology

How it runs.

Coolify + Docker auf eigenem VPS. MongoDB lokal mitgehostet. OpenAI als Übersetzungs-/Classification-Layer. Socket.IO und Telegram als Sinks.
pipeline.live·tick 00:00·interval 5–10m
published/24h 0·p50 latency
SOURCE · 01
RSS · N sources
persian + intl media
fetched0
CRON · 02
rss-sync · 5–10m
setInterval · self-hosted
LOCK · 03
cron-lock · atomic
prevents double-translate
held2.4s
QUEUE · 04
pending_feeds
MongoDB · in-flight
in-flight0
TRANSLATE · 05
OpenAI · fa→en/de
nano · mini · 4.1
nanomini4.1
CLASSIFY · 06
Crisis-relevance
auto-publish gate
IMG · 07
cover scraper
og:image · twitter:image · fallback
cached82%
STORE · 08
published_feeds
MongoDB · indexed
total0
MODERATE · 09
manual queue
below crisis threshold
pending12
SOCKET · 10
Socket.IO
web clients · realtime
connected0
CITY · 11
city filter
TG channel routing
channels14
TG · 12
Telegram bot
city-targeted push
pushed0
EVENT LOG · /api/cron · /api/publish · MongoDB writes
12:04:18crontick · interval 7m · lock acquired 2.4s
12:04:14translateopenai mini · fa→en · 0.74s · 312 tokens
12:04:12classifycrisis-score 0.87 · auto-publish ✓
12:04:09imgcover · og:image · 280×180 · cached
12:04:07tgpush · #city-tehran · 2,180 subs · deduped
12:04:04socket.iobroadcast · 142 connected · room:home
12:04:01classifycrisis-score 0.34 · → moderate queue
12:03:57rssfetched · BBC Persian · 18 new items
12:03:52cityfilter · 14 channels · 8 matches
12:04:18crontick · interval 7m · lock acquired 2.4s
12:04:14translateopenai mini · fa→en · 0.74s · 312 tokens
12:04:12classifycrisis-score 0.87 · auto-publish ✓
12:04:09imgcover · og:image · 280×180 · cached
12:04:07tgpush · #city-tehran · 2,180 subs · deduped
12:04:04socket.iobroadcast · 142 connected · room:home
12:04:01classifycrisis-score 0.34 · → moderate queue
12:03:57rssfetched · BBC Persian · 18 new items
12:03:52cityfilter · 14 channels · 8 matches
§ 04Decisions · trade-offs

Four deliberate choices.

Pro Entscheidung: was gewählt, statt was, warum.
D/01

Coolify + Docker statt Vercel.

chosen
Self-hosted Coolify auf VPS · interner setInterval-Scheduler in src/lib/scheduler.ts
instead of
Vercel mit vercel.json Cron Jobs
reason
Vercel-Cron ist auf Free Tier 1×/Tag limitiert, Pro kostet 20 €/Monat. Eigener VPS erlaubt 5–10-Min-Intervalle + Socket.IO ohne Function-Timeouts + keine Kaltstarts bei Traffic-Spikes.
D/02

MongoDB als Queue, statt Redis.

chosen
Zwei Collections (pending_feedspublished_feeds) mit aiProcessed: false-Flag als Work-Queue, Distributed-Lock via Mongo-Dokument
instead of
Redis + BullMQ oder SQS
reason
MongoDB lief sowieso. Eine weitere Infrastruktur-Komponente hätte das Budget gesprengt und die Ausfall-Oberfläche verdoppelt. Für 100–500 Items/Tag reicht Polling + Lock.
D/03

OpenAI Multi-Model-Routing zur Laufzeit.

chosen
Modell-Auswahl per Admin-Setting (gpt-5-nano / mini / 4.1); Konfig in src/lib/openai-config.ts
instead of
Hardcoded gpt-4o auf allen Items
reason
80 % der RSS-Items sind Routine (Wetter, Wirtschaft, Sport) — nano reicht für Klassifikation + Translation und kostet ~1/30 von 4.1. Bei Krisen-Eskalation schaltet der Admin live auf 4.1 — ohne Deploy.
D/04

Socket.IO statt SSE oder Pusher.

chosen
Socket.IO via Pages-Router-Mount (src/pages/api/socket.ts) am eingebauten Next.js-Server — gleicher Host wie die API, optionaler Redis-Adapter für horizontale Skalierung
instead of
Server-Sent Events oder Pusher / Ably
reason
Pusher kostet ab 49 $/Monat bei >100 concurrent connections. Socket.IO braucht kein drittes System, hat built-in Reconnect-Logik und erlaubt später bidirektionale Admin-Moderation-Events ohne Umbau.
§ 05Highlights · interesting bits

Things that were not obvious.

Edge-Cases und Details, die erst beim Bauen klar wurden.

RTL mit mixed LTR-Segmenten

H/01
Persische Artikel enthalten regelmäßig lateinische Eigennamen, URLs, Hashtags (#MahsaAmini) und Zahlen. Naive RTL-Setups brechen das Layout, weil Browser den Bidi-Algorithmus inkonsistent auf gemischte Strings anwenden.

Aktueller Stand: <html dir="rtl"> per Locale, Vazirmatn als Variable-Font mit unicode-range-Fallback auf Inter für lateinische Glyphen — der Browser-Bidi-Algorithmus (UAX #9) übernimmt den Rest. Trägt Mainstream-Cases. Auf der Roadmap: unicode-bidi: plaintext plus explizites Unicode-Wrapping \u202B…\u202C der OpenAI-Outputs — Bidi gehört in die Content-Pipeline, nicht ins Stylesheet.

Distributed Lock via MongoDB-Dokument

H/02
Der Coolify-Scheduler läuft beim Deploy kurzzeitig in zwei Containern parallel (Rollout). Ein klassisches "nur eine Instanz"-Design führt zu doppeltem OpenAI-Spending — jedes Item 2× übersetzt = 2× Kosten + 2× Telegram-Push.

Lösung in src/lib/cron-lock.ts: atomares findOneAndUpdate auf ein locks-Dokument plus ein expireAfterSeconds: 0-TTL-Index auf expiresAt; Lock-Dauer per Job konfigurierbar (300–900 s). Der Prozess, der den Lock verliert, returnt früh. TTL fängt auch Crashes ab — kein hängender Prozess nach OOM-Kill.

Content-Enrichment Fallback-Chain

H/03
RSS liefert oft nur Titel + 200-Zeichen-Summary, teilweise ohne OG-Image. Reine RSS-Pipelines liefern kaputte Cards.

Dreistufige Chain, verdrahtet in src/app/api/cron/rss-sync/route.ts: (1) Artikel-URL via src/lib/content-fetcher.ts per JSDOM nach og:image / twitter:image / link rel=image_src scrapen, (2) Fallback auf die RSS-Image-Felder in Priorität: media:content (höchste Auflösung) > <enclosure> > media:thumbnail > erstes <img> im Content-HTML, (3) liefern beide Stufen nichts, rendert <NewsPlaceholder> eine Gradient-Card mit Kategorie- und Krisen-Typ-passendem Lucide-Icon — niemals ein Broken-Image. Jeder Netzwerk-Fetch ist in einen 10–15 s AbortController-Timeout gewickelt — eine tote Quelle blockiert die Pipeline nicht.

Crisis-Relevance als Auto-Publish-Gate

H/04
Die OpenAI-Antwort liefert nicht nur Übersetzungen, sondern ein JSON mit crisisRelevanceScore: 0–100 (persistiert als crisisRelevance). Admin-konfigurierbarer Threshold (minCrisisRelevanceThreshold) entscheidet autoPost — darunter landet das Item in der Mod-Queue.

Löst zwei Probleme auf einmal: (a) Wetter-News werden nicht als "Breaking" gepusht, (b) der Admin senkt den Threshold bei akuten Lagen temporär, um mehr durchzulassen.
§ 06Stack · in production

What's running.

Working toolchain in production — nichts Theoretisches.
Next.js 16React 19TypeScriptTailwindMongoDB (self-hosted)Socket.IOOpenAI (nano · mini · 4.1)Cheerioisomorphic-dompurifyCoolifyDockerHetzner VPSTelegram Bot APIPWA · Service WorkerVazir (Farsi)
§ 07Reflection · takeaways

What I learned.

Projekt läuft. Diese Dinge nehme ich in die nächsten mit.

Budget als Architektur-Constraint.

Die 50-€/Monat-Grenze hat mehr gute Entscheidungen erzwungen als sie verhindert hat. MongoDB-als-Queue, Multi-Model-Routing, eigener Socket-Server — alles Entscheidungen, die ich ohne den Cap nicht so konsequent durchgezogen hätte. Constraints are features.

Bidi ist nicht CSS-Eigenschaft, sondern Produkt-Problem.

RTL mit eingestreutem LTR-Content braucht eine Content-Pipeline-Entscheidung, keine Stylesheet-Property. Wer das auf die CSS-Schicht schiebt, scheitert. Beim nächsten Multilingual-Projekt plane ich Bidi-Normalisierung von Anfang an als Translation-Output-Format ein.

◦ NEXT CASE · 02 / 11
AI Builds
← alle Projekte