rss.chat for developers

rss.chat looks like a chat app, but everything in it is RSS. Every user is a feed, every message is an item, every conversation is a set of feeds pointing at each other — static XML files, republished the moment anything changes. Reading is open: no account, no key, no rate-limit dance. If you love RSS and want a cool weekend project, this page has everything you need.

Example code lives in sub-folders of this folder.

A feed, in full

This is the complete, unabridged feed of a real user — users.rss.network/manton/rss.xml, as it looked shortly after he signed up and said hello:

<?xml version="1.0" encoding="UTF-8"?>
<!-- RSS generated by rssnet v0.5.21 on Wed, 08 Jul 2026 14:48:18 GMT -->
<rss version="2.0" xmlns:source="https://source.scripting.com/">
    <channel>
        <title>Manton Reece</title>
        <link>https://manton.org/</link>
        <description></description>
        <pubDate>Wed, 08 Jul 2026 14:46:01 GMT</pubDate>
        <language>en-us</language>
        <generator>rssnet v0.5.21</generator>
        <docs>http://cyber.law.harvard.edu/rss/rss.html</docs>
        <lastBuildDate>Wed, 08 Jul 2026 14:48:18 GMT</lastBuildDate>
        <cloud domain="rpc.rsscloud.io" port="5337" path="/pleaseNotify" registerProcedure="" protocol="http-post" />
        <image>
            <title>Manton Reece</title>
            <url>https://micro.blog/manton/avatar.jpg</url>
            <link>https://manton.org/</link>
            <description></description>
            </image>
        <source:localTime>Wed, July 8, 2026 10:48 AM EDT</source:localTime>
        <source:self>https://users.rss.network/manton/rss.xml</source:self>
        <item>
            <description>Cool, I'm checking out the feed for my posts. I see the rssCloud element too.</description>
            <pubDate>Wed, 08 Jul 2026 14:46:01 GMT</pubDate>
            <guid>https://rss.chat/?id=206</guid>
            <source:markdown>Cool, I'm checking out the feed for my posts. I see the rssCloud element too.</source:markdown>
            <source:account service="rss.chat">manton</source:account>
            <source:inReplyTo>https://rss.chat/?id=205</source:inReplyTo>
            <source:comments count="1" feedUrl="https://users.rss.network/manton/comments/206.xml"/>
            </item>
        <item>
            <description>Hello! 👋</description>
            <pubDate>Wed, 08 Jul 2026 14:38:17 GMT</pubDate>
            <guid>https://rss.chat/?id=204</guid>
            <source:markdown>Hello! 👋</source:markdown>
            <source:account service="rss.chat">manton</source:account>
            <source:comments count="1" feedUrl="https://users.rss.network/manton/comments/204.xml"/>
            </item>
        </channel>
    </rss>

It's plain RSS 2.0, plus a handful of elements from the source namespace, documented at source.scripting.com. Here's what to look at, top to bottom:

  1. The cloud element is rssCloud — ask that server to notify you when this feed updates and you'll hear about new posts in seconds instead of on your next poll. Protocol docs at rsscloud.co. Polling is fine too; these are static files served from storage, so poll as often as you like.
  2. source:self is the feed's own address, so a copy that's been passed around can always say where it canonically lives.
  3. The description of an item is the post, as HTML. Titles are optional, as they are in RSS — a chat message usually doesn't have one. No length limits.
  4. The guid is the post's permalink — a page a person can open: rss.chat/?id=204.
  5. source:markdown is the same post as markdown, when the author wrote it that way. Rendering from markdown instead of scraping HTML is usually the nicer path.
  6. source:account says who wrote it and on what service.
  7. source:inReplyTo appears on replies — the permalink of the post being replied to. Follow it and you're one level up the conversation.
  8. source:comments is the other direction: this post has replies, here's how many, and here's a feed containing them — users.rss.network/manton/comments/204.xml. Items in a comments feed are ordinary items, exactly like the ones above, and when a reply has replies of its own, it carries its own source:comments. Conversations here aren't post-plus-comments, they're trees — any reply can start its own thread, and the whole tree is walkable from the feeds alone: source:comments to go down, source:inReplyTo to go up.
  9. One more thing appears in feeds that mix authors (the comments feeds, and the everyone feed below): RSS core's source element, naming the author and their home feed — <source url="https://users.rss.network/dave/rss.xml">Dave Winer</source>.

The other two documents

Two more addresses and the picture is complete:

  1. The everyone feed, all posts from all users: users.rss.network/rss.xml.
  2. The subscription list, every user feed on the site, in OPML: data.rss.network/subs.opml. Rebuilt when someone signs up. Subscribe your reader to it, or use it as the discovery point for anything you build.

An example app — walk a whole conversation from the feeds

The walker starts at one post and prints the entire conversation under it, indented like an outline, using nothing but feed reads. It's in the threadwalker sub-folder; the whole thing:

//threadwalker -- walks a whole rss.chat conversation from the RSS feeds alone, and prints it as an indented outline.
//An example app from the rss.chat developer docs: https://github.com/scripting/rss.chat/tree/master/devs

const https = require ("https");
const xml2js = require ("xml2js");

const urlStartingFeed = "https://users.rss.network/manton/rss.xml";
const guidStartingPost = "https://rss.chat/?id=204";

function httpGet (url, callback) {
    https.get (url, function (response) {
        var body = "";
        response.on ("data", function (chunk) {
            body += chunk;
            });
        response.on ("end", function () {
            callback (undefined, body);
            });
        }).on ("error", function (err) {
            callback (err);
            });
    }
function readFeedItems (urlFeed, callback) {
    httpGet (urlFeed, function (err, xmltext) {
        if (err) {
            callback (err);
            }
        else {
            xml2js.parseString (xmltext, {explicitArray: false}, function (err, jstruct) {
                if (err) {
                    callback (err);
                    }
                else {
                    var items = jstruct.rss.channel.item;
                    if (items === undefined) {
                        items = [];
                        }
                    if (!Array.isArray (items)) {
                        items = [items];
                        }
                    callback (undefined, items);
                    }
                });
            }
        });
    }
function printItem (item, indentlevel) {
    var author = "?";
    if (item ["source:account"] !== undefined) {
        author = item ["source:account"]._;
        }
    var theText = item.description;
    if (item ["source:markdown"] !== undefined) {
        theText = item ["source:markdown"];
        }
    console.log ("\t".repeat (indentlevel) + author + ": " + theText.split ("\n") [0]);
    }
function walkComments (item, indentlevel, callback) {
    const comments = item ["source:comments"];
    if (comments === undefined) {
        callback ();
        }
    else {
        readFeedItems (comments.$.feedUrl, function (err, replies) {
            if (err) {
                callback ();
                }
            else {
                var ix = 0;
                function nextReply () {
                    if (ix >= replies.length) {
                        callback ();
                        }
                    else {
                        const reply = replies [ix++];
                        printItem (reply, indentlevel);
                        walkComments (reply, indentlevel + 1, nextReply);
                        }
                    }
                nextReply ();
                }
            });
        }
    }

readFeedItems (urlStartingFeed, function (err, items) {
    if (err) {
        console.log (err.message);
        }
    else {
        items.forEach (function (item) {
            if (item.guid === guidStartingPost) {
                printItem (item, 0);
                walkComments (item, 1, function () {
                    });
                }
            });
        }
    });

Run it and out comes the conversation — the same thread you can read in the app at rss.chat/?id=204:

manton: Hello! 👋
    dave: hey manton, great to see you here.
        manton: Cool, I'm checking out the feed for my posts. I see the rssCloud element too.
            dave: and then look at the items, you'll see some new elements
        shanson: All the world's a feed,

From here a weekend goes fast: point it at the everyone feed and build a reader; watch rssCloud and build a notifier; read subs.opml and build stats; render a conversation tree as HTML and you've built a public archive browser.

Comments, questions?

Post them in the issues section of this repo.


This page was written by Claude Code, working on the project with Dave Winer.