Username
fridemo· Password6wVQj3Q@2Qz@
The demo is there to try the interface.
FreshRSS has an excellent engine: solid, self-hosted, great at fetching and storing feeds. Its built-in interface just wasn't for me, and none of the alternatives fit what I wanted — so I built my own.
FriRSS sits on top of FreshRSS through its Google Reader API and replaces only the interface.
A note in the interest of honesty: this is a personal project and I'm not a developer. FriRSS was built largely by "vibe coding" — describing what I wanted to an AI assistant and iterating. Use it in that spirit.
Almost everything is yours to tweak:
FriRSS is a frontend for FreshRSS, so you need a running FreshRSS instance with the Google Reader API enabled (Settings → Authentication → Allow API access).
mkdir -p frirss-data
docker run -d --name frirss \
-p 8080:80 \
-v "$PWD/frirss-data:/app/data" \
-e TZ=Europe/Zurich \
ghcr.io/fripix/frirss:latest
The database lands in a frirss-data folder right where you ran the command. The same image is on Docker Hub as fripix/frirss:latest if you prefer it, and a ready-to-edit docker-compose.yml is included.
Then open http://localhost:8080 and create the first account — it becomes the administrator. Registration then closes: new instances refuse sign-ups by default, and you open them again from Preferences → Administration when you want to invite someone. Connect your FreshRSS server next: its URL, your FreshRSS username, and the API password from FreshRSS → Settings → Profile.
Is your FreshRSS on a private address? Most self-hosted instances are —
http://192.168.1.20:8080,http://freshrsson a Docker network, anything your router alone can reach. The backend refuses private targets by default, so a logged-in user cannot aim it at your NAS, your database, or a cloud metadata endpoint. Trust your FreshRSS host explicitly and the block lifts for that one address:-e PROXY_INTERNAL_HOSTS=192.168.1.20Skip it and connecting fails while the container log shows
POST /api/proxy 403. Public FreshRSS URLs need nothing.
The first launch generates the JWT secret and the token-encryption key and stores them in the database, so backing up the data folder backs up everything — see Backups.
| Variable | Description | Default |
|---|---|---|
PUID | User id the Node process runs as (the data directory is adopted on start) | 1000 |
PGID | Group id for the same process | 1000 |
FRIRSS_BASE_URL | Public base URL — recommended behind a reverse proxy; fixes OIDC redirect URIs | derived |
FRIRSS_DATA_DIR | SQLite database directory | /app/data |
PROXY_REWRITES | public→internal URL rewrites for the backend proxy (from=to, comma-separated) | — |
PROXY_INTERNAL_HOSTS | Anti-SSRF allowlist: private hosts the proxy may reach directly, comma-separated. Needed when FreshRSS sits on a LAN or Docker address | — |
REDIS_URL | Enables the read cache (stale-while-revalidate); empty disables it | — |
CACHE_ARTICLES_PER_FEED | Articles kept per feed in the cache | 50 |
CACHE_TTL | Cache key expiry, in seconds | 86400 |
CACHE_SYNC_INTERVAL | Background pre-fetch interval in minutes (0 disables; needs REDIS_URL) | 0 |
CACHE_SYNC_ACTIVE_DAYS | Only pre-fetch for users seen in the last N days | 7 |
CACHE_SYNC_PARALLEL_USERS | Users pre-fetched in parallel | 3 |
FRIRSS_REFRESH_MAX_FEEDS | Number of feeds to refresh per button press (non-integer or < 1 → default) | 1000 |
FRIRSS_PROXY_RATE_LIMIT | Proxied and article-extraction requests allowed per user per minute — one shared budget (0 disables; non-integer or negative → default) | 600 |
CORS_ORIGIN | Allowed CORS origin(s) — only for split front/back deployments | — |
Since 1.4.10 the server extracts article text: the browser asks
GET /api/extractfirst, and falls back to extracting the page itself only when that route is missing (an older backend), refuses the page, is busy, or cannot reach it. WithREDIS_URLset the result is cached by URL rather than by account, so a page is extracted once for the whole instance instead of once per device: a second device, or another user reading the same feed, gets it instantly. Devices asking for the same cold page at the same moment are coalesced onto a single extraction, so the origin site sees one request rather than one per reader. Without Redis the route still runs — it simply keeps nothing, so the next device pays for the extraction again. Either way the work now happens on the server rather than on every phone, so budget for it: parsing a page blocks the single Node process that serves the whole instance for tens of milliseconds up to about a second on the largest pages. That work is bounded — one page is parsed at a time and at most five requests may be in flight or waiting, beyond which the server says so and the browser extracts locally — but the bound is a queue, not extra capacity: a busy instance is one where phones do the parsing again.
Single sign-on is configured at runtime in Preferences → Administration (issuer, client ID, client secret).
Two things are worth backing up, and they are not the same thing.
Preferences → Administration → Backup produces a single file holding everything FriRSS knows about itself: accounts and their password hashes, the configured FreshRSS servers, their tokens and the key that decrypts them, preferences, and instance settings. It does not hold your articles — those live in FreshRSS.
That file is therefore enough to impersonate every account on the instance, which is why a passphrase is mandatory (12 characters minimum). Lose the passphrase and the file is permanently unusable. There is no recovery path, by design; keep it somewhere other than next to the file.
Restore it from the same screen, or from the first-run screen of a fresh instance — which makes it a migration tool as much as a backup. Either way you see what the file contains before committing: when it was made, which version produced it, how many accounts and servers. Restoring replaces the instance's contents entirely and signs everyone out.
Environment variables are recorded in the file and shown at restore time, but never applied: they belong to the deployment, not to the backup. Copy them into your compose file yourself.
/app/data holds frirss.db and its write-ahead log — the JWT secret and the
token-encryption key included. Backing up that volume backs up the instance.
To take a copy out of a running container, use the snapshot script rather than
cp:
docker exec frirss node scripts/backup-db.js /app/data/backups
It goes through SQLite's .backup() API: atomic, and safe while the server is
writing. Copying frirss.db on its own is not a valid backup — the most
recent writes live in the -wal file beside it.
Locked out of every admin account? docker exec -it frirss node scripts/reset-password.js sets a new password from the terminal.
FriRSS holds the credentials to your FreshRSS server, so a few things are not optional:
script-src 'self') along with X-Frame-Options, X-Content-Type-Options and Referrer-Policy.PUID/PGID, 1000 by default): code execution inside Node no longer owns the data directory, where the JWT secret and the token-encryption key live. The data directory is adopted on start, so upgrading needs no action.FRIRSS_PROXY_RATE_LIMIT), so an account cannot turn the backend into an open relay.Dependency and image scans run on every push and can be launched by hand; the current state is in GitHub Actions.
Found a security issue? Please follow SECURITY.md rather than opening a public issue.
FriRSS is a personal project, but feedback, ideas and contributions are welcome.
CHANGELOG.mdTypeScript
94.8%
CSS
4.0%
Username
fridemo· Password6wVQj3Q@2Qz@
The demo is there to try the interface.
FreshRSS has an excellent engine: solid, self-hosted, great at fetching and storing feeds. Its built-in interface just wasn't for me, and none of the alternatives fit what I wanted — so I built my own.
FriRSS sits on top of FreshRSS through its Google Reader API and replaces only the interface.
A note in the interest of honesty: this is a personal project and I'm not a developer. FriRSS was built largely by "vibe coding" — describing what I wanted to an AI assistant and iterating. Use it in that spirit.
Almost everything is yours to tweak:
FriRSS is a frontend for FreshRSS, so you need a running FreshRSS instance with the Google Reader API enabled (Settings → Authentication → Allow API access).
mkdir -p frirss-data
docker run -d --name frirss \
-p 8080:80 \
-v "$PWD/frirss-data:/app/data" \
-e TZ=Europe/Zurich \
ghcr.io/fripix/frirss:latest
The database lands in a frirss-data folder right where you ran the command. The same image is on Docker Hub as fripix/frirss:latest if you prefer it, and a ready-to-edit docker-compose.yml is included.
Then open http://localhost:8080 and create the first account — it becomes the administrator. Registration then closes: new instances refuse sign-ups by default, and you open them again from Preferences → Administration when you want to invite someone. Connect your FreshRSS server next: its URL, your FreshRSS username, and the API password from FreshRSS → Settings → Profile.
Is your FreshRSS on a private address? Most self-hosted instances are —
http://192.168.1.20:8080,http://freshrsson a Docker network, anything your router alone can reach. The backend refuses private targets by default, so a logged-in user cannot aim it at your NAS, your database, or a cloud metadata endpoint. Trust your FreshRSS host explicitly and the block lifts for that one address:-e PROXY_INTERNAL_HOSTS=192.168.1.20Skip it and connecting fails while the container log shows
POST /api/proxy 403. Public FreshRSS URLs need nothing.
The first launch generates the JWT secret and the token-encryption key and stores them in the database, so backing up the data folder backs up everything — see Backups.
| Variable | Description | Default |
|---|---|---|
PUID | User id the Node process runs as (the data directory is adopted on start) | 1000 |
PGID | Group id for the same process | 1000 |
FRIRSS_BASE_URL | Public base URL — recommended behind a reverse proxy; fixes OIDC redirect URIs | derived |
FRIRSS_DATA_DIR | SQLite database directory | /app/data |
PROXY_REWRITES | public→internal URL rewrites for the backend proxy (from=to, comma-separated) | — |
PROXY_INTERNAL_HOSTS | Anti-SSRF allowlist: private hosts the proxy may reach directly, comma-separated. Needed when FreshRSS sits on a LAN or Docker address | — |
REDIS_URL | Enables the read cache (stale-while-revalidate); empty disables it | — |
CACHE_ARTICLES_PER_FEED | Articles kept per feed in the cache | 50 |
CACHE_TTL | Cache key expiry, in seconds | 86400 |
CACHE_SYNC_INTERVAL | Background pre-fetch interval in minutes (0 disables; needs REDIS_URL) | 0 |
CACHE_SYNC_ACTIVE_DAYS | Only pre-fetch for users seen in the last N days | 7 |
CACHE_SYNC_PARALLEL_USERS | Users pre-fetched in parallel | 3 |
FRIRSS_REFRESH_MAX_FEEDS | Number of feeds to refresh per button press (non-integer or < 1 → default) | 1000 |
FRIRSS_PROXY_RATE_LIMIT | Proxied and article-extraction requests allowed per user per minute — one shared budget (0 disables; non-integer or negative → default) | 600 |
CORS_ORIGIN | Allowed CORS origin(s) — only for split front/back deployments | — |
Since 1.4.10 the server extracts article text: the browser asks
GET /api/extractfirst, and falls back to extracting the page itself only when that route is missing (an older backend), refuses the page, is busy, or cannot reach it. WithREDIS_URLset the result is cached by URL rather than by account, so a page is extracted once for the whole instance instead of once per device: a second device, or another user reading the same feed, gets it instantly. Devices asking for the same cold page at the same moment are coalesced onto a single extraction, so the origin site sees one request rather than one per reader. Without Redis the route still runs — it simply keeps nothing, so the next device pays for the extraction again. Either way the work now happens on the server rather than on every phone, so budget for it: parsing a page blocks the single Node process that serves the whole instance for tens of milliseconds up to about a second on the largest pages. That work is bounded — one page is parsed at a time and at most five requests may be in flight or waiting, beyond which the server says so and the browser extracts locally — but the bound is a queue, not extra capacity: a busy instance is one where phones do the parsing again.
Single sign-on is configured at runtime in Preferences → Administration (issuer, client ID, client secret).
Two things are worth backing up, and they are not the same thing.
Preferences → Administration → Backup produces a single file holding everything FriRSS knows about itself: accounts and their password hashes, the configured FreshRSS servers, their tokens and the key that decrypts them, preferences, and instance settings. It does not hold your articles — those live in FreshRSS.
That file is therefore enough to impersonate every account on the instance, which is why a passphrase is mandatory (12 characters minimum). Lose the passphrase and the file is permanently unusable. There is no recovery path, by design; keep it somewhere other than next to the file.
Restore it from the same screen, or from the first-run screen of a fresh instance — which makes it a migration tool as much as a backup. Either way you see what the file contains before committing: when it was made, which version produced it, how many accounts and servers. Restoring replaces the instance's contents entirely and signs everyone out.
Environment variables are recorded in the file and shown at restore time, but never applied: they belong to the deployment, not to the backup. Copy them into your compose file yourself.
/app/data holds frirss.db and its write-ahead log — the JWT secret and the
token-encryption key included. Backing up that volume backs up the instance.
To take a copy out of a running container, use the snapshot script rather than
cp:
docker exec frirss node scripts/backup-db.js /app/data/backups
It goes through SQLite's .backup() API: atomic, and safe while the server is
writing. Copying frirss.db on its own is not a valid backup — the most
recent writes live in the -wal file beside it.
Locked out of every admin account? docker exec -it frirss node scripts/reset-password.js sets a new password from the terminal.
FriRSS holds the credentials to your FreshRSS server, so a few things are not optional:
script-src 'self') along with X-Frame-Options, X-Content-Type-Options and Referrer-Policy.PUID/PGID, 1000 by default): code execution inside Node no longer owns the data directory, where the JWT secret and the token-encryption key live. The data directory is adopted on start, so upgrading needs no action.FRIRSS_PROXY_RATE_LIMIT), so an account cannot turn the backend into an open relay.Dependency and image scans run on every push and can be launched by hand; the current state is in GitHub Actions.
Found a security issue? Please follow SECURITY.md rather than opening a public issue.
FriRSS is a personal project, but feedback, ideas and contributions are welcome.
CHANGELOG.mdTypeScript
94.8%
CSS
4.0%