Get the new rssnetwork.js and package.json from the repo and restart. No new packages this time, so there's no npm install step.
Subscribe to https://news.rss.chat/worknotes/rss.xml and every improvement to rss.chat arrives as it ships -- the worknotes from the server and the client, together in one feed, newest first. Each item carries a category element saying which side it came from, so a reader that filters by category can follow just one. The feed announces updates over rssCloud, so subscribers who speak it hear within seconds. And in the spirit of a product built on RSS: the feed is generated by a script you can read, published right next to the feed itself -- worknotesFeed.belt. If you run an rss.chat server, this feed is the way to hear about updates like the security fix of 7/23 the moment they're posted. You're reading the first item that was born in the feed.
A bug DW reported earlier but couldn't reproduce finally showed itself: paste a chunk of text that includes a link, put the cursor somewhere later in the text, press Return -- and the break landed at the link instead of the cursor, tearing the link out onto its own line. The trick to reproducing it: it only ever happened on the first Return after that kind of paste, never again in the same post -- which is exactly what made it so slippery. Now the break lands where the cursor is, and links stay in their sentences. (Theme v0.5.339.)
This is the first of a kind of note you'll see here from now on: when a change matters to people running their own installs, the steps to update will live right here in worknotes. For this one:
Get the new rssnetwork.js and package.json from the repo.
In your server's folder, run npm install. The updated package.json adds one package -- the cleaner -- and this brings it in.
Restart the server.
That is the whole update. Nothing in your config or your database changes, and the cleaner applies to new and edited posts the moment you restart.
A post's text is written by its author and then shown to everyone who reads the timeline, which means anything hidden inside it runs in every reader's browser. Until today the server stored a post's text exactly as it arrived, so a post could carry markup that did more than format words. As of server v0.6.3, every post passes through a cleaner on its way into the database. Ordinary writing comes through untouched -- links, bold and italic, quotes, lists, and pasted images are all kept -- but anything that could run code is removed before the post is ever stored. Because the cleaning happens as the post is written, it protects every reader of every feed and timeline at once, and no old or unusual browser can slip past it. Both public servers, rss.chat and demo.rss.chat, are already running it. Thanks to the reader who reported this privately.
Copy a screenshot or an image file to the clipboard, click into the composer, paste: a small "Uploading image..." note appears at the cursor and a moment later the picture takes its place, sized to fit the column. Publish it and the image is part of the post, served from your server like everything else you write. It works like Slack and GitHub -- no Upload command, no dialog, nothing to learn. The limit is 2MB per image; paste something bigger and a dialog says so. The Publish button waits politely until every pasted image has finished uploading, so a post can never go out with a half-arrived picture. Rich-text mode only for now -- markdown mode will get the same feature, inserting the image reference as text, over the same plumbing. First image posts in the wild: demo 227 and 228 ("We have images.") and rss.chat 378. (Theme v0.5.338, with a new uploadMedia call in api.js by DW.)
A new endpoint, /uploadmedia, accepts an image from a signed-in user -- up to 2MB, sent base64-encoded in the request body with the content type as a parameter -- stores it in a new media table in the database, and answers with the address it will be served from: /media/1, /media/2, and so on, permanent ids just like posts. Request that address and the image comes back byte-for-byte with the right content type. No filenames, no image processing, no separate file storage -- the picture lives in the same database as everything else, which means the same one-file simplicity on SQLite servers, and the same backup story: the export and import verbs carry the media table along (the bytes travel as text inside the JSON), so a migrated server keeps its images with their addresses intact. The table is called media rather than images on purpose -- one binary table can someday hold audio and video too, and the type column already tells them apart. MySQL servers get the table definition in installMysql.md. The upload limit is a config setting, maxMediaUploadBytes, 2MB by default. Under the hood, davesql (v0.7.1) learned to write binary values on both engines -- the last piece the feature needed. (Server v0.6.1.)
demo.rss.chat migrated this morning -- 58 users, 210 posts, 153 likes, 138 served files, five minutes -- so both public servers now run on one-file databases. davesql 0.7.0, the package carrying the engine, is published on npm. And the repo caught up with the reality: the example config.json ships with "flUseSqlite": true, install.md is now the SQLite install -- no database server, no schema to paste, a Backups section, and the six-step migration for existing MySQL servers -- while the whole MySQL story (config, schema, upgrade notes) moved to its own page, installMysql.md. Dave announced it on Scripting News. One lesson from the demo migration made it into the doc: if the import stops on a UNIQUE constraint error, the server touched the new empty database before the import ran -- delete the file and re-run.
This morning's note said an rss.chat server can run on SQLite; by this evening the flagship server does. Both production servers were upgraded to the new software (v0.6.0) during the afternoon, running on MySQL exactly as before -- proof that the change disturbs nothing for existing installs. Then the migration itself: export the whole database to one JSON file, set "flUseSqlite": true in config.json, import, restart. All 752 rows -- 12 users, 361 posts, 217 likes, 162 served files -- came across with their ids intact, so every permalink still works, and the Software versions dialog now reads "SQLite version: v3.49.2." The MySQL database was never written to during the switch, so rolling back would have been one config line -- it wasn't needed. Total elapsed time, including a wrong turn when an unsaved config file sent the import to the wrong engine (it stopped harmlessly on the first duplicate row): about twenty minutes. demo.rss.chat follows tomorrow.
Sign-in is a magic link, so a new server can't sign anyone in until it can send mail -- and nothing documented how. New page: email.md -- the four SMTP settings for sending through a provider you already use, or Amazon SES with a link to Scott Hanson's setup walkthrough. It's a step in the install checklist now, along with a link to the questions thread -- if something in the install isn't working or isn't explained, that's where to say so.
Servers can now run on SQLite as well as MySQL (that's the day's big server-side story -- see the server worknotes), and the dialog's last line follows along: it says "SQLite version" or "MySQL version" to match the actual engine, with the real version number either way. The server sends the engine name in a new databaseEngine member of the user-data record, and the client reads it as of v0.6.10. First seen live on scratchpad.rss.chat, the first server running on SQLite.
If you left the editor open with a draft when you last used the app, the next startup could crash quietly: the app tried to restore your draft before one of its writing tools -- the piece that turns rich text into markdown -- was ready. Everything after the crash never ran, and the damage showed up as two seemingly unrelated bugs: your name missing from the menu bar, and the Publish button refusing to enable while you typed in the body of a post (typing in the title, which takes a different path, still worked -- which is why the bug seemed to be about titles). It surfaced during the demo.rss.chat database migration this morning, was reproducible one minute and gone the next, and the last piece of the puzzle was browser caching: the fix was live but browsers kept running the old code until the cache-buster on the script address changed. Fixed by DW: the converter now starts first thing, and the script address was bumped so every browser picks up the cure. (Client v0.6.11.)
node rssnetwork.js export backup.json writes the whole database -- users, posts, likes, feeds -- into one JSON file and exits without starting the web server; node rssnetwork.js import backup.json loads that file into an empty server. Both work on either engine, which makes the pair the door between them: export from a MySQL server, import into a SQLite one, and the server's whole history comes along -- posts keep their ids, so every permalink survives. The Software versions dialog now names the engine too, so a glance tells you what a server is running on.
One new member in config.json chooses the engine: "database": {"flUseSqlite": true, "databaseFilePath": "data/myserver.db"}. The database is a single file that creates itself, tables and all, the first time the server runs -- the install becomes download, npm install, run. Everything else is identical: same features, same feeds, same API, and a server that says nothing stays on MySQL exactly as before. The engine itself went into davesql (v0.7.0), where all the SQL expertise lives, so the same switch is available to every app built on it. The first server running this way is live at scratchpad.rss.chat -- sign-up, posting, replies, likes, and feeds, all served out of one file on a fresh droplet that has never heard of MySQL.
Posts no longer carry blank lines at the end -- if your post trails off with empty paragraphs or stray Returns, the server trims them when it saves, so nothing renders as mysterious empty space below your last line (a new config setting, flRemoveBlanksAtEnd, is on by default; set it false to keep trailing blanks). The autolinker learned that install.md is a document's name, not a web address -- bare names ending in a file extension that happens to be a real internet domain ending (.md, .zip, .sh, .py) stay plain text, while real addresses, including bare domains like rss.chat, still become links. And the server's subscription list can now carry a title of your choosing: set titleForSublist in config.json; leave it out and the title is unchanged.
When you reply to a post, the editor used to lead with the post itself — the first few lines, taking up room, an eyesore you'd already read. Now it shows the author's avatar and name, and under the name one quiet line: a wedge and "46 words." Click the wedge and the whole post unfolds, click again and it tucks back away. It starts folded every time, because when you're writing a reply, that's really all you need — who you're answering and how much they said, one click from the full text. (Theme v0.5.331–0.5.337.)
Don Park asked for the version number in a tooltip, for telling a stale cached client from a current one. It went in a different door: the first command in the system menu, formerly About RSS.chat, is now Software versions — one dialog with the server address and the versions of everything: server, client, theme, and MySQL. The theme version number that used to sit in the menu bar during rapid UI development is gone — it had one job, confirming you were running the latest theme, and the dialog now covers all of it. Work by DW; his reply to Don is 353.
When a fresh copy of the app signs on, the older copy puts up a dialog and stops listening — that's the goodnight kiss, from two days ago. But if you dismissed that dialog by clicking outside it instead of clicking OK, the dialog went away and the app just sat there, disconnected. Now the page reloads no matter how the dialog is dismissed — OK, a click outside, or the Escape key all land you in the same place: a fresh copy of the app. (Client v0.6.8.)
The home page's title was hard-coded to "rss.chat" — every instance, whatever its name, said rss.chat in the tab. Now the title comes from the server's productNameForDisplay setting, so demo.rss.chat says demo.rss.chat, and your instance says whatever you named it. Work by DW.
As of server v0.5.32, /feed takes a format parameter: format=xml (the default) returns the RSS document as always, and format=json returns the same feed translated into JSON. The structure is rss.channel.item, the names are RSS 2.0's own names, every element -- source:markdown, source:inReplyTo, source:comments, all of it -- exactly where the XML puts it. An unsupported format name gets an error naming the two real ones.
Ricardo reported on issue #14 that the walker printed ? for every author -- it was still reading the item-level source:account element that came out of the feeds yesterday. Now it reads what the feeds actually say: replies carry a core RSS <source> element naming the author, and in a user's own feed the channel says whose feed it is. Also fixed while in there: the walker's starting feed still pointed at the old users.rss.network address, which now answers with a redirect that Node's bare https.get won't follow -- it points at https://rss.chat/users/manton/rss.xml directly. Verified against the live thread: the whole conversation prints, every author named. A few doc examples still showing old feed addresses were updated to match.
Every rss.chat server broadcasts every new post, edit, and like over a websocket the moment it happens -- the same stream the shipped client uses to keep timelines current. As of today there's a doc that tells you how to drink from it -- firehose.md: the address, the wire format, the two verbs, and the three things a well-behaved listener does -- and two demo apps in examples/firehose that prove it: a Node command-line app that logs each post as it arrives, and a browser page that shows the JSON flowing through. Each is about a page of code, adapted from the feedlandSocket demos -- it's the same protocol FeedLand uses, so a listener written for one can listen to the other. No account, no key: connect and the posts come to you.
Our feeds had been putting a source:account element inside every item. The source namespace defines it as a channel-level element — it says whose feed this is, not who wrote an item — and at the item level it was redundant anyway: in the mixed feeds, every item already carries a <source> element with the author's feed URL and name. As of today, each user's own feed says it once, in the channel: <source:account service="rss.chat">dave</source:account>. The items carry it no more. The everyone feed and the comments feeds have no channel account — they have many authors, and their items keep <source> for per-item attribution. If you've been reading the item-level element, read the channel-level one (single-author feeds) or <source> (mixed feeds) instead. Already-published feeds keep the old shape until they next rebuild. The report that got this fixed is issue #9. The channel-level support went into daverss, the shared RSS-building package, as a new optional setting.
If you're signed in and open the app in a second tab, or another browser, or another machine, the older copy puts up a friendly dialog: the app is running in another tab, click OK to reload this one, or close it, no work lost. This fixes a bug that bit quietly and repeatedly: a change you made in one place — say, the URL of your icon in settings — could be silently wiped when an older copy, still running with the old settings, saved over it. Now a stale copy stands down the moment a fresh one signs on, so your latest settings always win. When the app connects, it identifies you to the server, and the server sends a "goodnight" message to every other connection you have open — the name comes from that message. (Client v0.6.7.)
Every server's home page carries a feed auto-discovery link — the standard <link rel="alternate"> in the page head, pointing at that server's everyone feed. Point a feed reader at the home page of rss.chat, or demo.rss.chat, or your own install, and it finds the feed by itself, no hunting for the URL. The address is filled in per-instance by a new home-page macro, [%feedUrlEveryone%], so the same template serves every server correctly. Work by DW.
Clicking a name anywhere in the timeline used to open that person's website in a new tab — surprising when what you wanted was to see what they've been posting. Now the name does what the avatar does: it shows their timeline, right in the app. Their website isn't lost — it's linked from their profile page. This came out of real use: Dave clicked a name expecting the person's posts and landed on their homepage instead. (Theme v0.5.330.)
Scott Hanson tested the new database-mode instructions by having an AI agent do a fresh install on a throwaway server — it worked, and the one thing it had to hunt for became issue #8: the doc never said what port to point a reverse proxy at. Fixed the same hour: step 8 of install.md now says the server listens on port 1420 (configurable via port in config.json or the PORT environment variable) and websockets have their own port, 1422 (websocketPort). Separately, for anyone upgrading an existing S3 server to database mode, the upgrade section gained the step we ourselves missed: a one-line SQL update that rewrites the old feed addresses stored with each post — without it, every rebuilt feed comes up empty. If you run a server from before today, that section is worth a read.
The hover tips on the left-panel icons — Home, New post, Feed, Data — used to appear off to the right, floating over the timeline, pointing at nothing in particular. They now appear directly below the icon and its word. (Theme v0.5.329.)
Each post's header now ends with a small feed symbol, right after the time: name · time · feed. Click it and the author's feed opens — a real RSS 2.0 feed you can hand to any feed reader, no account needed, nothing to wait for. Hover it and the tooltip says whose feed it is, using the server's own display name — on rss.chat, "Dave Winer's rss.chat feed." The icon is deliberately quiet: the same gray as the time next to it, not the traditional orange, because it belongs on every post without shouting from every post. Now each post is visibly connected to the author's feed — which is the whole idea of the product, worn on its sleeve. Dave wrote it up on Scripting News: A feed icon on every post. (Theme v0.5.323–0.5.328.)
A new sub-menu links the docs for RSS and OPML — the two formats the product is built on, now a click away from the timeline. Later the same day: a Blog posts sub-menu, collecting the story so far — from "Can RSS be a social network?" through today's feed icon.
A new config setting, flFeedsInDatabase, turns it on. When it's true, the server stores its RSS feeds and its subscription list in a new files table and serves them from its own domain — your feed is at https://yourserver/users/yourname/rss.xml, the subscription list at https://yourserver/data/subs.opml, and the four feed-location settings from Monday's note aren't needed at all. When it's false (the built-in default), nothing changes — S3 publishing works exactly as before.
Turning it on for an existing server is one restart: at startup the server backfills — rebuilds every user's feed, every comments feed, and the everyone feed from the database, so the files are all there before the first request. Both of our servers, rss.chat and demo.rss.chat, made the switch today.
One thing the flag can't do for you: subscribers still point at your old S3 addresses. The fix is a redirect where the old feed domain is served. Ours is one rule in the Caddyfile — every request to the old domain answers with a permanent redirect to the same path under rss.chat — and well-behaved feed readers update their stored addresses when they see a 301. Details in install.md, which now describes the database-mode install; S3 remains documented in config.md for those who want it.
Until today the text of a post couldn't be selected — to copy a phrase from your own post you had to open the editor, and for someone else's post there was no way at all. Now post text selects like any text on any page: drag across it, copy it, quote it. The one wrinkle worth solving: clicking a long post is how you expand and collapse it, and a drag-select ends with a click — so the click that finishes a selection is ignored, and plain clicks still open and close the post the way they always have. (Theme v0.5.322.)
Yesterday's note told how a new server that didn't set its own S3 locations inherited defaults pointing at rss.chat's folders. As of this version those defaults are gone: rssS3Path, rssFeedUrl, opmlS3Path, and opmlListUrl start as undefined, and your config.json supplies the real values — see Feeds on S3 in config.md. rss.chat's own config now sets its four values explicitly, the same as every other install. (rssFilename keeps its default, rss.xml — that one is right for every server.)
A detail that made this easy to verify: the server rebuilds its subscription list, subs.opml, on startup. Restart with the new settings and the file appears at its new address right away — no waiting for a post to trigger a rebuild.
Dave installed it on a different machine, following install.md for real, start to finish. It's open — no whitelist, anyone can join. Two lessons from the exercise, both now in the docs:
First, every server needs its own S3 locations for feeds. The install initially inherited the defaults, which point at rss.chat's own folders — so the new server was overwriting the original's feeds. config.md now has a Feeds on S3 section covering the four settings, and they're in the example config.json so they're on every installer's list.
Second, websockets need a route in your reverse proxy. Live updates run over a websocket on their own port (websocketPort), and the proxy in front of the server has to send upgrade requests there — with Caddy, that's a matcher on your domain plus the Connection/Upgrade headers, proxying to that port. And if more than one of these apps runs behind the same proxy, each needs its own port. Until the route existed, the site worked fine and only live updates were missing — if new posts don't appear without a reload, this is the first place to look.
Also new today: server v0.5.26 adds blockedUsersList to config.json — an array of email addresses that can't sign up, sign in, or post. It's checked fresh from the file on every use, so adding an address takes effect immediately, no restart. Case doesn't matter.
The server now turns bare URLs into links when a post saves (the story is in the server worknotes), and the client keeps up: when you update a post, the copy in the timeline repaints from what the server actually saved, so the new links are clickable immediately — no reload needed. (Theme v0.5.321.)
Type or paste a web address into a post and it's clickable when it publishes — no more selecting the text and reaching for the link button. The idea came from a user, Don Park, the day before it shipped.
The work happens on the server, in newPost and updatePost, so every client gets it for free. The post's text passes through the Autolinker package on its way to the database — it understands HTML, so it only touches plain text: a URL that's already part of a link, or sitting inside an image tag, is left alone, and the link text is exactly what the writer typed. The stored markdown is untouched — the source stays as written; only the rendered text gets the links.
api.md, new in server/docs, documents the HTTP interface between the rss.chat client and its server — every read and write call, the no-passwords email flow, the item record field by field, and the websocket verbs. The client that ships with the product is just one user of this API; the doc is for whoever wants to build the next one.
Also in server/docs: install.md's "An AI can do this install" section moved to the end of the doc — it's a good trick, but not the first thing a new host-runner needs to read.
There was a sequence that could bring down a feed rebuild: someone replies to a post, the author deletes the post, then the reply gets edited. Rebuilding the comments feed found the parent gone — deleted posts are filtered out of every query — and crashed trying to read it. Now the build answers the way this server always answers: "Can't build the comments feed for post N because the post has been deleted."
Also: this file moved from server/docs to server/code. Worknotes live with the code, because you need the worknotes to read the code.
Surface — the icon that pulled you out of a deep conversation back to your place in the timeline — lived less than two days, but it proved the idea. Now that lifeline is Home's first stage: when you're buried in a story, a profile, a flipped view, or an open conversation, clicking Home brings you back to the timeline with the cursor still on the post you were reading. Click it again and you're at the top, everything reset, cursor on the newest post. It's the same rhythm as the Home tab in the Twitter and Instagram apps — tap once to come back, tap again for the top — with one icon fewer to remember. (Theme v0.5.318–0.5.320, including a fix for a sneaky side effect: the icon panel used to shift a hair when the page scrolled, which could put a different icon under your mouse between the first click and the second.)
rss.chat's confirmation emails had been landing in spam. The app now sends from hello@rss.chat, and the domain is verified with Amazon SES, so Gmail and the rest can see the mail is really from us. The full story is in the server worknotes.
The cause: mailSender was a gmail.com address, but the mail actually goes out through Amazon SES, and Gmail sends mail to spam when the sending server isn't authorized to send for the address's domain — as policy, since 2024. The fix, useful to anyone deploying this server: verify your domain as an identity in the SES console (it hands you three DKIM CNAME records to add to your DNS), then set mailSender in config.json to an address on that domain. It doesn't need a real mailbox behind it — rss.chat now sends as hello@rss.chat. Verification took minutes, and the first email after the change landed in the inbox.
The server moved into the rss.chat repo. One repo for the whole product now, organized by part: the server's code is at server/code, and these docs — install, config, and this file — are at server/docs. The software is unchanged; only the address is new.
github.com/scripting/rss.chat is organized the way the product is: a client folder and a server folder, each with its own code and docs, and a new top-level examples folder for complete apps built on the APIs. The first example is threadwalker, a small Node app that walks a whole conversation using nothing but the RSS feeds — no API calls, no account — and prints it as an indented outline.
These docs moved too. The basics doc is now the readme of client/docs, so it's the first thing you see when you browse the folder, and the firehose documentation is a section inside it — one document, because the whole site is for developers.
Dave opened a conversation, read it, and then couldn't see how to get back — the wedge that opens a thread goes light once it's open, and light means "nothing to see here." His instinct was the Home button, and it was disabled. That instinct is now the design: whenever a conversation is open in the timeline, Home lights up, and clicking it closes everything and returns you to the top — the same fresh timeline Home has always meant. (Theme v0.5.317.)
Don Park signed up — welcome! — and reported that the account-name help text said "Name must be 4 chars." when longer names clearly work. The check was always a minimum; the words now say so: "Name must be at least 4 chars." Dave applied the fix. The report also surfaced something the help text was covering for: nothing actually stops a too-short name from going through. That enforcement is on the todo list.
The icon labels added yesterday were the theme's guesses, mapped from icon names. Dave gave each icon a title of its own in the app's data, and the theme now displays exactly those — theme designers get the app's words, not their own inventions. Also new since this morning: the Home button knows when you're already home, and disables itself. (Theme v0.5.315.)
Opening a conversation with the wedge had a flaw: a reply might already be sitting in the timeline as its own post, so expanding its parent put the same words on screen twice. Now, when replies open under a post, any of them that were standing in the timeline fold into the conversation — every post appears exactly once. Close the wedge and they return to their places. If your cursor was on one of those posts, it follows it into the conversation. (Theme v0.5.316.)
Clicking a post's timestamp used to do something surprising when the post was a reply: instead of the post you asked for, you got its parent's page with your post nested inside it. Now every post's link shows that post as its own page, with its replies below — and "Replying to" at the top is the way up to the parent, one step at a time. This came directly out of real use: Dave wanted to link to a particular reply from a blog post, clicked its timestamp, and landed somewhere confusing. With the wedge to go down and Surface to come back up, the post itself is the right place to land. (Theme v0.5.313–314.)
A new icon in the left panel, and the missing half of yesterday's wedge. The wedge takes you down into a conversation; Surface brings you back up. Wherever you've gotten to — a story page, a profile, the flipped view — one click returns you to the timeline with the cursor on the post you were just reading, scrolled into view. It's not Home: Home takes you to the top and starts fresh, Surface puts you back exactly where you left off. Dave's reaction on first click: it's a lifeline, for when you're buried in something and just want out. Announced on rss.chat in post 216 — written by Claude, reviewed word for word by Dave before it posted, which is the standing rule for anything published from the claude account. (Theme v0.5.312.)
The hover tips on the left-panel icons were appearing far off to the right, floating in empty space. The panel was much wider than its labels needed, and the tips hang off the panel's far edge. The panel now fits its contents: the tips appear right beside the icons, and the whole icon column sits closer to the timeline.