A personal, single-user, full-stack media browser. Reddit-primary, NSFW-focused, mobile-first.
The GitHub repo is still named
subreddit-media-viewerfor historical reasons. The project's name is Nightfeed.
Active personal project. Not packaged for distribution. Not intended for shared / multi-user deployment. No auth, no rate limiting beyond what upstream sources impose.
- Backend: Node 20+ (ESM), Express,
morgan,cors,dotenv. No database. - Frontend: React 18 + Vite, plain JS,
hls.jsfor HLS playback, dynamicdashjsimport for DASH. State in React hooks + localStorage (+ IndexedDB once feed-mode watch tracking lands). - Tooling: ESLint (flat config), Prettier, Vitest (frontend),
node:test(backend). SeeTESTING.md. - AI assistants:
CLAUDE.mdis the canonical project context for Claude / Copilot. Read it before contributing changes.
| Source | Endpoint prefix | Notes |
|---|---|---|
/api/subreddit, /api/reddit, /api/user |
Primary source. Comments supported in the lightbox (and feed mode once shipped). | |
| Coomer | /api/coomer |
Secondary. Creator-indexed. |
| Eporner | /api/eporner |
Secondary. |
| YouTube | /api/youtube |
Secondary. |
Adding a new source is a future architectural goal — see CLAUDE.md "Source-adapter goal."
- Subreddit browsing with
hot,new,topsorts. - Reddit filters: keyword, flair, include/exclude terms, min score, search scope, NSFW toggle, host filter.
- Multi-source unified grid with source toggle.
- Lightbox modal with comments (Reddit) and inline video playback.
- Favorites, blocklist (hidden authors), saved subreddits, recently-searched.
- Preferences: theme (dark / light / system), density.
- URL state sync — current filter set is reflected in the URL bar.
- Skeleton grids, infinite scroll, debounced search.
- In progress: TikTok-style full-screen feed mode, watch-time-aware ranking, mobile gestures, PWA install. See
issues/feed-mode-prd.md.
.
├── CLAUDE.md, memory.md, TESTING.md # project docs for humans + AI
├── README.md # this file
├── .github/
│ ├── copilot-instructions.md
│ └── workflows/ci.yml
├── issues/ # PRDs + tracer-bullet issues
├── backend/
│ ├── .env.example
│ └── src/{app.js, server.js, routes/, services/, utils/}
└── frontend/
├── .env.example
├── index.html
└── src/{App.jsx, main.jsx, components/, hooks/, utils/, styles.css}
-
Requires Node 20 or newer.
-
From the repo root:
npm install -
Create env files:
backend/.envfrombackend/.env.examplefrontend/.envfromfrontend/.env.example
-
Run both packages:
npm run dev -
Open:
- Frontend:
http://localhost:5173 - Backend:
http://localhost:3001
- Frontend:
| Var | Default | Notes |
|---|---|---|
PORT |
3001 |
|
REDDIT_BASE_URL |
https://www.reddit.com |
|
REDDIT_USER_AGENT |
(none) | Recommended; Reddit may rate-limit without one. |
| (others) | See backend/.env.example for the current set. |
| Var | Default | Notes |
|---|---|---|
VITE_API_BASE_URL |
http://localhost:3001 |
|
VITE_HTTPS |
false |
Set true to enable HTTPS dev via vite-plugin-mkcert (required for autoplay-with-sound on mobile Chrome). |
Nightfeed is built mobile-first against a Samsung Galaxy S22 Ultra. To test on a phone against a laptop-hosted backend:
-
Start the LAN-exposed dev server:
npm run dev:lanThis binds the Vite dev server to
0.0.0.0:5173. -
Find your laptop's LAN IP (
hostname -Ion Linux/Mac,ipconfigon Windows). -
On the phone, open
http://<laptop-ip>:5173. -
For sound autoplay in feed mode, you need HTTPS. Set
VITE_HTTPS=trueinfrontend/.env, restart, and trust the locally-generated cert on the phone (first visit will show a warning; accept once). -
PWA install (once issue 019 ships): Chrome menu -> "Install app" / "Add to Home screen". The home-screen icon launches Nightfeed fullscreen.
| Script | Where | Effect |
|---|---|---|
npm run dev |
root | Backend + frontend, hot reload. |
npm run dev:lan |
root | As above, frontend bound to 0.0.0.0. |
npm run build |
root | Production build of the frontend. |
npm run lint |
root | ESLint across both packages. |
npm test |
root | Test both packages (Vitest + node:test). |
GET /api/subreddit/:name
Query params: sort, after, limit, includeNsfw, timeRange, keyword, includeTerms, excludeTerms, flair, minScore, onlyRedditHosted, searchScope.
GET /api/reddit/comments?permalink=...&limit=12
GET /api/user/:username — Reddit user submissions.
GET /api/coomer/..., GET /api/eporner/..., GET /api/youtube/..., GET /api/nsfw/new, GET /api/media/..., GET /api/external/redgifs/:id.
These shift more than the Reddit routes. See the route files for the current contract: backend/src/routes/.
GET /api/health -> { ok: true, service: 'nightfeed-api' }.
- Reddit-hosted video metadata is intentionally minimal (public listing endpoints). The lightbox provides an "Open on Reddit" path for richer playback when the in-app player is preview-only.
- Upstream rate limits are not paginated around by Nightfeed; sustained heavy use will see 429s.
- No persistence beyond localStorage and (for the upcoming feed mode) IndexedDB. State is browser-bound. A future feature ("userstate backup") will mirror it to a JSON file behind the backend; see
issues/userstate-backup-prd.md(currently deferred).
This is a personal project. There's no public contribution flow. If you're working on it (or letting Claude / Copilot work on it), the contract is:
- Read
CLAUDE.md. - Pick an issue from
issues/. - Follow
TESTING.mdfor what "done" means. - Update
memory.mdif anything about state, decisions, or open questions changed.
No license currently. Treat as proprietary until one is added.