# RSSHub Admin Guide

This guide is the operational and architectural reference for **RSSHub** — the open-source, self-hostable feed-generation framework. It is written for the people who run it and the people who extend it: operators deploying and tuning an instance, and developers adding routes or understanding the internals.

RSSHub is a TypeScript application built on the [Hono](https://hono.dev) web framework. A single instance loads **over 1,500 route namespaces** (thousands of individual routes), each of which scrapes one source and returns a normalized feed. The same codebase runs unchanged on Node.js, Cloudflare Workers, Vercel, and Fly.io because Hono is runtime-agnostic.

If you are looking for how to *consume* feeds (URL structure, output formats, query parameters), see the [User Guide](/DIYgod/RSSHub/user-guide/). This guide covers everything behind the feed.

---

## 1. Executive Summary

| Attribute | Value |
|-----------|-------|
| Project | RSSHub — "Everything is RSSible" |
| Author | DIYgod and contributors |
| License | AGPL-3.0 |
| Language | TypeScript (ESM, `"type": "module"`) |
| Runtime | Node.js `^22.20.0 \|\| ^24`; also Cloudflare Workers, Vercel, Bun-class runtimes |
| Web framework | Hono `4.x` |
| Package manager | pnpm `10.x` |
| Default port | `1200` |
| HTTP surface | `GET /:namespace/*` (feeds), `GET /api/*` (metadata API), plus `/`, `/healthz`, `/robots.txt`, `/metrics` |
| Caching backends | in-memory (LRU), Redis, or external HTTP cache |
| Configuration | Environment variables, read in `lib/config.ts` |

**What it does.** RSSHub receives an HTTP `GET` for a feed path, dispatches to the matching route handler, which fetches and parses a source into a normalized data structure, then a middleware chain post-processes that data (filtering, full-text, hotlink rewriting) and renders it as RSS, Atom, JSON Feed, or RSS3.

**What makes it operationally distinctive.** Routes are auto-discovered from the filesystem and compiled into a static registry at build time. A single shared middleware chain implements every universal query parameter once, so all routes behave consistently. Caching is request-coalescing: concurrent requests for the same feed are serialized so the source is hit at most once per cache window.

---

## 2. Architecture

### 2.1 Request lifecycle

```text
            ┌──────────────────────────────────────────────────────────────┐
  HTTP GET  │  Hono app (lib/app-bootstrap.tsx)                             │
 ─────────► │                                                              │
            │  GLOBAL MIDDLEWARE (run in order, each wraps the next):      │
            │    trimTrailingSlash → compress → jsxRenderer                 │
            │    → logger → trace → honeybadger → sentry                    │
            │    → access-control → debug → template → header               │
            │    → anti-hotlink → parameter → cache                         │
            │                          │                                    │
            │                          ▼                                    │
            │  ROUTE REGISTRY (lib/registry.ts)                             │
            │    app.basePath('/'+namespace).get(routePath, handler)        │
            │                          │                                    │
            │                          ▼                                    │
            │  ROUTE HANDLER (lib/routes/<ns>/<route>.ts)                   │
            │    fetch source → parse → return normalized Data              │
            │                          │                                    │
            │      (unwinds back up the middleware chain)                   │
            │                          ▼                                    │
            │  template middleware renders Data → RSS / Atom / JSON / RSS3  │
            └──────────────────────────────────────────────────────────────┘
                                       │
                                       ▼  Response
```

The chain is declared in `lib/app-bootstrap.tsx`. Each middleware calls `await next()` and then runs its post-processing as the stack unwinds — so `template`, `header`, `anti-hotlink`, `parameter`, and `cache` all act on the route's output *after* the handler returns. The route registry (`lib/registry.ts`) is mounted last with `app.route('/', registry)`; the metadata API is mounted at `app.route('/api', api)`.

### 2.2 The middleware chain in detail

| Order | Middleware | Source | Responsibility |
|------:|-----------|--------|----------------|
| 1 | `trimTrailingSlash` | `hono/trailing-slash` | Normalize trailing slashes. |
| 2 | `compress` | `hono/compress` | gzip the response. |
| 3 | `jsxRenderer` | `hono/jsx-renderer` | Stream JSX as XML (the RSS/Atom views are JSX). Sets the XML doctype. |
| 4 | `logger` | `lib/middleware/logger.ts` | Per-request logging. |
| 5 | `trace` | `lib/middleware/trace.ts` | OpenTelemetry request tracing/metrics. |
| 6 | `honeybadger` | `lib/middleware/honeybadger.ts` | Honeybadger error reporting (if configured). |
| 7 | `sentry` | `lib/middleware/sentry.ts` | Sentry error reporting (if configured). |
| 8 | `access-control` | `lib/middleware/access-control.ts` | Enforce `ACCESS_KEY` via `?key=`/`?code=`. |
| 9 | `debug` | `lib/middleware/debug.ts` | Debug counters and `?debug=` gating. |
| 10 | `template` | `lib/middleware/template.tsx` | Render `Data` into the requested output format. |
| 11 | `header` | `lib/middleware/header.ts` | Response headers (CORS via `ALLOW_ORIGIN`, etc.). |
| 12 | `anti-hotlink` | `lib/middleware/anti-hotlink.ts` | Rewrite media URLs through a hotlink template. |
| 13 | `parameter` | `lib/middleware/parameter.ts` | Apply universal feed parameters (filter, limit, fulltext, brief, opencc, chatgpt, …). |
| 14 | `cache` | `lib/middleware/cache.ts` | Serve from / populate the route cache; coalesce concurrent identical requests. |

> [!NOTE]
> Because middleware post-processing runs as the stack unwinds, the *textual* order above is also the response-shaping order in reverse: the handler produces `Data`, then `parameter` filters it, then `anti-hotlink` rewrites media, then `template` serializes it. `cache` wraps the whole route execution to store the serialized result.

### 2.3 Route discovery and the registry

Routes live under `lib/routes/<namespace>/`. Each namespace directory contains:

- `namespace.ts` — exports a `namespace: Namespace` object (display name, source `url`, `description`, language, categories).
- One file per route — exports a `route: Route` object (path template, parameters, example, handler, radar rules, feature flags).

How they get loaded depends on `NODE_ENV` (`lib/registry.ts`):

| Mode | Source of routes |
|------|------------------|
| `development` (default) | `directoryImport()` scans `lib/routes/` at startup and imports every `.ts`/`.tsx`. |
| `production` | Pre-built `assets/build/routes.js` (generated by `pnpm build:routes`). |
| `test` | Pre-built `assets/build/routes.json`. |
| Package/library (`IS_PACKAGE`) | `assets/build/routes.js`. |

For each namespace the registry creates a Hono sub-app with `app.basePath('/'+namespace)` and registers every route's path as a `GET` handler. Routes are **sorted so literal path segments take priority over `:parameter` segments**, ensuring specific paths match before catch-alls. Route handlers are lazy: the handler module is imported on first hit in production via a `module()` thunk.

> [!IMPORTANT]
> Adding or renaming a route requires rebuilding the static registry for production/worker deployments (`pnpm build:routes`). In development the directory scan picks up changes on restart. A route present on disk but missing from `assets/build/routes.js` will 404 in production.

### 2.4 The route contract (`Route` type)

Defined in `lib/types.ts`. Every route exports an object with these fields:

| Field | Required | Purpose |
|-------|:--------:|---------|
| `path` | ✅ | Path template, e.g. `/issue/:user/:repo/:state?/:labels?`. May be an array of templates. |
| `name` | ✅ | Human-readable route name (also the default Radar rule title). |
| `maintainers` | ✅ | GitHub usernames responsible for the route. |
| `handler` | ✅ | `(ctx) => Data \| Response \| null` — does the actual fetching/parsing. |
| `example` | ✅ | A working example URL, shown in docs and used by tests. |
| `parameters` | – | Per-segment docs: description, default, and enumerated `options`. |
| `categories` | – | Catalog categories (e.g. `programming`, `social-media`). |
| `view` | – | Suggested reader view type (`ViewType`). |
| `radar` | – | Source→target rules for RSSHub Radar auto-detection. |
| `description` | – | Extra notes (Markdown). |
| `features` | – | Capability/requirement flags (see below). |

The `features` object signals operational requirements:

| Flag | Meaning |
|------|---------|
| `requireConfig` | Route needs environment configuration (named keys with descriptions); used to surface "this route needs a token/cookie". |
| `requirePuppeteer` | Route drives a headless browser (Playwright/Puppeteer). |
| `antiCrawler` | Source has anti-bot measures. |
| `supportBT` / `supportPodcast` / `supportScihub` | Route emits BitTorrent enclosures / podcast metadata / supports the `scihub` parameter. |
| `nsfw` | Route is NSFW — filtered out entirely when `DISABLE_NSFW=true`. |

### 2.5 Output rendering

The `template` middleware (`lib/middleware/template.tsx`) selects the serializer from `?format=`:

| `format` | Renderer | Notes |
|----------|----------|-------|
| `rss` (default) | `<RSS>` JSX view | RSS 2.0 XML. |
| `atom` | `<Atom>` JSX view | Atom 1.0 XML. |
| `json` | `json()` | JSON Feed; `Content-Type: application/feed+json`. |
| `rss3` / `ums` | `rss3()` | RSS3 JSON (`ums` retained for backward compatibility). |
| `debug.json` | raw debug JSON | Only when `DEBUG_INFO` is enabled. |
| `N.debug.html` | item N's raw description HTML | Only when `DEBUG_INFO` is enabled; for route debugging. |

The middleware also normalizes data before rendering: collapses whitespace, trims titles to `TITLE_LENGTH_LIMIT`, strips Unicode control characters from descriptions, converts dates to ISO-8601 for non-RSS formats, and computes the RSS `<ttl>`.

### 2.6 The metadata API (`/api`)

Mounted at `/api`, built with `@hono/zod-openapi` (`lib/api/index.ts`). It powers the documentation site, RSSHub Radar, and external tooling:

| Endpoint | Purpose |
|----------|---------|
| `GET /api/namespace/all` | All namespaces + routes. |
| `GET /api/namespace/:namespace` | One namespace's routes. |
| `GET /api/category/:category` | Routes by category. |
| `GET /api/radar/rules/all` | All Radar detection rules. |
| `GET /api/radar/rules/:domain` | Radar rules for a domain. |
| `GET /api/route/status` | Route status data. |
| `GET /api/follow/config` | Folo integration config. |
| `GET /api/openapi.json` | OpenAPI 3.1 document. |
| `GET /api/reference` | Interactive API reference (Scalar UI). |

---

## 3. System Requirements

| Resource | Recommendation |
|----------|----------------|
| Node.js | `^22.20.0 \|\| ^24` (only if running from source/npm; the Docker image bundles Node 24). |
| Memory | ≥512 MB for a basic instance; ≥1–2 GB if you enable the headless browser (Playwright/Chromium) for browser-dependent routes. |
| Redis | Optional but recommended for production caching (`redis:alpine` is sufficient). |
| Headless browser | Optional. Needed only for routes with `requirePuppeteer`. Provide via a remote endpoint (`PLAYWRIGHT_WS_ENDPOINT`) or the `chromium-bundled` image. |
| Disk | Modest for the app; the Chromium-bundled image is substantially larger. |
| Outbound network | RSSHub makes outbound HTTP to source sites; many routes target sites that may require a proxy in certain regions. |

> [!TIP]
> Start with the lightweight default image (no bundled browser) and add a separate headless-browser container only when you actually need a `requirePuppeteer` route. It keeps memory and image size down.

---

## 4. Configuration Reference

All configuration is via **environment variables**, parsed once in `lib/config.ts`. Values are read from `process.env` (and a `.env` file via `dotenv`). Booleans accept `true`/`1` for true and `false`/`0`/empty for false. This section is the authoritative, source-derived reference.

> [!NOTE]
> Defaults below are exactly as coded in `lib/config.ts`. An empty "Default" cell means the value is unset/optional unless noted.

### 4.1 Application

| Variable | Default | Purpose |
|----------|---------|---------|
| `DISALLOW_ROBOT` | `false` | Block search-engine crawlers (adds robots directives). |
| `ENABLE_CLUSTER` | `false` | Run in Node cluster mode (multiple workers). |
| `IS_PACKAGE` | unset | Internal: instance is running as the published npm library. |
| `NODE_NAME` | unset | A name for this node, surfaced in logs/metrics. |
| `PUPPETEER_WS_ENDPOINT` | unset | WebSocket endpoint of a remote Puppeteer browser. |
| `PLAYWRIGHT_WS_ENDPOINT` | falls back to `PUPPETEER_WS_ENDPOINT` | WebSocket endpoint of a remote Playwright browser. |
| `CHROMIUM_EXECUTABLE_PATH` | unset | Path to a local Chromium binary (set automatically in the arm Docker image). |

### 4.2 Network

| Variable | Default | Purpose |
|----------|---------|---------|
| `PORT` | `1200` | Listening port. |
| `LISTEN_INADDR_ANY` | `true` | Allow connections from any address (public). Set `0` to bind localhost-only. |
| `DISABLE_IPV6` | `false` | Force IPv4 for outbound requests. |
| `REQUEST_RETRY` | `2` | Outbound request retry count on failure. |
| `REQUEST_TIMEOUT` | `30000` | Outbound request timeout (ms). |
| `UA` | a Chrome desktop UA string | User-Agent for outbound requests. |
| `NO_RANDOM_UA` | `false` | If true (and `UA` unset), use the fixed RSSHub UA instead of a randomized one. |
| `ALLOW_ORIGIN` | unset | CORS `Access-Control-Allow-Origin` value for the API/feeds. |

### 4.3 Caching

RSSHub caches route output to avoid re-scraping sources. The cache key is `xxhash64(path + format + limit)`, so different formats and limits cache independently.

| Variable | Default | Purpose |
|----------|---------|---------|
| `CACHE_TYPE` | `memory` | `memory` (LRU), `redis`, `http`, or empty string to disable caching. |
| `CACHE_EXPIRE` | `300` | Route cache TTL in seconds (also drives the RSS `<ttl>`). |
| `CACHE_CONTENT_EXPIRE` | `3600` | TTL for "unchanging" content (e.g. full-text article bodies), seconds. |
| `CACHE_REQUEST_TIMEOUT` | `60` | Request-coalescing lock TTL (seconds) — how long a duplicate request waits. |
| `MEMORY_MAX` | `256` | Max items in the in-memory LRU cache. |
| `REDIS_URL` | `redis://localhost:6379/` | Redis connection URL (when `CACHE_TYPE=redis`). |
| `CACHE_HTTP_URL` | unset | External HTTP cache endpoint (when `CACHE_TYPE=http`). |
| `CACHE_HTTP_TOKEN` | unset | Auth token for the HTTP cache. |

> [!WARNING]
> Setting `CACHE_TYPE=''` (empty) disables caching entirely. The instance will re-scrape on every request — heavy load on both RSSHub and the source sites, and it lowers the RSS `<ttl>` to 1 minute. Only do this for debugging.

### 4.4 Proxy

For reaching sources that are geo-blocked or rate-limited. Configure either a single URI, a list, or discrete parts.

| Variable | Default | Purpose |
|----------|---------|---------|
| `PROXY_URI` | unset | Single proxy URI (`http://`, `https://`, `socks://`, etc.). |
| `PROXY_URIS` | unset | Comma-separated list of proxy URIs (load-balanced/failover). |
| `PROXY_PROTOCOL` | unset | Proxy protocol when specifying parts. |
| `PROXY_HOST` | unset | Proxy host. |
| `PROXY_PORT` | unset | Proxy port. |
| `PROXY_AUTH` | unset | Proxy `Authorization` value. |
| `PROXY_URL_REGEX` | `.*` | Only proxy outbound URLs matching this regex. |
| `PROXY_STRATEGY` | `all` | `all` (proxy every request) or `on_retry` (only after a direct attempt fails). |
| `PROXY_FAILOVER_TIMEOUT` | `5000` | Failover timeout (ms) when using `PROXY_URIS`. |
| `PROXY_HEALTH_CHECK_INTERVAL` | `60000` | Proxy health-check interval (ms). |
| `PAC_URI` | unset | Proxy auto-config (PAC) file URI. |
| `PAC_SCRIPT` | unset | Inline PAC script. |

### 4.5 Access control

| Variable | Default | Purpose |
|----------|---------|---------|
| `ACCESS_KEY` | unset | If set, every feed request must include `?key=<ACCESS_KEY>` or `?code=<md5(path+ACCESS_KEY)>`. The root, `robots.txt`, `favicon.ico`, and `logo.png` remain open. |

> [!IMPORTANT]
> `ACCESS_KEY` is the only built-in authentication. It gates feed access, not the `/api` metadata endpoints' shape — but those run behind the same middleware, so a set key protects them too (except the always-open paths). It does **not** encrypt anything; treat the key as a shared secret and serve over HTTPS.

### 4.6 Logging, errors, and observability

| Variable | Default | Purpose |
|----------|---------|---------|
| `DEBUG_INFO` | `true` | Controls the `/` debug panel, `/metrics`, and `debug.json`/`.debug.html` formats. `true` = always on; `false` = off; any other string = on only when the request includes `?debug=<that string>`. |
| `LOGGER_LEVEL` | `info` | Winston log level (`debug`, `info`, `warn`, `error`). |
| `NO_LOGFILES` | `false` | Disable writing log files (log to console only). |
| `SHOW_LOGGER_TIMESTAMP` | `false` | Prefix log lines with timestamps. |
| `OTEL_SECONDS_BUCKET` | `0.01,0.1,1,2,5,15,30,60` | OpenTelemetry histogram buckets (seconds). |
| `OTEL_MILLISECONDS_BUCKET` | `10,20,50,100,250,500,1000,5000,15000` | OpenTelemetry histogram buckets (ms). |
| `HONEYBADGER_API_KEY` | unset | Enable Honeybadger error reporting. |
| `SENTRY` | unset | Sentry DSN (enables Sentry error reporting). |
| `SENTRY_ROUTE_TIMEOUT` / `ERROR_TRACKING_ROUTE_TIMEOUT` | `30000` | Report routes slower than this (ms) to error tracking. |
| `ENABLE_REMOTE_DEBUGGING` | `false` | Expose a remote Node debugging endpoint (use only in trusted networks). |

> [!CAUTION]
> Leaving `DEBUG_INFO=true` on a public instance exposes a debug panel and `/metrics` with route hit/error counts and recently-requested paths. For a hardened public deployment set `DEBUG_INFO=false`, which also disables `/metrics`.

### 4.7 Feed behavior

| Variable | Default | Purpose |
|----------|---------|---------|
| `TITLE_LENGTH_LIMIT` | `150` | Max item title length (bytes; CJK chars count as 2) before truncation with `...`. |
| `SUFFIX` | unset | HTML appended to every item description. |
| `FILTER_REGEX_ENGINE` | `re2` | Engine for `filter*` parameters: `re2` (safe, linear-time) or `regexp` (full JS RegExp, allows backtracking). |
| `ALLOW_USER_HOTLINK_TEMPLATE` | `false` | Allow users to pass `image_hotlink_template` / `multimedia_hotlink_template` query params. |
| `HOTLINK_TEMPLATE` | unset | Operator-forced media-URL rewrite template (overrides user templates). Supports `${property}` and `${property_ue}` (URL-encoded) interpolation over URL parts (`host`, `pathname`, `href`, …). |
| `HOTLINK_INCLUDE_PATHS` | unset | Comma-separated route-path prefixes to which the hotlink template applies. |
| `HOTLINK_EXCLUDE_PATHS` | unset | Comma-separated route-path prefixes to exclude from the hotlink template. |
| `ALLOW_USER_SUPPLY_UNSAFE_DOMAIN` | `false` | Permit routes that take a user-supplied target domain (SSRF risk — see Security). |
| `DISABLE_NSFW` | `false` | Filter out all routes flagged `features.nsfw` at registry build. |

> [!NOTE]
> The hotlink template only accepts a fixed allowlist of URL properties (`hash`, `host`, `hostname`, `href`, `origin`, `password`, `pathname`, `port`, `protocol`, `search`, `searchParams`, `username`). An unknown property throws `Invalid URL property`. This validation is the guardrail that makes user-supplied templates safe-ish — but keep `ALLOW_USER_HOTLINK_TEMPLATE=false` unless you need it.

### 4.8 OpenAI / AI features (the `chatgpt` parameter)

| Variable | Default | Purpose |
|----------|---------|---------|
| `OPENAI_API_KEY` | unset | Enables the `?chatgpt=` parameter. Without it, `chatgpt` is a no-op. |
| `OPENAI_MODEL` | `gpt-3.5-turbo-16k` | Model name. |
| `OPENAI_API_ENDPOINT` | `https://api.openai.com/v1` | API base URL (point at any OpenAI-compatible endpoint). |
| `OPENAI_TEMPERATURE` | `0.2` | Sampling temperature. |
| `OPENAI_MAX_TOKENS` | unset | Max completion tokens. |
| `OPENAI_INPUT_OPTION` | `description` | What to process: `description`, `title`, or `both`. |
| `OPENAI_PROMPT` | "Please summarize the following article…" | System prompt for description processing. |
| `OPENAI_PROMPT_TITLE` | "Please translate the following title into Simplified Chinese…" | System prompt for title processing. |

### 4.9 Folo integration

| Variable | Default | Purpose |
|----------|---------|---------|
| `FOLLOW_OWNER_USER_ID` | unset | Folo owner user ID for this instance. |
| `FOLLOW_DESCRIPTION` | unset | Instance description shown in Folo. |
| `FOLLOW_PRICE` | unset | Listing price (Folo marketplace). |
| `FOLLOW_USER_LIMIT` | unset | User limit for the instance listing. |

### 4.10 Remote configuration

| Variable | Default | Purpose |
|----------|---------|---------|
| `REMOTE_CONFIG` | unset | URL to fetch a JSON config object at startup; its keys are merged over the environment and config is recomputed. |
| `REMOTE_CONFIG_AUTH` | unset | Value for the `Authorization: Basic` header sent when fetching `REMOTE_CONFIG`. |

> [!WARNING]
> `REMOTE_CONFIG` loads configuration over the network at boot. If the fetch fails the instance logs an error and continues with local config. Only point it at a source you fully control and serve it over HTTPS — it can inject any setting, including credentials and proxies.

### 4.11 Route-specific credentials (per-site)

Beyond the operational settings above, `lib/config.ts` defines **~150 site-specific** variables that supply cookies, tokens, API keys, or login credentials to individual routes. A route's docs (and its `features.requireConfig`) tell you which it needs. These follow consistent naming conventions:

| Pattern | Examples | Notes |
|---------|----------|-------|
| `<SITE>_COOKIE` / `<SITE>_COOKIES` | `DOUBAN_COOKIE`, `WEIBO_COOKIES`, `ZHIHU_COOKIES`, `LOFTER_COOKIE` | A logged-in session cookie for the source. |
| `<SITE>_TOKEN` / `<SITE>_*_TOKEN` | `NOTION_TOKEN`, `TELEGRAM_TOKEN`, `READWISE_ACCESS_TOKEN`, `ZSXQ_ACCESS_TOKEN` | API or session tokens. |
| `<SITE>_USERNAME` + `<SITE>_PASSWORD` | `IWARA_USERNAME`/`IWARA_PASSWORD`, `DIDA365_*`, `MANGADEX_*`, `NHENTAI_*` | Login credential pairs. |
| `<SITE>_API_KEY` / `<SITE>_KEY` | `DISQUS_API_KEY`, `LASTFM_API_KEY`, `PIXABAY_KEY`, `YOUTUBE_KEY` | Third-party API keys. |
| OAuth client triplets | `SPOTIFY_CLIENT_ID`/`SPOTIFY_CLIENT_SECRET`/`SPOTIFY_REFRESHTOKEN`, `TUMBLR_*`, `YOUTUBE_CLIENT_ID/SECRET/REFRESH_TOKEN`, `TWITTER_CONSUMER_*`/`TWITTER_ACCESS_*` | Full OAuth app credentials. |

A handful use a **dynamic suffix** so you can supply several accounts for the same source. The suffix after the prefix becomes a lookup key:

| Prefixed variable | Keyed by | Used for |
|-------------------|----------|----------|
| `BILIBILI_COOKIE_<uid>` | the bilibili UID | Per-account bilibili cookies. |
| `DISCUZ_COOKIE_<cid>` | a Discuz config id | Per-site Discuz cookies. |
| `MEDIUM_COOKIE_<username>` | a Medium username (lowercased) | Per-account Medium cookies. |
| `EMAIL_CONFIG_<id>` | an arbitrary id | IMAP mailbox config for email-to-RSS routes. |
| `DISCOURSE_CONFIG_<id>` | an arbitrary id | A JSON Discourse instance config (host + key). |

> [!TIP]
> For the complete, current list of which routes need which variable, consult each route's page on [docs.rsshub.app](https://docs.rsshub.app) or the source of truth — the `ConfigEnvKeys` union and the matching `config` object in `lib/config.ts`. Provide only the credentials for routes you actually use; every credential variable is optional.

> [!CAUTION]
> These variables hold real account credentials. Inject them as secrets (Docker secrets, `wrangler secret put`, your platform's secret store), never bake them into an image or commit them. A leaked `*_COOKIE` is a full session hijack of that account.

---

## 5. Deployment

The same build runs on several targets. The npm `scripts` (`package.json`) define each path.

### 5.1 Docker (recommended)

Single container, in-memory cache:

```bash
docker run -d --name rsshub -p 1200:1200 --restart always diygod/rsshub
```

Production stack with Redis and a headless browser — `docker-compose.yml` ships in the repo:

```yaml
services:
    rsshub:
        image: diygod/rsshub
        restart: always
        ports:
            - '1200:1200'
        environment:
            NODE_ENV: production
            CACHE_TYPE: redis
            REDIS_URL: 'redis://redis:6379/'
            PLAYWRIGHT_WS_ENDPOINT: 'ws://browserless:3000'
        healthcheck:
            test: ['CMD', 'curl', '-f', 'http://localhost:1200/healthz']
            interval: 30s
            timeout: 10s
            retries: 3
        depends_on:
            - redis
            - browserless
    browserless:
        image: browserless/chrome
        restart: always
    redis:
        image: redis:alpine
        restart: always
        volumes:
            - redis-data:/data
volumes:
    redis-data:
```

```bash
docker compose up -d
```

```text
[+] Running 3/3
 ✔ Container rsshub-redis-1        Started
 ✔ Container rsshub-browserless-1  Started
 ✔ Container rsshub-rsshub-1       Started
```

> [!TIP]
> The image tag `diygod/rsshub:chromium-bundled` ships Chromium inside the RSSHub container, so you can drop the separate `browserless` service (and its `PLAYWRIGHT_WS_ENDPOINT`) at the cost of more memory and a larger image. The `docker-compose.yml` comments mark exactly which lines to remove for the bundled approach.

The image exposes `1200`, runs `npm run start` under `dumb-init` as PID-1, and defaults to `TZ=Asia/Shanghai` — override `TZ` if you want local timestamps.

### 5.2 Cloudflare Workers

RSSHub builds a Worker bundle (`wrangler.toml`, `dist-worker/worker.mjs`). Caching uses a KV namespace, and a Browser binding provides Playwright.

```bash
npm run worker-deploy   # builds the worker bundle, then `wrangler deploy`
```

> [!IMPORTANT]
> On Workers, set sensitive config with `wrangler secret put <KEY>` (e.g. `ACCESS_KEY`, `GITHUB_ACCESS_TOKEN`), not in `[vars]`. The **Workers Paid plan is recommended** — the Free plan caps CPU at 10 ms/request, which is too tight for many routes; the Paid plan allows 30 s. Browser-dependent routes (`requirePuppeteer`) need the Browser binding, which is Paid-only. The Worker uses the `nodejs_compat` flag plus Node shims in `lib/shims/` — some Node-only operations (child process, `vm`) are deliberately unsupported there.

### 5.3 Vercel

`vercel.json` declares the Hono framework preset. Build with `npm run vercel-build` (which runs `build:routes` then `tsdown --config tsdown-vercel.config.ts`). Connect the repo to Vercel and set environment variables in the project dashboard.

### 5.4 Fly.io

`fly.toml` is preconfigured: internal port `1200`, forced HTTPS, autoscaling with suspend, and a `/healthz` health check.

```bash
fly deploy
```

### 5.5 From source / npm

```bash
pnpm install
pnpm build        # build:routes + tsdown
pnpm start        # NODE_ENV=production node dist/index.mjs
```

For development with hot reload and the directory-scan route loader:

```bash
pnpm dev          # tsx watch lib/index.ts, NODE_ENV=dev
```

RSSHub is also published as a library (`rsshub` on npm, `IS_PACKAGE` mode) for embedding the feed engine into another app.

---

## 6. Operations

### 6.1 Health and observability

| Endpoint | Use |
|----------|-----|
| `GET /healthz` | Liveness probe. Returns success when the app is serving. Wire it into Docker/K8s/Fly health checks. |
| `GET /metrics` | Prometheus metrics (request counts, errors, latency histograms). Available only when `DEBUG_INFO != false`. |
| `GET /` | Landing/debug page. |

OpenTelemetry is built in (the `trace` middleware and `@opentelemetry/*` deps). Configure histogram buckets via `OTEL_SECONDS_BUCKET` / `OTEL_MILLISECONDS_BUCKET`. Errors can be shipped to Honeybadger (`HONEYBADGER_API_KEY`) and/or Sentry (`SENTRY`).

### 6.2 Logs

Winston handles logging at `LOGGER_LEVEL`. By default RSSHub writes log files; set `NO_LOGFILES=true` to log to console only (recommended for containers, where you want stdout capture). `SHOW_LOGGER_TIMESTAMP=true` adds timestamps if your log shipper does not.

```bash
docker logs -f rsshub
```

### 6.3 Caching operations

- **Pick Redis for any multi-instance or production deployment** so the cache is shared and survives restarts. In-memory caching is per-process and lost on restart.
- The request-coalescing lock (`cache.ts`) means a cold-cache feed that is requested concurrently makes the source call **once**; the other requests wait up to `CACHE_REQUEST_TIMEOUT` then either serve the now-warm cache or receive `RequestInProgressError` (503).
- Cache keys include `format` and `limit`, so `?format=json` and `?format=rss` of the same route are cached separately.
- To force a route to bypass caching, a handler can set `Cache-Control: no-cache`; the cache middleware then skips storing it.

### 6.4 Startup checklist

- [ ] Choose `CACHE_TYPE` (`redis` for production) and provide `REDIS_URL`.
- [ ] Set `DEBUG_INFO=false` for a hardened public instance.
- [ ] Set `ACCESS_KEY` if the instance is private.
- [ ] Configure a proxy (`PROXY_URI`/`PROXY_URIS`) if your sources need one.
- [ ] Provide only the route credentials you need, as secrets.
- [ ] Decide on headless-browser strategy (none / sidecar `browserless` / bundled image).
- [ ] Confirm `GET /healthz` returns success.
- [ ] If self-building for production/Workers, run `pnpm build:routes` so the static registry is current.

---

## 7. Security

> [!IMPORTANT]
> RSSHub is a server that fetches arbitrary external URLs on behalf of requests. Treat it as an outbound proxy and harden accordingly.

| Concern | Mitigation |
|---------|------------|
| **SSRF (server-side request forgery)** | Some routes accept a user-supplied target domain. These are gated behind `ALLOW_USER_SUPPLY_UNSAFE_DOMAIN` (default `false`). Keep it off unless you trust all callers; turning it on lets users make your server fetch internal/metadata endpoints. |
| **Open access** | By default any client can pull any feed. Set `ACCESS_KEY` for private instances; serve over HTTPS so the key/feeds are not sniffable. |
| **Credential exposure** | All `*_COOKIE`/`*_TOKEN`/`*_PASSWORD` variables are live account secrets. Inject as platform secrets; never commit or bake into images. A leaked cookie hijacks that account. |
| **Debug surface** | `DEBUG_INFO=true` exposes recently-requested paths, error counts, and `/metrics`. Set `false` on public instances. |
| **Hotlink template abuse** | `ALLOW_USER_HOTLINK_TEMPLATE=true` lets users rewrite media URLs; mitigated by a strict URL-property allowlist, but leave it off unless required. |
| **Regex DoS** | The default `FILTER_REGEX_ENGINE=re2` is immune to catastrophic backtracking. Only switch to `regexp` (full JS RegExp) if you trust the users supplying `filter` patterns. |
| **Remote config injection** | `REMOTE_CONFIG` fetches settings over the network at boot. Point it only at a source you control, over HTTPS, with `REMOTE_CONFIG_AUTH`. |
| **Content sanitization** | RSSHub strips `<script>` tags, removes `onclick`/`onerror`/`onload` attributes, and sets `referrerpolicy=no-referrer` on images/iframes during item processing (`parameter.ts`), reducing the risk of malicious item HTML in readers. |
| **Remote debugging** | `ENABLE_REMOTE_DEBUGGING=true` opens a Node inspector — use only inside a trusted network, never on a public host. |

### Error handling and status codes

The central error handler (`lib/errors/index.tsx`) maps failures to HTTP statuses and reports to Honeybadger/Sentry when configured:

| Error class | HTTP status | Meaning |
|-------------|:-----------:|---------|
| `RejectError` | 403 | Access control denied (bad/missing `key`/`code`). |
| `NotFoundError` | 404 | No matching route ("The route does not exist or has been deleted."). |
| `RequestInProgressError` | 503 | Duplicate request still fetching; sets a `Cache-Control` window. |
| `HTTPError` / `RequestError` / `FetchError` | 503 | Upstream source failed. |
| *(default)* | 503 | Any other route error. |

In production, error responses show only `error.message` (not the stack); non-production shows the stack. Requesting `?format=json` returns the error as JSON instead of an HTML page.

---

## 8. Codebase Map (for contributors)

```text
lib/
  app-bootstrap.tsx     Hono app + global middleware chain
  app.ts                Entry that loads request-rewriter then the app
  index.ts / server.ts  Node server entry (@hono/node-server)
  worker.ts             Cloudflare Workers entry
  config.ts             ALL environment configuration (source of truth)
  registry.ts           Route auto-discovery + Hono mounting
  types.ts              Route / Namespace / APIRoute / Data type contracts
  middleware/           The 14-stage chain (access-control, parameter, cache, …)
  views/                RSS / Atom / JSON / RSS3 renderers (JSX)
  api/                  Public metadata API (namespace, category, radar, route)
  utils/                Shared helpers: got/ofetch HTTP, cache, playwright,
                        proxy, header-generator, parse-date, render, …
  routes/<namespace>/   1,500+ source integrations (namespace.ts + route files)
  routes-deprecated/    Legacy routes kept for backward compatibility
  shims/                Node-API shims for the Workers runtime
scripts/workflow/       build-routes / build-docs / format-description tooling
assets/build/           Generated static route registry (routes.js / routes.json)
```

### Adding a route (high level)

1. Create `lib/routes/<namespace>/namespace.ts` (if the namespace is new) exporting a `Namespace`.
2. Create a route file exporting `route: Route` — fill `path`, `name`, `maintainers`, `example`, `parameters`, and an async `handler(ctx)` that returns normalized `Data` (`title`, `link`, `item[]`).
3. Add `radar` rules so RSSHub Radar can auto-detect the feed from the source URL.
4. If the route needs credentials, read them from `config` (add the key to `lib/config.ts`) and set `features.requireConfig`.
5. Mark `features.requirePuppeteer` if you drive a headless browser, `features.nsfw` if applicable.
6. Run `pnpm build:routes` and test with `pnpm dev`.

> [!NOTE]
> Handlers return *data*, not a serialized feed. The shared middleware chain handles output format, filtering, limiting, caching, and hotlink rewriting — so a route author never re-implements those. This separation is why every route supports the same universal parameters automatically.

---

## 9. Known Limitations & Improvement Areas

> [!NOTE]
> These are observations from the source at the documented commit, not defects. They are the natural trade-offs of a community-scale scraping framework.

- **Source fragility.** Each route depends on an external site's structure; sites change and routes break. The `this route is empty` guard and the maintainers list mitigate this, but breakage is inherent. A route-status dashboard (`/api/route/status`) helps operators spot failures.
- **Credential sprawl.** ~150 site-specific credential variables live in one `config.ts` union. It is exhaustive and well-organized, but onboarding a credential-heavy deployment means consulting per-route docs. A generated "which routes need which env var" matrix would help operators.
- **Workers runtime gaps.** The Cloudflare Workers target relies on shims (`lib/shims/`) that explicitly throw for `child_process`, `vm`, and some `xxhash` APIs. Routes depending on those Node features will fail on Workers — there is no static guard preventing such a route from being deployed to a Worker; it surfaces at request time.
- **Headless-browser cost.** `requirePuppeteer` routes need Chromium, which dominates memory and image size. The split (sidecar vs. bundled image vs. remote endpoint) is flexible but adds operational decisions.
- **Cache coalescing timeout.** Under a cold cache and high concurrency for the same heavy route, waiters can hit `RequestInProgressError` (503) after `CACHE_REQUEST_TIMEOUT`. Tuning that value trades responsiveness against duplicate upstream load.
- **Regex engine choice.** `re2` (default) is safe but lacks some PCRE features; `regexp` is full-featured but exposes the instance to ReDoS from user-supplied `filter` patterns. There is no per-request engine override — it is instance-wide.

---

_Generated June 2026 from DIYgod/RSSHub at 3567ced. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
