
Short answer: To publish an RSS feed from Firebase, write a Cloud Function that queries the newest published documents in Firestore, builds RSS 2.0 XML with escaped text, absolute links and RFC 822 dates, and returns it with the content type application/rss+xml and a public cache header. Add a Hosting rewrite so the function answers at a clean address such as /feed.xml, create the composite index that Firestore asks for, test the result with curl and a validator, and then give the address to an auto-poster so every new item reaches your social channels.
Firebase is a popular back end for small products, community apps, directories and news pages, because it lets one developer ship quickly. The same speed has a side effect: nobody builds the feed. Firestore stores documents, Hosting serves files, and neither has any idea what RSS is. Yet readers, aggregators and auto-posters all expect a feed address. This guide builds a Firebase Cloud Functions RSS feed with the second generation runtime, explains each decision, and ends with the checks that make it reliable in production.
A function-based feed fits when your content already lives in Firestore:
If your site is built with a static generator or a CMS, use its own feed instead; see how to find your Hugo, Jekyll or Eleventy feed. If your content sits in a headless service, the options are compared in auto-posting from Notion, Airtable or a headless CMS.
The example uses a collection named posts. Each document has a title, a slug, a summary, a status, a publication timestamp and an optional image address. The document identifier will be the GUID of the item, so it never changes even if the slug does.
posts/{postId}
title: "Version 2.4 is out"
slug: "version-2-4"
summary: "What changed in the new release."
status: "published" // draft | scheduled | published
publishedAt: Timestamp // 2026-10-18 07:40:00 UTC
imageUrl: "https://www.example.com/images/v24.png"Use a Firestore timestamp for the date and not a string. Timestamps sort correctly, convert cleanly to JavaScript dates and let the query itself exclude items that are not public yet.
Create the project with the Firebase command line tools, choose Functions with JavaScript and install the dependencies. Then put the following in index.js:
const { onRequest } = require("firebase-functions/v2/https");
const { initializeApp } = require("firebase-admin/app");
const { getFirestore, Timestamp } = require("firebase-admin/firestore");
const crypto = require("crypto");
initializeApp();
const db = getFirestore();
const SITE_URL = "https://www.example.com";
const esc = (value) => String(value ?? "")
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """);
exports.feed = onRequest({ region: "europe-west1", memory: "256MiB" }, async (req, res) => {
const snap = await db.collection("posts")
.where("status", "==", "published")
.where("publishedAt", "<=", Timestamp.now())
.orderBy("publishedAt", "desc")
.limit(20)
.get();
const items = snap.docs.map((doc) => {
const p = doc.data();
return `
<item>
<title>${esc(p.title)}</title>
<link>${SITE_URL}/news/${esc(p.slug)}/</link>
<guid isPermaLink="false">${esc(doc.id)}</guid>
<pubDate>${p.publishedAt.toDate().toUTCString()}</pubDate>
<description>${esc(p.summary)}</description>
</item>`;
}).join("");
const newest = snap.empty ? new Date(0) : snap.docs[0].get("publishedAt").toDate();
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Example Product News</title>
<link>${SITE_URL}/news/</link>
<description>Release notes and announcements</description>
<language>en</language>
<lastBuildDate>${newest.toUTCString()}</lastBuildDate>
<atom:link href="${SITE_URL}/feed.xml" rel="self" type="application/rss+xml"/>${items}
</channel>
</rss>`;
const etag = '"' + crypto.createHash("sha1").update(xml).digest("hex") + '"';
res.set("Content-Type", "application/rss+xml; charset=utf-8");
res.set("Cache-Control", "public, max-age=300, s-maxage=600");
res.set("ETag", etag);
if (req.get("If-None-Match") === etag) {
res.status(304).end();
return;
}
res.status(200).send(xml);
});Three details matter. The query filters on both the status and the date, so drafts and scheduled documents never appear. The escape helper replaces the ampersand first, otherwise the entities it creates would be escaped again. The date uses toUTCString(), which gives an English RFC 822 style value such as Sun, 18 Oct 2026 07:40:00 GMT whatever the server language. The GUID is marked with isPermaLink="false" because it is an identifier and not an address.
The first time the function runs, Firestore will probably refuse the query and print an error with a link. A query that combines an equality filter on one field with a range filter and ordering on another needs a composite index. Follow the link, or define the index in firestore.indexes.json with the status field ascending and the publishedAt field descending, and deploy it. Indexes take a few minutes to build. Until then the function returns an error, so deploy the index before you connect an auto-poster, not after.
A deployed function has a long default address that includes the project and the region. Do not publish that. Add a rewrite to firebase.json so your own domain answers:
{
"hosting": {
"public": "public",
"rewrites": [
{
"source": "/feed.xml",
"function": { "functionId": "feed", "region": "europe-west1" }
}
]
}
}Deploy with firebase deploy --only functions,hosting. The address https://www.example.com/feed.xml now reaches the function. A stable address matters because auto-posters remember it; changing it later means reconnecting every channel. Hosting also gives you a content delivery network in front of the function, which is why the response carries an s-maxage value: it tells the network how long it may keep the same copy.
| Layer | Setting | Effect |
|---|---|---|
| Browser and readers | max-age of 300 seconds | Repeated requests reuse the same file for a short time |
| Hosting network | s-maxage of 600 seconds | Most requests never reach the function or the database |
| Function | One query, limit of twenty | Each real request reads a small, fixed number of documents |
| ETag | Hash of the XML | Auto-posters can receive a 304 answer instead of the whole file |
Firestore bills by the number of documents read, so caching is not only a speed measure, it is a cost control. A feed that is polled every minute by several services would otherwise read twenty documents many thousands of times a day. Keep the lifetime short enough that a new post appears within minutes. A long lifetime delays every share by the same amount; the effect is described in RSS feed caching explained. The ETag logic is explained in ETags and conditional GET.
Functions that scale to zero can take a moment to start after a quiet period. If the feed matters to your business, set a minimum number of instances for the function, or rely on the cache in front of it so most requests never wake it. A slow first response can look like a timeout to an auto-poster; see RSS feed timeouts. Cloud Functions also require a project with billing enabled, so check the current terms for your account before you plan around it.
Auto-posters build the preview card from the image of the item, so it is worth including one. Store the full public address of the picture in the document, for example a file in Cloud Storage with a public download address, and add it to each item as an enclosure element with the address, the file size and the real type. If you do not know the size, many readers accept a value of zero, but the type should be accurate. Keep images reasonably small and in a common format, and never point to an address that needs a login. The behaviour of these tags is explained in RSS enclosure tags explained.
The function uses the Admin SDK, which bypasses Firestore security rules. That is convenient, but it means the code alone decides what becomes public. Build the item from named fields as shown, rather than spreading the whole document into the feed. Never include internal notes, author email addresses, moderation flags or draft text. Keep your security rules strict for client apps, and let the function be the only public window onto the content.
curl -i and check for the 200 status, the XML content type, the cache headers and the ETag. The method is explained in debugging an RSS feed with curl.If-None-Match header and confirm that the answer is 304.If something looks wrong later, the checklist in RSS feed not updating helps you find the cause quickly. Broken relative links are the other classic problem; see relative URLs in RSS feeds.
PostRSS has been publishing RSS and Atom feeds to social networks since 2014. Paste the address of your feed.xml rewrite, connect your accounts and choose how each post is built from the title, the description or the page title. PostRSS checks the feed as often as every minute and shares each new item, with its image and link, to the channels you choose among 66 supported networks, messengers, team chats and blogs, including LinkedIn, X, Bluesky, Mastodon and Telegram. Keyword filters, UTM parameters and posting windows let you tune each channel, and webhooks connect PostRSS to your own systems. See the PostRSS features page and the plan limits on the pricing page. More developer guides are under the Technical Guide tag.
Firebase does not publish feeds, but one Cloud Function and one rewrite are enough. Query only published documents dated in the past, newest first, with a limit of twenty. Create the composite index, escape every value, use the document identifier as the GUID, format dates in English UTC, build links from a fixed site address and send cache headers with an ETag. Test with curl and a validator, keep the address unchanged and connect it to an auto-poster. After that, every item you publish in your app can reach your social channels automatically.
No. Hosting serves files and rewrites requests. You need a function or a build step that creates the XML, and a rewrite that exposes it at a clean address.
The query combines an equality filter with a range filter and an order on another field, which requires a composite index. Open the link in the error message or define the index in your project, deploy it and wait until it is ready.
Yes, if your content changes rarely. A script can write a static feed.xml into the public folder during deployment. A function is better when content is added through the app all day, because the feed then updates without a new deployment.
Between ten and twenty of the newest published items is enough. An auto-poster only needs recent items, and a smaller file is faster to download and cheaper to produce.
Give every item a stable GUID, such as the document identifier, and avoid editing the publication date of items that are already public. Some tools treat a changed date as a new item.
Yes. PostRSS reads any valid RSS or Atom address that is reachable without a login. Paste the address, connect your channels and new items are shared as soon as they appear.
What changed in the networks, what broke, and how to fix it before it costs you reach.
Az Internet Solutions, a PostRSS mögött álló csapat fejlesztései. Mindegyik a maga módján spórol időt.