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

Ein Krisen-Feed. In drei Sprachen. In Sekunden.

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

AzadiFeed — Screenshot
live · azadifeed.com
§ 01 — Problem · motivation

Warum es das gibt.

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.

§ 02 — Constraints · operating box

Der Rahmen, in den es passen musste.

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, gemischtes 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 — meist ruhig, bei Krisenereignissen ein Vielfaches des Normalbetriebs innerhalb von Minuten.
C/07 · PRIVACY
IP-Hashing, 24-h-Retention für Crisis-Reports, DSGVO-konform für die deutsche Diaspora.
C/08 · OFFLINE
PWA mit Service Worker — Nutzer im Iran mit instabiler Verbindung lesen gecachte Meldungen weiter.
§ 03 — Architecture · system topology

Wie es läuft.

Coolify + Docker auf eigenem VPS. MongoDB lokal mitgehostet. OpenAI als Übersetzungs-/Klassifikations-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

◦ Schematische Darstellung, Zahlen illustrativ.

§ 04 — Decisions · trade-offs

Vier bewusste Entscheidungen.

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

Coolify + Docker statt Vercel.

chosen
Self-hosted Coolify auf VPS · interner setInterval-Scheduler in einem eigenen Modul
instead of
Vercel mit vercel.json-Cron-Jobs
reason
Vercel-Cron ist im Free Tier auf 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_feeds → published_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); zentral konfiguriert
instead of
Hardcoded gpt-4o auf allen Items
reason
80 % der RSS-Items sind Routine (Wetter, Wirtschaft, Sport) — nano reicht für Klassifikation + Übersetzung 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 als eigener Socket-Endpoint 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 gleichzeitigen Verbindungen. Socket.IO braucht kein drittes System, hat eingebaute Reconnect-Logik und erlaubt später bidirektionale Admin-Moderation-Events ohne Umbau.
§ 05 — Highlights · interesting bits

Was nicht offensichtlich war.

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

RTL mit gemischten 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 im Cron-Lock-Modul: 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 im RSS-Sync-Cron: (1) Artikel-URL via Content-Fetcher 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 über 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.
§ 06 — Stack · in production

Was läuft.

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

Was ich gelernt habe.

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 keine CSS-Eigenschaft, sondern ein 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 · 04 / 10
AI Builds → ↗
← alle Projekte