{"slug": "show-hn-newsroom-self-hosted-ai-ranked-meta-aggregator-for-hn-subreddits-etc", "title": "Show HN: Newsroom Self-hosted, AI-ranked meta-aggregator for HN Subreddits etc.", "summary": "Newsroom, a self-hosted, AI-ranked meta-aggregator for Hacker News, Substack, podcasts, Bluesky, Reddit, and other feeds, was released on GitHub. The Node.js-based tool combines keyword and optional AI ranking to create a personalized feed with Save/Dismiss, topic filters, and an Advisor for refining interests. It supports multi-user data models and APIs, with a web UI and an upcoming Expo mobile feed.", "body_md": "**Focused news aggregator** — an aggregator of aggregators. It sits on top of Hacker News, Substack, podcasts, Bluesky, Reddit, and other personal feeds you subscribe to, then ranks stories to the topics you care about.\n\nInstead of browsing each source separately, Newsroom pulls recent items into one hybrid-ranked feed (keywords + optional AI), with Save / Dismiss, topic filters, and an in-app Advisor for refining interests.\n\nPersonal-first; multi-user-ready data model and APIs. Web UI today; Expo mobile feed next.\n\n- Node.js 20+\n[pnpm](https://pnpm.io)9 (`corepack enable`\n\nrecommended)- Docker (Postgres + Ollama via Compose —\n`docker compose up -d`\n\n; see[docs/ops-local.md](/SpektrNO/newsroom/blob/main/docs/ops-local.md#ollama)) - Optional: host\n[Ollama](https://ollama.com)only if you want easier**GPU** access —[docs/ops-local.md](/SpektrNO/newsroom/blob/main/docs/ops-local.md#optional-host-install-for-gpu)\n\n```\ncp .env.example .env\ncp apps/web/.env.example apps/web/.env.local\n# Set BETTER_AUTH_SECRET to ≥32 characters in BOTH files, e.g. openssl rand -base64 32\n# Keep shared vars (DB, Ollama, RANK_*) in sync — Next does not read root `.env`.\n# See docs/ops-local.md#environment-files\n\npnpm install\ndocker compose up -d          # Postgres + Ollama\npnpm db:migrate\npnpm db:seed                  # demo user + HN + Platformer Substack + example topic\n# First time: docker exec -it newsroom-ollama ollama pull llama3.2\npnpm --filter @newsroom/web dev\n```\n\nOpen [http://localhost:3000](http://localhost:3000) — sign up / sign in. Default seed login after `pnpm db:seed`\n\n: ** demo@example.com** /\n\n**newsroom-demo**. Authenticated home is the ranked\n\n**Feed**; use\n\n**Topics**,\n\n**Advisor**,\n\n**Sources**, and\n\n**Settings** in the masthead.\n\nScores are per user: after seed + ingest + rank, sign in as the seeded demo user (or run `SEED_USER_ID=<your-user-id> pnpm db:seed`\n\nthen `pnpm worker:rank`\n\n, or use **Rank latest** on the Feed) to see story rows.\n\nCheck health:\n\n```\ncurl -sS http://localhost:3000/api/health | jq\n```\n\nOne-shot ingest then rank (requires migrate + seed or your own subscriptions/topics):\n\n```\npnpm worker:ingest            # upserts articles; marks affected users dirty; enqueues per-user rank jobs\npnpm worker:rank              # drains rank jobs for dirty ∩ active users (feed activity in last 30m)\npnpm worker:rank -- --all-dirty  # enqueue/drain all dirty users (ignore activity gate)\npnpm worker:prune-scores      # prune stale scores + articles older than ARTICLE_TTL_DAYS (keeps saved; also runs after Rank latest)\n# or: NEWSROOM_WORKER_ONCE=ingest|rank|prune-scores pnpm --filter @newsroom/worker start\n```\n\nAfter ingest + rank, refresh the Feed on [http://localhost:3000](http://localhost:3000) — story rows appear with Save / Dismiss; filter by topic, source, search, or Saved. Topic/source changes mark you dirty and clear unscored feed rows; opening the Feed records activity and enqueues catch-up rank when dirty. **Rank latest** always ranks the signed-in user. **Wipe rankings** clears `new`\n\n/`seen`\n\nscores (Saved/Dismissed stay) and does not auto re-rank.\n\nLong-running worker (claims `ingest`\n\nand `rank`\n\njobs; ingest cadence ~12 minutes):\n\n```\npnpm --filter @newsroom/worker start\n```\n\n| Path | Role |\n|---|---|\n`apps/web` |\nNext.js — auth, editorial feed / topics / sources / settings UI, APIs |\n`apps/mobile` |\nExpo Router shell (health via api-client) |\n`apps/worker` |\nPostgres job poller + one-shot ingest/rank CLI |\n`packages/db` |\nDrizzle schema + migrations (auth, ingest, topics, scores) |\n`packages/ai` |\n`AiProvider` + `createAiProvider` (Ollama / OpenAI / Google) + keyword/`rankArticleBatch` helpers |\n`packages/sources` |\nSource adapters: HN, Substack/RSS, podcasts, Bluesky, Reddit |\n`packages/api-client` |\nTyped client (health, sources, topics, topic-tree, feed) |\n\n`make help`\n\nlists the same shortcuts. Prefer Make for day-to-day; `pnpm`\n\n/ `docker compose`\n\nremain the underlying commands.\n\n| Target | Description |\n|---|---|\n`make setup` |\nCopy `.env` + `apps/web/.env.local` if missing, then `pnpm install` |\n`make install` |\nInstall workspace deps |\n`make up` |\nStart Postgres; Compose Ollama only if `:11434` is free (else use host Ollama) |\n`make up-postgres` |\nPostgres only (skip Ollama) |\n`make up-gpu` |\nCompose with NVIDIA GPU passthrough for Ollama |\n`make down` |\nStop Compose services |\n`make logs` |\nTail Compose logs |\n`make migrate` |\nApply Drizzle migrations |\n`make generate` |\nGenerate Drizzle migrations from schema |\n`make seed` |\nDemo user + HN + Platformer Substack + example topic |\n`make studio` |\nDrizzle Studio |\n`make web` |\nNext.js dev server (:3000) |\n`make worker` |\nLong-running ingest + rank job poller |\n`make ingest` |\nOne-shot ingest then exit |\n`make rank` |\nOne-shot rank (`RANK_ARGS=-- --all-dirty` optional) |\n`make prune` |\nOne-shot prune stale scores + old articles |\n`make test` |\nAI + sources unit tests (offline-safe) |\n`make test-ai` / `test-web` / `test-worker` / `test-sources` |\nPackage test suites |\n`make typecheck` / `make build` |\nTurbo typecheck / build |\n`make ollama-pull` |\nPull model into Compose Ollama (`OLLAMA_MODEL=llama3.2` default) |\n`make verify` |\nLocal acceptance script (web must be up) |\n\n| Command | Description |\n|---|---|\n`pnpm install` |\nInstall workspace deps |\n`docker compose up -d` |\nStart Postgres + Ollama |\n`docker compose up -d postgres` |\nPostgres only (skip Ollama) |\n`docker compose -f docker-compose.yml -f docker-compose.gpu.yml up -d` |\nStart with NVIDIA GPU passthrough for Ollama (needs NVIDIA Container Toolkit on host, see\n|\n\n`docker exec -it newsroom-ollama ollama pull llama3.2`\n\n`docker exec -it newsroom-ollama ollama pull llama3.1:8b`\n\n/ `qwen2.5:7b`\n\n`reason`\n\nquality, slower (see [docs/ops-local.md#model-options](/SpektrNO/newsroom/blob/main/docs/ops-local.md#model-options))`pnpm db:generate`\n\n`pnpm db:migrate`\n\n`DATABASE_URL`\n\n`pnpm db:seed`\n\n`AI & infra`\n\ntopic`pnpm db:studio`\n\n`pnpm --filter @newsroom/web dev`\n\n/ `pnpm web:dev`\n\n`pnpm --filter @newsroom/web build`\n\n/ `start`\n\n`pnpm --filter @newsroom/worker start`\n\n/ `pnpm worker:start`\n\n`pnpm worker:ingest`\n\n/ `pnpm --filter @newsroom/worker ingest`\n\n`pnpm worker:rank`\n\n/ `pnpm --filter @newsroom/worker rank`\n\n`pnpm worker:prune-scores`\n\n`pnpm --filter @newsroom/mobile start`\n\n/ `pnpm mobile:start`\n\n`pnpm sources:test`\n\n`pnpm worker:test`\n\n`pnpm web:test`\n\n`pnpm --filter @newsroom/ai test`\n\n/ `pnpm ai:test`\n\n`pnpm --filter @newsroom/ai smoke`\n\n/ `pnpm ai:smoke`\n\n`AI_PROVIDER`\n\n(skips if unreachable; `AI_SMOKE=1`\n\n/ `OLLAMA_SMOKE=1`\n\nto require it)`pnpm build`\n\n/ `pnpm typecheck`\n\n`./scripts/verify-scaffold.sh`\n\nEnv vars: see [docs/ops-local.md#environment-files](/SpektrNO/newsroom/blob/main/docs/ops-local.md#environment-files) for **which file** loads what. Templates: root [ .env.example](/SpektrNO/newsroom/blob/main/.env.example) (worker/CLI) and\n\n[→](/SpektrNO/newsroom/blob/main/apps/web/.env.example)\n\n`apps/web/.env.example`\n\n`apps/web/.env.local`\n\n(Next / Rank latest). Shared: `DATABASE_URL`\n\n, `BETTER_AUTH_*`\n\n, `OLLAMA_*`\n\n, `RANK_*`\n\n, `AI_TOKEN_*`\n\n, TTLs. Web-only optional: `LANGSEARCH_API_KEY`\n\n(Sources feed search). Root-only: `GITHUB_*`\n\n, `SEED_USER_ID`\n\n, `NEWSROOM_WORKER_ONCE`\n\n. Mobile: `EXPO_PUBLIC_API_URL`\n\n. Ranking tiers: [docs/ops-local.md#ranking-model-tiers](/SpektrNO/newsroom/blob/main/docs/ops-local.md#ranking-model-tiers).\n\n| Method | Path | Notes |\n|---|---|---|\n`GET/PATCH` |\n`/api/settings/rank-model` |\nGet / set ranking model tier: `none` | `fast` | `standard` |\n`GET/PUT/DELETE` |\n`/api/settings/ai-credentials` |\nOptional BYOK OpenAI/Google key (encrypted; requires `AI_CREDENTIALS_KEY` ) |\n\n| Method | Path | Notes |\n|---|---|---|\n`GET/POST` |\n`/api/sources` |\nList / create (caller’s sources only) |\n`PATCH/DELETE` |\n`/api/sources/:id` |\nUpdate / delete own source |\n`GET` |\n`/api/feed-catalog` |\nCurated suggested feeds |\n`POST` |\n`/api/feed-search` |\nDiscover RSS/Atom URLs via LangSearch (`LANGSEARCH_API_KEY` ); `{ query }` → `{ results }` |\n\n| Method | Path | Notes |\n|---|---|---|\n`GET` |\n`/api/topic-tree` |\nCurated catalog `{ version, nodes[] }` (selectable leaves for topic names) |\n`GET/POST` |\n`/api/topics` |\nList / create (caller’s topics only; `name` = catalog leaf label) |\n`PATCH/DELETE` |\n`/api/topics/:id` |\nUpdate / delete own topic |\n`GET` |\n`/api/feed?cursor=&topic=&excludeTopic=&source=&status=&limit=` |\nRanked scores; default is `new` /`seen` only; `topic=` includes (OR), `excludeTopic=` excludes; `status=saved` (etc.) filters to that status; returns `rankedCount` / `evaluatedCount` / `articlesCount` pipeline counters |\n`POST` |\n`/api/feed/:articleId/seen|saved|dismissed` |\nUpdate status; `404` if no score row |\n\nRanking formulas and job behavior: [docs/decisions/002-hybrid-ranking.md](/SpektrNO/newsroom/blob/main/docs/decisions/002-hybrid-ranking.md). Health, seed, Compose: [docs/ops-local.md](/SpektrNO/newsroom/blob/main/docs/ops-local.md).\n\n| Doc | Purpose |\n|---|---|\n|\n\n[docs/ops-local.md](/SpektrNO/newsroom/blob/main/docs/ops-local.md)[docs/decisions/001-ingest-url-and-hn.md](/SpektrNO/newsroom/blob/main/docs/decisions/001-ingest-url-and-hn.md)[docs/decisions/002-hybrid-ranking.md](/SpektrNO/newsroom/blob/main/docs/decisions/002-hybrid-ranking.md)[docs/decisions/004-ai-confirmed-topic-membership.md](/SpektrNO/newsroom/blob/main/docs/decisions/004-ai-confirmed-topic-membership.md)[docs/feature-backlog.md](/SpektrNO/newsroom/blob/main/docs/feature-backlog.md)[docs/feature-completed.md](/SpektrNO/newsroom/blob/main/docs/feature-completed.md)[docs/github-workflow.md](/SpektrNO/newsroom/blob/main/docs/github-workflow.md)[docs/contributing.md](/SpektrNO/newsroom/blob/main/docs/contributing.md)Core loop is shipped: auth, multi-source ingest (HN, Substack/RSS, podcasts, Bluesky, Reddit), hybrid rank + Advisor (Ollama / OpenAI / Google, optional BYOK), editorial web UI. Expo feed UI is next per the backlog.\n\n[MIT](/SpektrNO/newsroom/blob/main/LICENSE) © 2026 SpektrNO", "url": "https://wpnews.pro/news/show-hn-newsroom-self-hosted-ai-ranked-meta-aggregator-for-hn-subreddits-etc", "canonical_source": "https://github.com/SpektrNO/newsroom", "published_at": "2026-08-03 10:20:29+00:00", "updated_at": "2026-08-03 10:25:08.547368+00:00", "lang": "en", "topics": ["ai-products", "ai-tools", "artificial-intelligence"], "entities": ["Newsroom", "Hacker News", "Substack", "Bluesky", "Reddit", "GitHub", "Node.js", "Expo"], "alternates": {"html": "https://wpnews.pro/news/show-hn-newsroom-self-hosted-ai-ranked-meta-aggregator-for-hn-subreddits-etc", "markdown": "https://wpnews.pro/news/show-hn-newsroom-self-hosted-ai-ranked-meta-aggregator-for-hn-subreddits-etc.md", "text": "https://wpnews.pro/news/show-hn-newsroom-self-hosted-ai-ranked-meta-aggregator-for-hn-subreddits-etc.txt", "jsonld": "https://wpnews.pro/news/show-hn-newsroom-self-hosted-ai-ranked-meta-aggregator-for-hn-subreddits-etc.jsonld"}}