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.
Instead 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.
Personal-first; multi-user-ready data model and APIs. Web UI today; Expo mobile feed next.
- Node.js 20+
pnpm9 (
corepack enable
recommended)- Docker (Postgres + Ollama via Compose —
docker compose up -d
; seedocs/ops-local.md) - Optional: host Ollamaonly if you want easierGPU access —docs/ops-local.md
cp .env.example .env
cp apps/web/.env.example apps/web/.env.local
pnpm install
docker compose up -d # Postgres + Ollama
pnpm db:migrate
pnpm db:seed # demo user + HN + Platformer Substack + example topic
pnpm --filter @newsroom/web dev
Open http://localhost:3000 — sign up / sign in. Default seed login after pnpm db:seed
: ** demo@example.com** /
newsroom-demo. Authenticated home is the ranked
Feed; use
Topics,
Advisor,
Sources, and
Settings in the masthead.
Scores 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
then pnpm worker:rank
, or use Rank latest on the Feed) to see story rows.
Check health:
curl -sS http://localhost:3000/api/health | jq
One-shot ingest then rank (requires migrate + seed or your own subscriptions/topics):
pnpm worker:ingest # upserts articles; marks affected users dirty; enqueues per-user rank jobs
pnpm worker:rank # drains rank jobs for dirty ∩ active users (feed activity in last 30m)
pnpm worker:rank -- --all-dirty # enqueue/drain all dirty users (ignore activity gate)
pnpm worker:prune-scores # prune stale scores + articles older than ARTICLE_TTL_DAYS (keeps saved; also runs after Rank latest)
After ingest + rank, refresh the Feed on 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
/seen
scores (Saved/Dismissed stay) and does not auto re-rank.
Long-running worker (claims ingest
and rank
jobs; ingest cadence ~12 minutes):
pnpm --filter @newsroom/worker start
| Path | Role |
|---|---|
apps/web |
|
| Next.js — auth, editorial feed / topics / sources / settings UI, APIs | |
apps/mobile |
|
| Expo Router shell (health via api-client) | |
apps/worker |
|
| Postgres job poller + one-shot ingest/rank CLI | |
packages/db |
|
| Drizzle schema + migrations (auth, ingest, topics, scores) | |
packages/ai |
|
AiProvider + createAiProvider (Ollama / OpenAI / Google) + keyword/rankArticleBatch helpers |
|
packages/sources |
|
| Source adapters: HN, Substack/RSS, podcasts, Bluesky, Reddit | |
packages/api-client |
|
| Typed client (health, sources, topics, topic-tree, feed) |
make help
lists the same shortcuts. Prefer Make for day-to-day; pnpm
/ docker compose
remain the underlying commands.
| Target | Description |
|---|---|
make setup |
|
Copy .env + apps/web/.env.local if missing, then pnpm install |
|
make install |
|
| Install workspace deps | |
make up |
|
Start Postgres; Compose Ollama only if :11434 is free (else use host Ollama) |
|
make up-postgres |
|
| Postgres only (skip Ollama) | |
make up-gpu |
|
| Compose with NVIDIA GPU passthrough for Ollama | |
make down |
|
| Stop Compose services | |
make logs |
|
| Tail Compose logs | |
make migrate |
|
| Apply Drizzle migrations | |
make generate |
|
| Generate Drizzle migrations from schema | |
make seed |
|
| Demo user + HN + Platformer Substack + example topic | |
make studio |
|
| Drizzle Studio | |
make web |
|
| Next.js dev server (:3000) | |
make worker |
|
| Long-running ingest + rank job poller | |
make ingest |
|
| One-shot ingest then exit | |
make rank |
|
One-shot rank (RANK_ARGS=-- --all-dirty optional) |
|
make prune |
|
| One-shot prune stale scores + old articles | |
make test |
|
| AI + sources unit tests (offline-safe) | |
make test-ai / test-web / test-worker / test-sources |
|
| Package test suites | |
make typecheck / make build |
|
| Turbo typecheck / build | |
make ollama-pull |
|
Pull model into Compose Ollama (OLLAMA_MODEL=llama3.2 default) |
|
make verify |
|
| Local acceptance script (web must be up) |
| Command | Description |
|---|---|
pnpm install |
|
| Install workspace deps | |
docker compose up -d |
|
| Start Postgres + Ollama | |
docker compose up -d postgres |
|
| Postgres only (skip Ollama) | |
docker compose -f docker-compose.yml -f docker-compose.gpu.yml up -d |
|
| Start with NVIDIA GPU passthrough for Ollama (needs NVIDIA Container Toolkit on host, see | |
docker exec -it newsroom-ollama ollama pull llama3.2
docker exec -it newsroom-ollama ollama pull llama3.1:8b
/ qwen2.5:7b
reason
quality, slower (see docs/ops-local.md#model-options)pnpm db:generate
pnpm db:migrate
DATABASE_URL
pnpm db:seed
AI & infra
topicpnpm db:studio
pnpm --filter @newsroom/web dev
/ pnpm web:dev
pnpm --filter @newsroom/web build
/ start
pnpm --filter @newsroom/worker start
/ pnpm worker:start
pnpm worker:ingest
/ pnpm --filter @newsroom/worker ingest
pnpm worker:rank
/ pnpm --filter @newsroom/worker rank
pnpm worker:prune-scores
pnpm --filter @newsroom/mobile start
/ pnpm mobile:start
pnpm sources:test
pnpm worker:test
pnpm web:test
pnpm --filter @newsroom/ai test
/ pnpm ai:test
pnpm --filter @newsroom/ai smoke
/ pnpm ai:smoke
AI_PROVIDER
(skips if unreachable; AI_SMOKE=1
/ OLLAMA_SMOKE=1
to require it)pnpm build
/ pnpm typecheck
./scripts/verify-scaffold.sh
Env vars: see docs/ops-local.md#environment-files for which file loads what. Templates: root .env.example (worker/CLI) and
apps/web/.env.example
apps/web/.env.local
(Next / Rank latest). Shared: DATABASE_URL
, BETTER_AUTH_*
, OLLAMA_*
, RANK_*
, AI_TOKEN_*
, TTLs. Web-only optional: LANGSEARCH_API_KEY
(Sources feed search). Root-only: GITHUB_*
, SEED_USER_ID
, NEWSROOM_WORKER_ONCE
. Mobile: EXPO_PUBLIC_API_URL
. Ranking tiers: docs/ops-local.md#ranking-model-tiers.
| Method | Path | Notes |
|---|---|---|
GET/PATCH |
||
/api/settings/rank-model |
||
Get / set ranking model tier: none |
fast |
standard |
GET/PUT/DELETE |
||
/api/settings/ai-credentials |
||
Optional BYOK OpenAI/Google key (encrypted; requires AI_CREDENTIALS_KEY ) |
| Method | Path | Notes |
|---|---|---|
GET/POST |
||
/api/sources |
||
| List / create (caller’s sources only) | ||
PATCH/DELETE |
||
/api/sources/:id |
||
| Update / delete own source | ||
GET |
||
/api/feed-catalog |
||
| Curated suggested feeds | ||
POST |
||
/api/feed-search |
||
Discover RSS/Atom URLs via LangSearch (LANGSEARCH_API_KEY ); { query } → { results } |
| Method | Path | Notes |
|---|---|---|
GET |
||
/api/topic-tree |
||
Curated catalog { version, nodes[] } (selectable leaves for topic names) |
||
GET/POST |
||
/api/topics |
||
List / create (caller’s topics only; name = catalog leaf label) |
||
PATCH/DELETE |
||
/api/topics/:id |
||
| Update / delete own topic | ||
GET |
||
/api/feed?cursor=&topic=&excludeTopic=&source=&status=&limit= |
||
Ranked scores; default is new /seen only; topic= includes (OR), excludeTopic= excludes; status=saved (etc.) filters to that status; returns rankedCount / evaluatedCount / articlesCount pipeline counters |
||
POST |
||
| `/api/feed/:articleId/seen | saved | dismissed` |
Update status; 404 if no score row |
Ranking formulas and job behavior: docs/decisions/002-hybrid-ranking.md. Health, seed, Compose: docs/ops-local.md.
| Doc | Purpose |
|---|---|
docs/ops-local.mddocs/decisions/001-ingest-url-and-hn.mddocs/decisions/002-hybrid-ranking.mddocs/decisions/004-ai-confirmed-topic-membership.mddocs/feature-backlog.mddocs/feature-completed.mddocs/github-workflow.mddocs/contributing.mdCore 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.
MIT © 2026 SpektrNO