# Authenticated ICS feeds for private calendar events

**URL:** https://meta.discourse.org/t/authenticated-ics-feeds-for-private-calendar-events/393203
**Category:** Feature
**Tags:** events
**Created:** [January 12, 2026, 8:46am UTC](https://meta.discourse.org/t/authenticated-ics-feeds-for-private-calendar-events/393203 "2026-01-12T08:46:37Z")
**Posts on this page:** 11
**Page:** 1

<div class="post-metadata">

### Author: ![Ethsim2](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/ethsim2/32/522255_2.png) [@Ethsim2](https://meta.discourse.org/u/Ethsim2)
#### Post date: [January 12, 2026, 8:46am UTC](https://meta.discourse.org/t/authenticated-ics-feeds-for-private-calendar-events/393203/1 "2026-01-12T08:46:37Z")

</div>

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](https://meta.discourse.org/t/re-add-full-ics-export/230713/9).

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.

---

<div class="post-metadata">

### Author: ![kosmamoczek](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/kosmamoczek/32/453118_2.png) [@kosmamoczek](https://meta.discourse.org/u/kosmamoczek)
#### Post date: [March 4, 2026, 4:06pm UTC](https://meta.discourse.org/t/authenticated-ics-feeds-for-private-calendar-events/393203/3 "2026-03-04T16:06:54Z")

</div>

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:

> **[kalendasz](https://git.hswro.org/kosma/kalendasz)**
>
> Aplikacja webowa do generowania linku do kalendarza na Discourse.

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.

---

<div class="post-metadata">

### Author: ![Falco](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/falco/32/179432_2.png) [@Falco](https://meta.discourse.org/u/Falco)
#### Post date: [March 13, 2026, 9:09pm UTC](https://meta.discourse.org/t/authenticated-ics-feeds-for-private-calendar-events/393203/4 "2026-03-13T21:09:59Z")

</div>

> [@Ethsim2](#):
>
> 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)?

I added that earlier this week [in this PR](https://github.com/discourse/discourse/pull/38534). However the ergonomics of generating User API Keys aren’t great for non-technical users. To make this seamless, I’m following up with:

[https://github.com/discourse/discourse/pull/38598](https://github.com/discourse/discourse/pull/38598)

Which tries to make this as friendly as possible

 ![image](https://global.discourse-cdn.com/meta/original/4X/1/a/e/1ae3c680283d2843d71dac6082bf764c3025f565.jpeg)

---

<div class="post-metadata">

### Author: ![Falco](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/falco/32/179432_2.png) [@Falco](https://meta.discourse.org/u/Falco)
#### Post date: [March 17, 2026, 2:40pm UTC](https://meta.discourse.org/t/authenticated-ics-feeds-for-private-calendar-events/393203/5 "2026-03-17T14:40:07Z")

</div>

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

---

<div class="post-metadata">

### Author: ![Ethsim2](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/ethsim2/32/522255_2.png) [@Ethsim2](https://meta.discourse.org/u/Ethsim2)
#### Post date: [March 17, 2026, 5:17pm UTC](https://meta.discourse.org/t/authenticated-ics-feeds-for-private-calendar-events/393203/6 "2026-03-17T17:17:35Z")

</div>

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:

```bash
# 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:

```yaml
expose:
  - "80:80"
  - "443:443"

```

to something like:

```yaml
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:

```bash
cd /var/discourse
sudo ./launcher rebuild app

```

Then create a host-level Nginx site such as:

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

```

with something along the lines of:

```nginx
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:

```bash
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:

```bash
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.:

```nginx
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:

```bash
/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.

---

<div class="post-metadata">

### Author: ![Falco](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/falco/32/179432_2.png) [@Falco](https://meta.discourse.org/u/Falco)
#### Post date: [March 17, 2026, 5:27pm UTC](https://meta.discourse.org/t/authenticated-ics-feeds-for-private-calendar-events/393203/7 "2026-03-17T17:27:00Z")

</div>

> [@Ethsim2](#):
>
> 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

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!?

---

<div class="post-metadata">

### Author: ![Ethsim2](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/ethsim2/32/522255_2.png) [@Ethsim2](https://meta.discourse.org/u/Ethsim2)
#### Post date: [March 17, 2026, 5:38pm UTC](https://meta.discourse.org/t/authenticated-ics-feeds-for-private-calendar-events/393203/8 "2026-03-17T17:38:41Z")

</div>

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.

---

<div class="post-metadata">

### Author: ![Falco](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/falco/32/179432_2.png) [@Falco](https://meta.discourse.org/u/Falco)
#### Post date: [March 17, 2026, 8:28pm UTC](https://meta.discourse.org/t/authenticated-ics-feeds-for-private-calendar-events/393203/9 "2026-03-17T20:28:55Z")

</div>

> [@Ethsim2](#):
>
> however, it shows **no events at all**

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.

---

<div class="post-metadata">

### Author: ![Ethsim2](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/ethsim2/32/522255_2.png) [@Ethsim2](https://meta.discourse.org/u/Ethsim2)
#### Post date: [March 17, 2026, 9:04pm UTC](https://meta.discourse.org/t/authenticated-ics-feeds-for-private-calendar-events/393203/10 "2026-03-17T21:04:25Z")

</div>

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

---

<div class="post-metadata">

### Author: ![Falco](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/falco/32/179432_2.png) [@Falco](https://meta.discourse.org/u/Falco)
#### Post date: [March 21, 2026, 5:31pm UTC](https://meta.discourse.org/t/authenticated-ics-feeds-for-private-calendar-events/393203/11 "2026-03-21T17:31:09Z")

</div>

> [@Calendar subscription URLs for external calendar apps](https://meta.discourse.org/t/calendar-subscription-urls-for-external-calendar-apps/398902):
>
> We’ve added a new Calendar tab to user preferences that lets you subscribe to Discourse feeds in external calendar apps like Google Calendar, Apple Calendar, and Microsoft Outlook. down_arrowHow it works Navigate to your Preferences → Calendar tab and click Generate Subscription URLs. You’ll get one-click subscribe buttons for: Google Calendar — opens Google Calendar with the feed pre-filled Microsoft Outlook — opens Outlook’s web subscription dialog Apple Calendar — t…

---

<div class="post-metadata">

### Author: ![Falco](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/falco/32/179432_2.png) [@Falco](https://meta.discourse.org/u/Falco)
#### Post date: [March 21, 2026, 5:31pm UTC](https://meta.discourse.org/t/authenticated-ics-feeds-for-private-calendar-events/393203/12 "2026-03-21T17:31:13Z")

</div>


