Authenticated ICS feeds for private calendar events

The public ICS export has recently been reintroduced via

GET /discourse-post-event/events.ics, which is a great improvement following the work in

Re-Add full ICS export.

At the moment, this endpoint appears to be limited to events visible to anonymous users. As a result, events in private categories or categories restricted from the default everyone group cannot be subscribed to in external calendar clients (e.g. Google Calendar, Outlook).

Would it be feasible to support authenticated access to this endpoint, similar to how Discourse handles private RSS/Atom feeds (for example, via a per-user token or read-only API key)?

This wouldn’t change any permission rules - it would simply allow calendar clients to access events the user is already authorised to see.

I’m raising this as a separate, scoped request following the reintroduction of the public ICS feed, as previously suggested.

לייק 1

After poring over the code I ended up coding up a very simple proxy that handles various hurdles needed to create a User API Key and pass it to the API; it also generates a link that the users need to paste in their calendar apps:

Hopefully all of this will one day make it to Discourse code and it will no longer be needed - in the meantime I am sharing this in the hope of making life easier for others.

I added that earlier this week in this PR. However the ergonomics of generating User API Keys aren’t great for non-technical users. To make this seamless, I’m following up with:

Which tries to make this as friendly as possible

לייק 1

I just merged this feature can you give it a try @Ethsim2 ?

לייק 1

Thanks - I can confirm the authenticated feed itself is now being generated properly on my side.

The remaining issue seems to be client compatibility: neither Google Calendar nor Outlook appears happy subscribing directly to an authenticated ICS feed in this form, so for now I’m planning to work around it by putting a host-level Nginx reverse proxy in front of my single-container Discourse install and serving a plain .ics file there instead.

Because I’m on the standard single-container setup, I think this means moving ports 80/443 off the container, making Discourse listen on an internal high port, then having host Nginx proxy the forum and also serve a static calendar feed path.

Roughly, the commands I’m expecting to use are:

# 1. Install host nginx + certbot
sudo apt update
sudo apt install -y nginx snapd
sudo snap install core
sudo snap refresh core
sudo snap install --classic certbot
sudo ln -sf /snap/bin/certbot /usr/bin/certbot

# 2. Stop the Discourse container so ports 80/443 can be freed
cd /var/discourse
sudo ./launcher stop app

# 3. Edit the container config so Discourse no longer binds directly to 80/443
sudo nano /var/discourse/containers/app.yml

Then in app.yml change the expose section from:

expose:
  - "80:80"
  - "443:443"

to something like:

expose:
  - "127.0.0.1:8080:80"

and, if present, remove the container SSL / Let’s Encrypt templates so TLS is terminated on the host reverse proxy instead.

Then rebuild:

cd /var/discourse
sudo ./launcher rebuild app

Then create a host-level Nginx site such as:

sudo nano /etc/nginx/sites-available/discourse

with something along the lines of:

server {
    listen 80;
    listen [::]:80;
    server_name forum.example.com;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location /private-calendar/ethan.ics {
        alias /var/www/private-calendar/ethan.ics;
        default_type text/calendar;
        add_header Content-Type "text/calendar; charset=utf-8";
    }
}

Enable it and reload:

sudo mkdir -p /var/www/private-calendar
sudo mkdir -p /var/www/certbot
sudo ln -s /etc/nginx/sites-available/discourse /etc/nginx/sites-enabled/discourse
sudo nginx -t
sudo systemctl reload nginx

Then obtain a Let’s Encrypt certificate with Certbot and let it update the Nginx config:

sudo certbot --nginx -d forum.example.com
sudo nginx -t
sudo systemctl reload nginx

After that, the Nginx config will typically include an HTTPS server block as well, e.g.:

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name forum.example.com;

    ssl_certificate /etc/letsencrypt/live/forum.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/forum.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location /private-calendar/ethan.ics {
        alias /var/www/private-calendar/ethan.ics;
        default_type text/calendar;
        add_header Content-Type "text/calendar; charset=utf-8";
    }
}

At that point I can use a script/timer on the host to fetch the authenticated Discourse ICS and write out:

/var/www/private-calendar/ethan.ics

which Google Calendar / Outlook can subscribe to as a normal public ICS URL.

So from my perspective the Discourse side looks solved now; the practical remaining gap is mostly that major calendar clients don’t handle authenticated ICS feeds particularly well, which is why I’m falling back to a public proxy/static-file approach for now.

I’m also assuming Certbot is the simplest route here, since it can manage Let’s Encrypt issuance/renewal directly against host Nginx. I could also use acme.sh, but my impression is that it would be more of a manual choice here rather than the most straightforward path for this specific setup.

Wait what?

I’m using it with my Google Calendar just fine, as well some of my colleagues.

What part of it is incompatible with Google Calendar!?

Ah - thanks, that helps narrow it down.

On my side, Google Calendar does accept the subscription URL (via “From URL”), but the behaviour I’m seeing is:

  • the calendar is added successfully
  • however, it shows no events at all

So it’s not a case of the URL being rejected - it’s more that the feed appears empty from Google’s perspective.

Given that the raw ICS clearly contains VEVENT entries (e.g. with UID, DTSTART, SUMMARY, etc.), this makes me think it might be something like:

  • Google filtering out past events (most of my test data is historical)
  • or something about how the feed is being interpreted (e.g. time range, caching, or headers)

Let me know if there’s anything specific you’d like me to check in the feed itself.

2 לייקים

Can you check /logs for errors? I just fixed one where old recurring events where resulting on the feed failing.

If it was the same error, you need to update.

i’d have to dig back, earlier than 7pm, as there’s many warnings/ errors related to Discourse AI - token limit

לייק 1