Vite/React + shadcn/ui frontend with Superhuman-style keyboard nav (j/k/o/v/e/s/u, feed switching, localStorage caching for instant loads) backed by an Express API that polls the HN Firebase API into Postgres and serves cached feeds/comments. Instrumented with OpenTelemetry (traces/metrics/logs via OTLP/HTTP).
2.7 KiB
HN Reader
A lightweight, keyboard-driven Hacker News reader. Navigate like
Superhuman/Linear: j/k to move, o/Enter to read, e to
archive, s to star, numbers to switch feeds. Feed data is cached in
localStorage for instant loads, then revalidated in the background.
Press ? in the app for the full shortcut list.
Stack
- Frontend: Vite + React + TypeScript, shadcn/ui (Radix primitives, Tailwind v4, Nova preset)
- Backend: Express (
server/), polls the HN Firebase API every 5 minutes and caches stories in Postgres - Database: Postgres 16 (Docker), schema in
scripts/init.sql - Observability: OpenTelemetry traces/metrics/logs shipped via OTLP/HTTP
Architecture
src/ React SPA (feeds, keyboard nav, comments dialog)
server/ Express API + HN sync job + OTel bootstrap
scripts/ DB schema, backup script
docker-compose.yml Postgres container for this app
.gitea/workflows/ CI: lint, typecheck, build
The API caches HN's top/new/best/ask/show/job feeds (100 items each)
in Postgres so the frontend never talks to Hacker News directly.
Per-story read/starred/archived state lives in story_state and is
returned inline with each feed response.
Local development
cp .env.example .env # fill in POSTGRES_PASSWORD
docker compose up -d # start Postgres
npm install
npm run dev:server # API on :3012 (loaded from .env)
npm run dev # Vite dev server on :3012, proxies /api
Production
Runs as the app-54d6877c systemd unit: builds are not automated on
deploy, so after pulling changes run npm run build then restart the
service (sudo systemctl restart app-54d6877c). The compiled SPA is
served as static files by the same Express process that serves /api,
on port 3012.
Nightly Postgres backups run via the app-54d6877c-backup.timer
systemd timer (scripts/backup.sh), keeping 7 days of pg_dump
snapshots in ~/backups.
Design system
Built with shadcn/ui (components.json, Radix base, Nova preset).
Components live under src/components/ui and are added with
npx shadcn@latest add <component> — nothing here is hand-rolled, so
swapping in a custom registry later (e.g. a privatecloud registry)
is a matter of updating components.json's registries field and
re-running add against it.
Observability
The API sends traces, metrics, and logs to an OTLP/HTTP collector at
OTEL_EXPORTER_OTLP_ENDPOINT (defaults to http://localhost:4318),
instrumenting HTTP, Express, and Postgres calls automatically via
@opentelemetry/auto-instrumentations-node. View it in Grafana at
localhost:3400.