A modern, beautifully designed Hacker News reader with AI-powered discussion summarization.
Browse top stories, save bookmarks, and let an AI agent distill thousands of comments into structured, actionable insights — all in one sleek interface.
🚀 Quick Start • 🏗️ Architecture • 🤖 AI Agent • 📦 Tech Stack • 🗂️ Data Modeling • 🔮 Roadmap
One-command setup:
docker compose up --buildThen open http://localhost:3000
- ✨ Features
- 📦 Tech Stack
- 🏗️ Architecture & System Design
- 🔄 Data Flow
- 🗂️ Data Modeling Deep-Dive
- 🤖 The AI Summarization Agent
- 🚀 Getting Started
- 🧠 Approach & Design Decisions
- ⚖️ Tradeoffs
- 🔮 Future Improvements
- 📁 Project Structure
| Feature | Description |
|---|---|
| 📰 Live Story Feed | Real-time Hacker News top/new/best stories with infinite scroll |
| 🔖 Instant Bookmarks | Save stories frictionlessly with zero sign-up required |
| 🤖 AI Discussion Summarization | One-click AI summary of comment threads with key points & sentiment |
| ⚡ Optimistic UI | Instant visual feedback with automatic rollback on errors |
| 🔁 Durable Background Jobs | AI pipeline with automatic retries and step-based checkpointing |
| 🧩 Shared AI Intelligence | Summaries are computed once and shared across all users |
| 📱 Responsive Design | Tailwind CSS-powered layout with Framer Motion animations |
| 🐳 One-Command Deployment | Full stack via Docker Compose — no manual setup required |
| Layer | Technology | Why We Chose It |
|---|---|---|
| Framework | Next.js 16 (App Router) | Server Components, Server Actions, and API Routes all in one — no separate backend needed |
| Language | TypeScript | End-to-end type safety across client, server, and database |
| Database | PostgreSQL 16 + Prisma 7 | Reliable relational DB with type-safe ORM, migrations, and connection pooling |
| Data Source | Algolia HN Search API | Fetches full comment trees in a single request — 10–20× faster than the official Firebase API |
| AI Engine | OpenRouter (nvidia/nemotron-3-nano) |
Free-tier LLM with structured JSON output and strong technical reasoning |
| Background Jobs | Inngest | Durable, step-based functions with automatic retries, observability, and no infra to manage |
| State Management | React Query (TanStack) | Server state caching, infinite scroll, and optimistic mutations out of the box |
| Styling | Tailwind CSS 4 | Utility-first CSS with a custom design system and zero runtime cost |
| Animations | Framer Motion | Smooth page transitions, micro-interactions, and gesture support |
| Icons | Lucide React | Consistent, tree-shakeable, lightweight icon library |
| Containerization | Docker + Docker Compose | Single command to spin up app + Postgres + Inngest Dev Server |
Dev Monks is a monolithic Next.js app that integrates external services for AI and job orchestration, with a containerized Postgres database. All services run inside Docker Compose for local development.
┌────────────────────────────────────────────────────────────────────┐
│ DOCKER COMPOSE │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ Next.js App (:3000) │ │
│ │ │ │
│ │ ┌─────────────────┐ ┌────────────────┐ ┌──────────────┐ │ │
│ │ │ App Router │ │ API Routes │ │Server Actions│ │ │
│ │ │ (SSR Pages) │ │ /api/* │ │ (Bookmarks) │ │ │
│ │ └────────┬─────────┘ └───────┬────────┘ └──────┬───────┘ │ │
│ │ │ │ │ │ │
│ │ ┌────────▼────────────────────▼───────────────────▼────────┐ │ │
│ │ │ Shared Libraries │ │ │
│ │ │ hn-api.ts │ ai-service.ts │ prisma.ts │ data.ts │ │ │
│ │ └────────┬───────────────┬────────────────────┬────────────┘ │ │
│ └───────────┼───────────────┼────────────────────┼──────────────┘ │
│ │ │ │ │
│ ┌───────────▼──┐ ┌─────────▼─────┐ ┌──────────▼──────────────┐ │
│ │ Algolia HN │ │ OpenRouter │ │ PostgreSQL │ │
│ │ Search API │ │ (LLM / AI) │ │ (:5432) │ │
│ │ (External) │ │ (External) │ │ ┌─────────────────────┐ │ │
│ │ │ │ │ │ │ Bookmarks Table │ │ │
│ └──────────────┘ └───────────────┘ │ │ Summaries Table │ │ │
│ │ └─────────────────────┘ │ │
│ └─────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Inngest Dev Server (:8288) │ │
│ │ Background Job Orchestrator │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ summarize-discussion (Durable Fn) │ │ │
│ │ │ │ │ │
│ │ │ Step 1 ──► Check DB for existing summary │ │ │
│ │ │ Step 2 ──► Fetch story metadata from Algolia │ │ │
│ │ │ Step 3 ──► Fetch & flatten comment tree │ │ │
│ │ │ Step 4 ──► Send to OpenRouter LLM │ │ │
│ │ │ Step 5 ──► Upsert result to PostgreSQL │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────┘
| Component | File(s) | Responsibility |
|---|---|---|
| Home Feed | app/page.tsx |
Infinite-scroll story listing with React Query |
| Story Detail | app/story/[id]/ |
Full comment tree + AI summary trigger |
| Bookmarks Page | app/bookmarks/ |
Saved story list with snapshot data |
| Story API | api/stories/ |
Paginated feed, featured story, and summarize endpoints |
| Inngest Endpoint | api/inngest/ |
Webhook receiver for background job events |
| Server Actions | actions/bookmarks.ts |
Toggle, list, and check bookmark state |
| Summarize Function | inngest/summarize-discussion |
5-step durable AI pipeline |
| Prisma Models | prisma/schema.prisma |
Bookmark + Summary with composite constraints |
User visits page
└─► React Query (useStories hook)
└─► GET /api/stories?type=top&page=0
└─► Algolia HN Search API
└─► Returns stories + metadata (author, score, comments count, url)
└─► Cached in React Query (stale: 2min, GC: 10min)
└─► Rendered with virtual infinite scroll pagination
User clicks "Summarize Discussion"
└─► POST /api/stories/[id]/summarize
└─► Triggers Inngest event: "story/summarize.requested"
├─► Step 1: Query DB — return instantly if summary exists
├─► Step 2: Fetch story from Algolia
├─► Step 3: Flatten comment tree (max 200 comments, depth 3)
├─► Step 4: Prompt OpenRouter LLM → structured JSON response
└─► Step 5: Upsert Summary into PostgreSQL
Meanwhile: UI polls GET /api/stories/[id]/summarize every 2 seconds
└─► Displays <AISummaryCard> with key points once available
User clicks bookmark icon
└─► BookmarkContext applies optimistic state update (instant UI)
└─► Server Action: toggleBookmark(storyId)
├─► Prisma: Check existing → Create or Delete record
├─► Stores metadata snapshot (title, url, score, author, time)
└─► Revalidates: /, /bookmarks, /story/[id]
└─► On error: Rollback optimistic state automatically
The schema is intentionally lean — designed for performance, cross-user AI sharing, and per-user personalization without auth complexity.
Instead of a full authentication system, Dev Monks uses a cookie-based anonymous user ID:
- On first visit, a unique
userIdUUID is generated and stored in a secure, HTTP-only cookie. Bookmarkrecords use a composite unique constraint:@@unique([storyId, userId]), ensuring each user has independent bookmarks without a user table.- Zero friction: Users get persistent bookmarks instantly, with a clear upgrade path to real accounts later (just replace the cookie ID with an authenticated user ID).
When a story is bookmarked, we don't just store a foreign key. We capture a point-in-time snapshot of the story's metadata:
Bookmark {
id String @id
storyId String
userId String (from cookie)
title String ← snapshot
url String? ← snapshot
score Int ← snapshot
author String ← snapshot
time DateTime ← snapshot
createdAt DateTime @default(now())
@@unique([storyId, userId])
}
Why? HN stories can be flagged, deleted, or modified at any time. Snapshots ensure bookmarks remain stable and readable even if the upstream data changes or disappears.
Summary {
id String @id
storyId String @unique ← one summary per story, globally
title String
overview String
keyPoints Json ← stored as JSON array for structured rendering
sentiment String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Key design decisions:
storyIdis a unique index — only one AI summary is ever generated per story.- Shared intelligence — because AI analysis of technical discussions is objective, all users share the same summary. If User A triggers it, User B sees it instantly for free.
keyPointsas JSON — enables the UI to render structured bullet points rather than parsing plain text.- Upsert on write — prevents duplicate summaries even if multiple users click "Summarize" simultaneously.
The AI agent isn't a simple API call — it's a Durable Agentic Workflow built on Inngest with a strict analytical persona.
The agent is configured via a carefully engineered system prompt that defines its role as an Elite Hacker News Analyst:
- Mission: Extract technical signal from social noise.
- Directives:
- Identify contrarian and dissenting viewpoints
- Map technical debates and unresolved disagreements
- Answer "The So What?" — why this discussion matters to engineers
- Call out hype vs. substance
- Constraints:
- Temperature
0.1for high determinism and consistency - Strict JSON output schema to prevent UI breakage
- Hard cap on tokens to stay within context windows
- Temperature
AI pipelines fail — networks time out, LLMs rate-limit, databases hiccup. Inngest makes the pipeline resilient:
| Feature | Benefit |
|---|---|
| Step-based execution | Each step is independently retried on failure — a failed DB write doesn't re-run the LLM |
| Automatic retries | Exponential backoff on transient failures (network errors, rate limits) |
| Event-driven trigger | Decoupled from the HTTP request — the UI doesn't wait for the LLM |
| Observability dashboard | Real-time job status, step-by-step logs at localhost:8288 |
| Idempotency | Safe to trigger the same summary multiple times — Step 1 exits early if it already exists |
┌─────────────────────────────────────────────────────────────┐
│ summarize-discussion │
│ │
│ ① IDEMPOTENCY CHECK │
│ └─► Query DB for storyId → return cached if found │
│ │
│ ② CONTEXT ASSEMBLY │
│ └─► Algolia: fetch story + full comment tree │
│ └─► Flatten nested comments (max depth: 3) │
│ └─► Truncate to top 200 comments (95% of signal) │
│ │
│ ③ SANITIZATION │
│ └─► Handle orphan stories (0 comments) │
│ └─► Generate graceful "No discussion yet" notice │
│ │
│ ④ LLM ORCHESTRATION │
│ └─► System prompt: Elite Analyst persona │
│ └─► User prompt: formatted story + flattened comments │
│ └─► OpenRouter (nvidia/nemotron-3-nano) │
│ └─► Structured JSON: { overview, keyPoints[], sentiment }│
│ │
│ ⑤ PERSISTENCE │
│ └─► Upsert Summary into PostgreSQL │
│ └─► UI polling resolves → AISummaryCard renders │
└─────────────────────────────────────────────────────────────┘
- Docker Desktop v20+
- Docker Compose (included with Docker Desktop)
- An OpenRouter API Key (free tier is sufficient)
The fastest way to run the full stack — app, database, and Inngest — in one command.
# 1. Clone the repository
git clone <repo-url>
cd dev-monks
# 2. Configure environment variables
cp .env.example .env
# Open .env and set OPENROUTER_API_KEY
# 3. Start all services
docker compose up --build
# 4. Visit the app
open http://localhost:3000
# 5. (Optional) Inngest job dashboard
open http://localhost:8288Note: Story browsing, search, and bookmarks work without an
OPENROUTER_API_KEY. Only the AI summarization feature requires it.
For faster iteration without Docker.
# 1. Install dependencies
npm install
# 2. Set up environment variables
cp .env.example .env
# Set DATABASE_URL (your local Postgres) and OPENROUTER_API_KEY
# 3. Set up the database
npx prisma generate
npx prisma migrate deploy
# 4. Start the Next.js dev server
npm run dev
# 5. (Optional) Start the Inngest dev server for AI background jobs
npx inngest-cli@latest dev -u http://localhost:3000/api/inngest| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
✅ | PostgreSQL connection string |
OPENROUTER_API_KEY |
Your OpenRouter API key (free at openrouter.ai) | |
INNGEST_EVENT_KEY |
Docker only | Set automatically by Docker Compose |
INNGEST_SIGNING_KEY |
Docker only | Set automatically by Docker Compose |
The official HN Firebase API requires one HTTP request per item. A story with 500 comments requires 500+ sequential roundtrips — at ~50ms each, that's 25+ seconds of latency.
Algolia's Search API returns the entire comment tree in a single request, making it:
- 10–20× faster for page loads
- Essential for AI summarization (we need all comments at once to pass to the LLM)
- The only viable approach for a smooth UX
The minor tradeoff is a slight indexing delay vs. real-time Firebase data.
LLM calls are slow, failure-prone, and expensive to re-run. A naive implementation inside a Next.js API route would:
- Time out on Vercel (60s limit)
- Lose progress on any network failure
- Re-run expensive LLM calls on retries
Inngest provides step-based checkpointing: if Step 4 (LLM call) succeeds but Step 5 (DB write) fails, only Step 5 is retried — not the LLM call. This makes the pipeline both reliable and cost-efficient.
- Free tier — no cost for experimentation and evaluation
- JSON mode — structured output prevents UI breakage
- OpenRouter abstraction — trivial to swap to Claude 3.5, GPT-4o, or Gemini via a one-line model ID change
- Sufficient quality for technical comment summarization
OAuth adds signup friction that kills conversion for "save for later" features. Cookie-based anonymous IDs give users:
- Instant bookmarks on first visit with zero sign-up
- Session-persistent saves tied to their browser
- A clear migration path — replace the cookie ID with an authenticated user ID when adding NextAuth
- Type-safe queries with full TypeScript inference
- Migration system that works identically in Docker, local Postgres, and cloud providers (Neon, Railway, Supabase)
- Connection pooling via
pgadapter, avoiding Serverless cold-start connection exhaustion
| Decision | ✅ Benefit | |
|---|---|---|
| Algolia API over Firebase | 10–20× faster story + comment fetching | Slight indexing delay vs. real-time Firebase |
| Free LLM (nemotron-3-nano) | Zero AI cost for evaluation | Lower quality vs. GPT-4o or Claude 3.5; rate limits under load |
| Cookie-based anonymous auth | Zero-friction bookmarking; no sign-up required | No cross-device sync; easy to lose on cookie clear |
| Next.js Server Components | Great SEO, faster initial page load, no client JS bloat | Requires careful "use client" boundary management for interactivity |
| Inngest for background jobs | Durable retries, observability, step checkpointing | Additional service dependency; adds Docker Compose complexity |
| Optimistic UI for bookmarks | Instant visual feedback even before DB write | Requires rollback logic and error handling |
| Snapshot pattern for bookmarks | Stable bookmarks even if HN story is deleted/flagged | Slightly stale data if a story's score/title changes significantly |
| Shared AI summaries | Cost-efficient; instant for subsequent users | Summary is computed once — doesn't update as the discussion grows |
- 🔐 Authentication — Add NextAuth.js for cross-device bookmark syncing and user accounts
- ⚡ Real-time Updates — WebSocket or SSE for live score and comment count updates
- 🚀 Redis Caching — Cache hot summaries and story feeds to reduce DB + Algolia load
- 🧠 Better AI Models — One-line swap to Claude 3.5 Sonnet or GPT-4o via OpenRouter
- 🔍 Full-Text Search — Search across bookmarks and AI-generated summaries
- 🌙 Dark Mode — User-selectable theme with
next-themes - 🧪 Testing — E2E tests with Playwright and Inngest failure simulation
- 📊 Analytics — Track which stories get summarized most, summary quality feedback
- 🔔 Notifications — Alert users when a summary is ready (email or browser push)
- 📤 Export — Export bookmarks and summaries to Markdown, Notion, or Obsidian
dev-monks/
│
├── 🐳 docker-compose.yml # Multi-service orchestration (app + postgres + inngest)
├── 🐳 Dockerfile # Multi-stage production build
│
├── prisma/
│ ├── schema.prisma # DB schema — Bookmark & Summary models
│ └── migrations/ # Version-controlled migration history
│
├── scripts/
│ └── docker-entrypoint.sh # DB readiness check + auto-migration on startup
│
└── src/
├── app/
│ ├── page.tsx # 🏠 Home feed — infinite scroll story listing
│ ├── story/[id]/ # 📄 Story detail — full comment tree + AI summary
│ ├── bookmarks/ # 🔖 Saved stories page
│ ├── actions/
│ │ └── bookmarks.ts # ⚡ Server Actions — toggle, list, check bookmarks
│ └── api/
│ ├── stories/ # 📡 REST endpoints — feed, featured, summarize
│ └── inngest/ # 🔁 Inngest webhook receiver
│
├── components/
│ ├── hn/ # 🧩 Domain components (PostCard, Comment, SummarizeButton, AISummaryCard)
│ ├── layout/ # 🎨 Layout (Navbar, Hero, Footer)
│ └── ui/ # 🔧 Reusable UI (Skeletons, ErrorAlert, Icons)
│
├── context/
│ └── BookmarkContext.tsx # 🗃️ Global bookmark state with optimistic updates
│
├── cookies/ # 🍪 Anonymous user ID generation & retrieval
├── hooks/
│ └── useStories.ts # 🪝 React Query hook — infinite paginated story feed
│
├── inngest/
│ └── summarize-discussion.ts # 🤖 5-step durable AI summarization function
│
├── lib/
│ ├── hn-api.ts # 🌐 Algolia HN API client
│ ├── ai-service.ts # 🧠 OpenRouter LLM integration
│ └── prisma.ts # 🗄️ Prisma client singleton
│
├── types/ # 📐 TypeScript type definitions
└── utils/ # 🛠️ Helpers — date formatting, text truncation, HTML sanitization
Built with ❤️ by Dev Monks
If this project helped you, please consider giving it a ⭐ on GitHub!


