Summary
vutuv already publishes two public RSS 2.0 feeds. This feature adds personalized feeds so a member can follow their own vutuv activity from any feed reader (NetNewsWire, Thunderbird, Feedly, …) without opening the app or relying on email:
- Timeline (private) — posts from the people you follow plus your connections
- Notifications (private) — new followers, connection requests/accepts, endorsements (Empfehlungen), replies to your posts
- Saved posts (private) — your bookmarks
- Tag feeds (public) — a per-tag feed of public posts, no auth
The hard part is the three private feeds: RSS readers don't log in and don't carry a session cookie, so a private feed needs a bearer credential embedded in the feed URL. That credential can leak, which drives most of the security design below.
What already exists (for context)
Two public feeds, rendered by VutuvWeb.Feeds and served by VutuvWeb.FeedController:
- Per-member:
/<slug>/posts/feed.xml — a member's original public posts (Vutuv.Posts.recent_public_posts/2)
- Site firehose:
/posts/feed.xml — latest public posts across the site, aggregating only members who opted out of nothing
Both render the anonymous public view and carry the member's noindex/noai content signals (VutuvWeb.ContentPolicy). They hold nothing private, so they need no authentication. This issue does not change them; it adds new feeds alongside.
New feeds
Private feeds (timeline, notifications, saved)
| Feed |
URL (example) |
Source |
| Timeline |
/feed/<token>/timeline.xml |
Vutuv.Posts.feed_page/2 |
| Notifications |
/feed/<token>/notifications.xml |
Vutuv.Activity.notifications_page/2 |
| Saved |
/feed/<token>/saved.xml |
the member's bookmarks (Vutuv.Posts.bookmark_post/2 & friends) |
- Timeline and saved render posts; reuse the existing item renderer in
VutuvWeb.Feeds.
- Notifications is a different item shape (an activity event, not a post). Add a notifications-item renderer in
VutuvWeb.Feeds. Each item links to the relevant in-app page and needs a stable guid. System text names accounts by their @handle, consistent with the rest of the app, never the clear name.
Public tag feeds
- New route
/tags/<tag>/feed.xml, public, modeled on the per-member feed.
- Aggregate only opted-in members and carry the permissive content signals, mirroring how the site firehose already handles mixed opt-outs.
- Because it is public, it must join the agent-format sibling system per
CLAUDE.md: the tag page already has .md/.txt/.json siblings, and the new feed.xml must be listed in /llms.txt (VutuvWeb.PageController.llms) and offered via a <link rel="alternate" type="application/rss+xml"> autodiscovery tag on the tag page.
Authentication: capability URLs via named feed tokens
Private feeds authenticate with a capability URL — a long, unguessable token baked into the feed URL itself. The token is the credential; anyone holding the URL can read that feed.
Tokens are managed like the existing Personal Access Tokens (/api/v1), so we reuse that infrastructure and UX rather than inventing a new one:
- Backing model:
Vutuv.ApiAuth / Vutuv.ApiAuth.Token / Vutuv.ApiAuth.Scopes; management controller VutuvWeb.AccessTokenController. A feed token is an access token with a feed-read scope.
- In settings the member creates a named token ("Phone reader", "Thunderbird"). Each token mints the feed URLs above.
- The full feed URL is shown once, at creation, via the existing show-once reveal (
<.secret_once>); we store only a hash of the token and compare in constant time, exactly as PATs do. If the member loses the URL they create a new token.
- Revocation: deleting the named token makes its URLs
404 immediately.
Per-token content level (what a leak exposes)
Each token carries a content-level flag:
- Public-only (default) — restricted / connections-only posts never enter the feed, so a leaked URL exposes nothing the public couldn't already see.
- Include restricted posts I can see — full quality-of-life timeline including connections-only posts, accepting that a leaked URL then exposes other people's restricted posts.
Default is public-only; including restricted content is a deliberate opt-in per token. This lets a member put a public-only token in a cloud-based reader and a full one in a local reader.
Security requirements
A URL-borne secret leaks easily (referrer headers, server access logs, shared OPML files, a reader's own cloud cache). Every private feed response must therefore:
- be served over HTTPS only;
- send
Cache-Control: private, no-store (never the public feeds' public, max-age=300), so shared proxies and reader clouds don't cache it;
- send
Referrer-Policy: no-referrer and X-Robots-Tag: noindex, nofollow;
- use a token with ≥160 bits of entropy, URL-safe, stored hashed and never logged in plaintext (note: the token sits in the URL path, so the nginx access-log format must not record it, or must redact it);
- contain only normal permalinks in its items — the token must never propagate into rendered HTML or onward referrers;
- be individually revocable, and rate-limited (readers poll frequently).
Private feeds are never autodiscoverable: no public page links to a capability URL. They are surfaced only on the logged-in settings page where the member copies the URL.
Out of scope
- Direct messages over RSS. Too sensitive for third-party-cached feeds, and a poll-based format doesn't suit real-time messaging. Explicitly excluded.
- No change to the two existing public feeds.
Why (quality of life)
Today a member learns about new followers, connection requests, replies, and their network's posts only inside the app or by email. RSS lets them pull all of that into the reader they already use, on their own schedule, with no extra notifications email. It also rounds out vutuv's "open and inspectable" posture: the public feeds already serve agents and humans; private feeds extend the same idea to the logged-in member.
Open questions / decisions for the implementer
- Token storage: extend the existing access-token table with a feed scope + content-level column, or a dedicated
feed_tokens table? (Migrations must stay N-1 backward compatible per CLAUDE.md.)
- One token can presumably serve all three private feeds; confirm whether the content-level flag should be per-token (assumed) or per-feed.
- Pagination: feeds cap at ~20 items like the existing ones; confirm that's enough for the notifications feed.
- Smoke-test in a real reader before deploy (the Swoosh/CSRF test-harness caveats in
CLAUDE.md don't apply here, but feed-reader behavior with capability URLs should be verified end to end).
Summary
vutuv already publishes two public RSS 2.0 feeds. This feature adds personalized feeds so a member can follow their own vutuv activity from any feed reader (NetNewsWire, Thunderbird, Feedly, …) without opening the app or relying on email:
The hard part is the three private feeds: RSS readers don't log in and don't carry a session cookie, so a private feed needs a bearer credential embedded in the feed URL. That credential can leak, which drives most of the security design below.
What already exists (for context)
Two public feeds, rendered by
VutuvWeb.Feedsand served byVutuvWeb.FeedController:/<slug>/posts/feed.xml— a member's original public posts (Vutuv.Posts.recent_public_posts/2)/posts/feed.xml— latest public posts across the site, aggregating only members who opted out of nothingBoth render the anonymous public view and carry the member's
noindex/noaicontent signals (VutuvWeb.ContentPolicy). They hold nothing private, so they need no authentication. This issue does not change them; it adds new feeds alongside.New feeds
Private feeds (timeline, notifications, saved)
/feed/<token>/timeline.xmlVutuv.Posts.feed_page/2/feed/<token>/notifications.xmlVutuv.Activity.notifications_page/2/feed/<token>/saved.xmlVutuv.Posts.bookmark_post/2& friends)VutuvWeb.Feeds.VutuvWeb.Feeds. Each item links to the relevant in-app page and needs a stableguid. System text names accounts by their@handle, consistent with the rest of the app, never the clear name.Public tag feeds
/tags/<tag>/feed.xml, public, modeled on the per-member feed.CLAUDE.md: the tag page already has.md/.txt/.jsonsiblings, and the newfeed.xmlmust be listed in/llms.txt(VutuvWeb.PageController.llms) and offered via a<link rel="alternate" type="application/rss+xml">autodiscovery tag on the tag page.Authentication: capability URLs via named feed tokens
Private feeds authenticate with a capability URL — a long, unguessable token baked into the feed URL itself. The token is the credential; anyone holding the URL can read that feed.
Tokens are managed like the existing Personal Access Tokens (
/api/v1), so we reuse that infrastructure and UX rather than inventing a new one:Vutuv.ApiAuth/Vutuv.ApiAuth.Token/Vutuv.ApiAuth.Scopes; management controllerVutuvWeb.AccessTokenController. A feed token is an access token with a feed-read scope.<.secret_once>); we store only a hash of the token and compare in constant time, exactly as PATs do. If the member loses the URL they create a new token.404immediately.Per-token content level (what a leak exposes)
Each token carries a content-level flag:
Default is public-only; including restricted content is a deliberate opt-in per token. This lets a member put a public-only token in a cloud-based reader and a full one in a local reader.
Security requirements
A URL-borne secret leaks easily (referrer headers, server access logs, shared OPML files, a reader's own cloud cache). Every private feed response must therefore:
Cache-Control: private, no-store(never the public feeds'public, max-age=300), so shared proxies and reader clouds don't cache it;Referrer-Policy: no-referrerandX-Robots-Tag: noindex, nofollow;Private feeds are never autodiscoverable: no public page links to a capability URL. They are surfaced only on the logged-in settings page where the member copies the URL.
Out of scope
Why (quality of life)
Today a member learns about new followers, connection requests, replies, and their network's posts only inside the app or by email. RSS lets them pull all of that into the reader they already use, on their own schedule, with no extra notifications email. It also rounds out vutuv's "open and inspectable" posture: the public feeds already serve agents and humans; private feeds extend the same idea to the logged-in member.
Open questions / decisions for the implementer
feed_tokenstable? (Migrations must stay N-1 backward compatible perCLAUDE.md.)CLAUDE.mddon't apply here, but feed-reader behavior with capability URLs should be verified end to end).