Vagus is an end-to-end automated news synthesis and intelligence briefing platform built with Bun, TypeScript, Hono, React 19, Tailwind CSS v4, tRPC, BetterAuth, and SQLite.
It ingests content from RSS feeds and web search queries, extracts key findings, clusters related articles into unified story topics, scores story relevance for individual user interest profiles, and synthesizes executive briefings complete with interactive favicon citations.
Vagus includes an adaptive feedback engine that continuously tailors digest generation to user preferences through explicit thumbs up / thumbs down interactions.
┌──────────────────────────────────────────────┐
│ User Feedback Event │
└──────────────────────┬───────────────────────┘
│
┌─────────────────────┴─────────────────────┐
▼ ▼
┌─────────────────────────┐ ┌──────────────────────────┐
│ Source Weight Tuning │ │ Cluster Preference │
│ (user_source_weight) │ │ (interest_profile) │
└────────────┬────────────┘ └────────────┬─────────────┘
│ │
▼ ▼
Adjust Multiplier (0.0x–2.0x) Blend Preference Vector (EMA)
0.0x = Mute all stories from source V_new = 0.75·V_old + 0.25·V_cluster
- Per-User Isolation: Multi-tenant isolation ensures one user's source preferences do not affect global ingest or other users' digests.
-
Dynamic Multipliers: Each source starts at a baseline weight of
1.0x.-
Thumbs Up on a source increments its weight multiplier (e.g.
1.0x→1.3x→1.6x→2.0xmax boost). -
Thumbs Down on a source decrements its weight multiplier (e.g.
1.0x→0.7x→0.4x→0.1x→0.0x).
-
Thumbs Up on a source increments its weight multiplier (e.g.
-
Source Muting: When a source's weight drops to
0.0x(or$\le 0.1$ ), story clusters relying exclusively on that source receive a score of0("Source muted by user preference") and are excluded from synthesis.
Clicking Thumbs Up or Thumbs Down on a Story Cluster card in the Digest Reader adapts the user's semantic profile without requiring manual keyword configuration:
-
Positive Vector Nudging (
positive_embedding):- Clicking Thumbs Up extracts the cluster's primary article vector (
$V_{cluster}$ ) and blends it into the user's positive preference vector using an Exponential Moving Average:$$V_{pos_new} = 0.75 \cdot V_{pos_old} + 0.25 \cdot V_{cluster}$$ - Future stories with high cosine similarity to
$V_{pos}$ receive a relevance score boost (up to$+0.20$ ).
- Clicking Thumbs Up extracts the cluster's primary article vector (
-
Negative Vector Nudging (
negative_embedding):- Clicking Thumbs Down blends the cluster vector into the user's negative preference vector:
$$V_{neg_new} = 0.75 \cdot V_{neg_old} + 0.25 \cdot V_{cluster}$$ - Future stories semantically similar to suppressed topics (e.g. personal banking listicles, product roundups, or unwanted niches) incur a cosine similarity penalty (up to
$-0.30$ ). Stories penalized below the user's similarity threshold are filtered out automatically.
- Clicking Thumbs Down blends the cluster vector into the user's negative preference vector:
- Every vote logs an immutable event record containing
user_id,target_type("source"or"cluster"),target_id, andvote(1,-1, or0). - Users can review and reset their custom source weightings at any time in Profile Management or Source Settings.
Vagus prevents repetitive digests when running multiple pipeline cycles per day:
processed_keyIngest Deduplication: Permanently records canonical item keys to prevent re-ingesting raw articles.- Prior Digest History Filtering: Before selecting clusters for a new run, the scoring engine queries all articles previously delivered in prior digests for that specific user.
- Delta Synthesis:
- Clusters with no new articles since the user's last digest are assigned a score of
0("Already delivered in previous digest") and filtered out. - Clusters with fresh updates (e.g., 1 new article published since the last run + background context) qualify as updated stories and are re-synthesized with the new developments highlighted.
- Clusters with no new articles since the user's last digest are assigned a score of
Vagus decouples global content processing from individual user profile evaluation:
-
Global Processing (Ingest → Extraction → Embedding → Clustering): A Pipeline Run (
run_id) executes centrally on a cron schedule or manual trigger. It ingests new items across all global sources, extracts article content, generates dense vector embeddings, and clusters related articles into unified Story Clusters. -
Per-Profile Scoring & Conditional Digest Generation: At the scoring stage, the run fans out to evaluate every active
(user, interest_profile)tuple in the system (e.g.General News,Tech & AI Deep Dive,Personal Finance):- Conditional Assembly: A briefing digest is created for a profile only if 1 or more story clusters qualify during scoring.
- Filtered Profiles: If all candidate stories fail a profile's rules, threshold, or recency check, the run completes for that profile without creating an empty digest.
[Fetch Sources] ──▶ [Extract Content] ──▶ [Article Summaries] ──▶ [Embed Vectors]
│
[Notify User] ◄── [Format Briefing] ◄── [Digest Assembly] ◄── [Story Cluster Synthesis] ◄── [Cluster & Score]
-
Ingest (
fetch-source): Fetches raw feeds withprocessed_keydeduplication. -
Extract (
extract-article): Crawls article text, parses metadata, and calculates reading times. - Article Summaries: Uses LLMs to produce concise 1-sentence headline bullets.
-
Embed (
embed-article): Generates dense vector embeddings using configured text embedding models. -
Cluster & Score (
cluster-run&score-user): Performs cosine similarity clustering ($\ge 0.80$ ) and scores candidate clusters against user interest profiles, applying source weights and preference vector adjustments. - Story Cluster Synthesis: Synthesizes cluster summaries and citation keys.
- Digest Assembly: Assembles overall executive briefing digests with bulleted trend cards, key takeaways, and why it matters.
-
Notify (
ntfy): Dispatches push notifications via ntfy.sh with deep links to the Digest Reader.
- Published Index:
/digestslists every published digest without requiring an account. - Shareable Links: Each digest has a stable public URL at
/digests/:id(for example,/digests/6). Anyone with the link can read the digest and open its cited sources. - Authenticated Feedback: Reading is public; signed-in users retain the thumbs-up and thumbs-down controls that tailor their own future digests.
- Google OAuth only for humans — there is no email/password login. API keys provide robot/automation access (sent via the
x-api-keyheader; a key acts as the admin who created it). - Access control:
ADMIN_EMAILS(comma-separated) become admins and bypass the domain whitelist; everyone else must matchSIGNUP_ALLOWED_DOMAINSto create an account. - API keys are created/revoked by admins in Admin Settings → API Keys (raw key shown once).
| Var | Purpose |
|---|---|
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET |
Google OAuth client. If unset, the Google login route is not mounted (sign-in 404s). |
BETTER_AUTH_URL |
Public HTTPS base URL, used for OAuth callbacks and secure cookies. |
ADMIN_EMAILS |
Comma-separated admin allowlist (bypasses the domain whitelist). |
SIGNUP_ALLOWED_DOMAINS |
Comma-separated domains allowed to create an account. |
Google OAuth redirect URI is ${BETTER_AUTH_URL}/api/auth/callback/google. These are wired through docker-compose.yml.
Set DEV_AUTH_ENABLED=true (ignored when NODE_ENV=production) to mount POST /dev/login, which simulates a Google sign-in under the same allowlist rules. Detached dev:agent starts also preseed an admin user and an API key to .dev-api-key. See AGENTS.md and docs/planning/AUTH_GOOGLE_ONLY.md.
- DB-Driven Settings: API keys, endpoints, worker concurrency, retention windows, and model routing choices are strictly database-backed.
- Editable LLM Prompts: System personas and user prompt templates for article summaries, story cluster synthesis, digest assembly, and relevance scoring can be edited directly in Admin Settings with
{{variable}}substitution tags and one-click resets.
- Runtime: Bun workspace monorepo (
apps/server+apps/web) - Backend: Hono on
Bun.serve, tRPC v10, BetterAuth, Kysely +bun:sqlite, plainjob queue - Frontend: React 19, tRPC + TanStack Query, Tailwind CSS v4 (
bun-plugin-tailwind)
bun run dev:agent # Start detached background dev server
bun run dev:status # Check running server status and port
bun run dev:stop # Gracefully stop dev serverbun run typecheck # Typecheck server and web apps
bun run test # Execute unit test suite (140+ passing tests)