I had been looking around for a self-hosted task manager, and settled on Vikunja. I really like the clean UX and simple configuration. But it has one significant shortcoming out of the box: mobile push notification support is a bit underwhelming.
The native (iOS/Android) app does support push notifications for reminders, but they have not worked for me. There are a few open issues around this, so maybe it will improve, but updates to the app seems slow. Even if they worked, the app lacks the rich, fine-grained event support that Vikunja itself offers via webhooks. There’s no configuration; presumably we can only get notifications for reminders.
Finally, it also depends on being able to connect to your vikunja instance. If you have a VPN connection to your home network running all the time, that might not be a big deal, but it feels fragile to me. I want something that’s going to be more robust.
I found a couple of old conversations about possible solutions — options for push notifications on android, and a configuration using ntfy and NodeRed. There are some interesting approaches, but I really didn’t want to stand up new infrastructure or use new cloud services. All the options depend on a cloud service like ntfy, and adding new infrastructure or local services to integrate it.
Home Assistant to the rescue!
But wait! I’m running Home Assistant, which opens a whole bunch of possibilities. I use Nabu Casa’s cloud service, so it’s always available. Without much effort, I was able to set up an integration so that Home Assistant can deliver notifications from Vikunja. I’ll still get them even if I can’t access Vikunja, because everything goes through HA.
This solution will:
- Allow Vikunja to send push notifications via the Home Assistant companion app to your phone
- Provide fine grained control over notifications using Vikunja’s webhooks
- Notifications include the type of event, name of project/task, and contents of comments
- Tapping on the notification will open the relevant task in Vikunja
What will actually work outside your network depends on what kind of remote access you have configured. If HA already works for you anywhere, you’ll get the notifications. If you can access your Vikunja instance, tapping on them will open the associated task. If you only have HA access but not Vikunja (e.g. you have Home Assistant cloud, but not some other general purpose VPN to access Vikunja on your network) you’ll still get the notifications, you just won’t be able to open them in Vikunja when you’re not in your home network.
Overview
The setup is pretty simple overall:
- Create an automation in HA that exposes a webhook
- Add webhooks in Vikunja for project or user-scoped events (or both) posting to the HA webhook
- When an event occurs, the HA automation is triggered and generates a push notification via the HA companion app
Step 1: The HA Automation
In automations.yaml, add a webhook trigger automation. This automation config creates the webhook itself as
/api/webhook/<your_webhook_id> — no other config is needed in HA.
You’ll need to replace these with the IPs of your services
- 172.16.2.100 - Home Assistant
- 172.16.2.101 - Vikunja
1- id: vikunja-reminder-notification2 alias: Vikunja reminder notification3 triggers:4 - trigger: webhook5 webhook_id: vikunja_reminder6 allowed_methods:7 - POST8 local_only: false9 conditions: []10 actions:11 # replace with your device's notify service12 - action: notify.mobile_app_pixel_1013 data:14 title: >-15 {%- set prefix = trigger.json.data.project.title + ': ' if trigger.json.data.project is defined and26 collapsed lines
16 trigger.json.data.project else '' -%} {{ prefix }}{{ trigger.json.event_name17 | replace('.', ' ')18 | replace('_', ' ')19 | title20 | default('Task Event', boolean=True) }}21 message: >-22 {%- if trigger.json.data.comment is defined and trigger.json.data.comment -%} {{ trigger.json.data.doer.name23 }}: {{ trigger.json.data.comment.comment | truncate(200) }} {%- else -%} {{ trigger.json.data.task.title24 | default(trigger.json.data.tasks | map(attribute='title') | join(', ')25 | default('Task event')) }}26 {%- endif %}27 data:28 icon_url: https://cdn.jsdelivr.net/gh/go-vikunja/vikunja/frontend/public/images/icons/icon-maskable.png29 notification_icon: >-30 {%- if 'comment' in trigger.json.event_name %}mdi:comment {%- elif 'reminder' in trigger.json.event_name31 %}mdi:bell {%- elif 'overdue' in trigger.json.event_name %}mdi:clock-alert {%- elif 'deleted' in32 trigger.json.event_name %}mdi:delete {%- elif 'assignee' in trigger.json.event_name %}mdi:account {%- else33 %}mdi:checkbox-marked-circle{%- endif %}34 # replace with your Vikunja URL35 clickAction: >-36 {%- if trigger.json.event_name == 'task.deleted' and trigger.json.data.project is defined and37 trigger.json.data.project -%} https://your-vikunja-url/projects/{{ trigger.json.data.project.id }} {%- elif38 trigger.json.data.comment is defined and trigger.json.data.comment -%} https://your-vikunja-url/tasks/{{39 trigger.json.data.task.id }}#comment-{{ trigger.json.data.comment.id }} {%- else -%}40 https://your-vikunja-url/tasks/{{ trigger.json.data.task.id }} {%- endif %}41 mode: singleA few notes on the template. Vikunja’s webhook body looks like this:
1{2 "event_name": "task.reminder.fired",3 "time": "2026-05-29T17:24:00Z",4 "data": {5 "task": { "id": 42, "title": "Call the plumber", ... },6 "project": { ... },7 "user": { ... }8 }9}For comment events (task.comment.created), the body also includes data.comment with the comment text and ID, and
data.doer with the commenter’s name. data.task still contains the associated task.
The title template prepends the project name when available (e.g. “Homelab: Comment Created”), then replaces dots
and underscores with spaces so the event name reads naturally — task.reminder.fired becomes “Reminder Fired”,
task.comment.created becomes “Comment Created”, task.overdue becomes “Overdue”, etc. Project-level webhooks always
include data.project in the payload (the Vikunja webhook listener injects it automatically for events that don’t carry
it natively), so the prefix is reliable for project webhooks. For user-level webhooks (reminder/overdue), data.project
is included in those event structs directly.
The message template checks for a comment first and, if present, shows the commenter’s name followed by the comment
text (e.g. “Jamie: Don’t forget the attachment”). The truncate(200) guard prevents very long comments from hitting
FCM’s 4KB payload limit, which would cause the notification to fail silently. Otherwise it falls back to
data.task.title, then to the joined titles from the data.tasks array (used by tasks.overdue batch events).
The notification_icon sets the small status bar icon using any Material Design Icon
name. It varies by event type — a bell for reminders, a comment bubble for comments, etc.
The icon_url shows a thumbnail image on the right side of the notification. Point it at a publicly accessible PNG
— your Vikunja instance’s logo works if you’re always on your home network or VPN when you receive notifications,
otherwise use a public URL. Note: this does not replace the HA logo on the left; Android doesn’t allow apps to change
their own notification icon.
The clickAction opens the task directly when the notification is tapped. For deleted tasks it links to the project
instead (since the task URL would 404). For comment events it appends #comment-<id> to link directly to the comment —
though in practice Android’s browser context doesn’t scroll to anchors, so you land on the task page regardless. Still
worth including for platforms that do support it.
local_only: false is required because the webhook comes from the Docker container’s network, which gets NAT’d through
172.16.2.101. Even though that’s on the same LAN as HA, HA needs local_only: false to accept it — the internal
Docker bridge subnet (172.30.x.x) apparently doesn’t satisfy the “local” check.
Step 2: Configure Vikunja Webhooks
In Vikunja, go to Settings → Webhooks to add a user-level webhook (fires for all your projects):
- URL:
http://172.16.2.100:8123/api/webhook/vikunja_reminder - Events:
task.overdue,task.reminder.fired,tasks.overdue
You can also create a webhook at the project level, which exposes a much wider set of events:
| Event | Fires when |
|---|---|
task.created / task.updated / task.deleted | Task lifecycle |
task.comment.created / task.comment.edited / task.comment.deleted | Comments |
task.assignee.created / task.assignee.deleted | Assignment changes |
task.attachment.created / task.attachment.deleted | File attachments |
task.relation.created / task.relation.deleted | Task relations |
task.reminder.fired | Reminder time reached |
task.overdue / tasks.overdue | Overdue (single or batch) |
project.created / project.updated / project.deleted | Project lifecycle |
project.shared.user / project.shared.team | Sharing |
The automation handles all of these with the same template — just add whichever events you want to subscribe to in the Vikunja webhook config.
Step 3: The SSRF Problem
Vikunja has SSRF protection that blocks outgoing webhook requests to private IP ranges — including 172.16.0.0/12,
which covers my entire home network. So every time a reminder fired, the webhook attempt was logged as:
1level=ERROR msg="Error while handling message ...2reason_poisoned=Post "http://172.16.2.100:8123/...":3dial tcp 172.16.2.100:8123: prohibited IP address:4172.16.2.100 is not a permitted destination (denied by: 172.16.0.0/12)"There’s a config option to disable this check: outgoingrequests.allownonroutableips. Add it to your
docker-compose.yml environment block:
1services:2 vikunja:3 image: vikunja/vikunja:latest4 environment:5 # ... other config ...6 VIKUNJA_OUTGOINGREQUESTS_ALLOWNONROUTABLEIPS: 'true'With this set, Vikunja dispatches the webhook, HA receives it, and the push notification arrives.
The Template Gotcha
One more thing worth documenting. The Watermill internal event message (visible in Vikunja’s error logs) looks like
{"webhook_id":3,"payload":{"event_name":"...","data":{...}}}. This is not what Vikunja actually sends over HTTP.
The real HTTP body is just the WebhookPayload struct:
1{2 "event_name": "task.created",3 "time": "...",4 "data": { "task": { ... } }5}So the correct HA template path is trigger.json.data.task.title, not trigger.json.payload.data.task.title. Getting
this wrong produces a huge JSON blob as the fallback message, which either fails silently or produces an unusable
notification.
Final State
The automation triggers reliably for Vikunja events. The notification title shows the event type formatted as readable text, the message shows the task title or comment text (with attribution) depending on the event, and tapping the notification opens the task directly in Vikunja. Setting reminder times on tasks works as expected — Vikunja’s reminder cron checks every minute and fires the webhook when one is due.
The only remaining limitation is that reminders require the reminder time to fall in the current minute in the user’s configured timezone. If you set a reminder and nothing fires, double-check that Vikunja’s timezone config matches your local time — the reminder cron runs in UTC by default and the timezone adjustment happens per-user at dispatch time.
More Home Assistant Integration
This configuration is only about getting push notifications for Vikunja events. I use a VPN to access Vikunja in my homelab directly on the road. If you want more direct integration, check out the Vikunja Home Assistant Integration.