A real-time global news aggregator focused on geopolitical conflict and world events. 200 sources across every major region and perspective; related stories grouped across languages by multilingual embeddings; trending surfaced as it breaks. Headlines appear exactly as their newsrooms wrote them.
Live at noahsabaj.github.io/ww3watch · How it works
The rule the system is built on: machine intelligence routes stories — relevance, grouping, trending — but never rewrites them. The only model-touched content is opt-in translation, one click from the original. See docs/CONVENTIONS.md.
- Real-time feed — new articles, story regroupings, and trending changes push live via Supabase Realtime
- Cross-language story grouping — multilingual embeddings (e5-base, run locally in the pipeline) group a Persian headline with the Norwegian and English coverage of the same event; deterministic, quota-free
- Trending Now — LLM-picked top stories, updating live
- Wire detection — near-identical copies inside a story are marked, so "12 sources" doesn't overstate independent confirmation
- In-app reader + translation — cached extraction (survives link rot), on-demand translation into your reading language (set once; defaults from your browser locale), the original one click away
- Source roster with live health — every feed and its fetch health, public on /about
- Freshness dead-man's switch — the header shows when ingestion last succeeded; it goes amber/red if the pipeline stalls
- Region filtering, RTL, PWA — 16 region/perspective buckets; first-class Persian/Arabic/Hebrew rendering; installable
All free-tier, no provider that pauses idle hobby projects:
- SvelteKit + Svelte 5 runes — static SPA (
adapter-static) - GitHub Pages — hosting · GitHub Actions — scheduled ingestion (also runs as a container, see
Dockerfile) - Supabase — Postgres (+pgvector, pg_cron retention) + Realtime + two Deno Edge Functions (
reader,translate) - Transformers.js — multilingual-e5-base embeddings, locally on the runner (story grouping)
- Groq — relevance classification + trending picks · Cerebras — on-demand translation
- Tailwind CSS v4
GitHub Actions (cron, every 15 min) Browser (static SPA on GitHub Pages)
scripts/run-pipeline.ts +page.ts ── anon read ──► Supabase
roster ◄── sources table (health ──►) realtime ◄── INSERT/UPDATE events
fetch feeds (direct → CF proxy) ArticlePanel ──► Edge Functions
de-dup vs DB ∪ rejects reader (extract, cached)
classify (LLM) + shadow pre-filter translate (LLM, cached)
upsert articles (+body_hash wire marks)
embed titles (local e5-base)
assign stories (pgvector RPC) ──► stories
recompute trending (LLM picks)
pg_cron (daily): retention prune
- Ingestion is a Node script run by GitHub Actions (.github/workflows/pipeline.yml) — or anywhere, via the
Dockerfile. The feed roster lives in thesourcestable (health written back every run); curation is SQL, not commits. - Story grouping is deterministic: titles embed through a pinned multilingual model on the runner, and a pgvector RPC assigns each article to the nearest story representative within a time window. No LLM in the loop.
- Frontend is a static SPA. The initial load (src/routes/+page.ts) reads with the anon key; Realtime keeps articles, story regroupings, trending, and the freshness readout live.
reader/translaterun as Supabase Edge Functions (supabase/functions) — they need a server (SSRF-guarded fetch, the LLM key). Content is cached raw and sanitized on the client with DOMPurify at{@html}, so sanitizer upgrades apply retroactively.
npm install
npm run dev # frontend (needs PUBLIC_SUPABASE_* in .env)
npm test # vitest
npm run check # svelte-check
# run the ingestion pipeline once, locally:
node --import tsx --env-file=.env scripts/run-pipeline.tsCopy .env.example → .env and fill in credentials.
One-time setup (all free tier):
- Cerebras — create an API key at cloud.cerebras.ai; note the model (
gpt-oss-120b). - Supabase
- Enable RLS on
articlesandtrendingwith policies allowing anonSELECT(the SPA + realtime read with the anon key; writes stay service-role only). - Ensure
articlesis in thesupabase_realtimepublication. - Deploy the functions and set their secrets:
supabase functions deploy reader translate supabase secrets set LLM_BASE_URL=https://api.cerebras.ai/v1 LLM_API_KEY=... LLM_MODEL=gpt-oss-120b
- Enable RLS on
- GitHub
- Repo Settings → Secrets and variables → Actions → add:
SUPABASE_URL,SUPABASE_SECRET_KEY(thesb_secret_…key),LLM_BASE_URL,LLM_API_KEY,LLM_MODEL,PUBLIC_SUPABASE_URL,PUBLIC_SUPABASE_ANON_KEY, and (optional)FEED_PROXY_URL+FEED_PROXY_SECRET. - Settings → Pages → Source = GitHub Actions.
- Push to
main: deploy.yml publishes the site; pipeline.yml ingests every 15 min (or run it manually via Actions → Ingestion pipeline → Run workflow).
- Repo Settings → Secrets and variables → Actions → add:
- Feed proxy (optional but recommended) — many news-site WAFs block GitHub Actions' datacenter IPs, killing most feeds. The pipeline therefore fetches proxy-first (with a direct fallback) through the Cloudflare Worker in cloudflare/feed-proxy.js when configured. Setup:
- Add
FEED_PROXY_URL+FEED_PROXY_SECRETto the GitHub Actions secrets above, and set the Worker secret to the same value:wrangler secret put FEED_PROXY_SECRET. - For automatic Worker deploys, add
CLOUDFLARE_API_TOKEN+CLOUDFLARE_ACCOUNT_IDrepo secrets — then deploy-worker.yml deploys cloudflare/ (config in cloudflare/wrangler.toml) on every push tomainthat touches it, and dry-run-validates PRs. Without the token the deploy step warns and skips, so you can also deploy by hand:npx wrangler deployfromcloudflare/. Free tier covers it. DB schema lives in supabase/migrations.
- Add
The site deploys to
https://<user>.github.io/ww3watch(theBASE_PATH=/ww3watchin the deploy workflow handles the sub-path). For a custom domain, setBASE_PATHto empty and add aCNAME.
Scheduled GitHub Actions are auto-disabled after 60 days of repo inactivity — ordinary commits keep them alive.
AGPL-3.0 — the copyleft that applies to network services: if you run a modified WW3Watch as a website, you must offer your users the modified source. Chosen deliberately. This project's credibility rests on its pipeline being auditable — the LLM routes stories (classification, clustering, trending) but never rewrites what journalists wrote — and AGPL keeps every public derivative auditable on the same terms. The license covers this code, not the aggregated news content, which belongs to its publishers.