CI jobs, cron, systemd units and your own services report to one HTTPS endpoint. Posts that share a key update a single card and keep its timeline, so a deploy is one card on your Mac and your iPhone, not five messages in a chat channel.
If it can run a shell command, it can post.
A working replica of the Mac app. Pick a topic, open an event.
If it can run curl, it can post. A title is the
only required field. Add a key if the event will change, and a topic to file it under.
curl -sS --max-time 5 https://farolim.dev/api/events \
-H "Authorization: Bearer $FAROLIM_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"topic": "acme/deploys",
"key": "deploy:web:4f2c1a9",
"status": "running",
"title": "Deploying web 4f2c1a9"
}' || true
{"id": "01K1…", "seq": 4471, "op": "created", "revision": 1}
A deploy reports when it starts, while it checks itself and when it finishes. Post
every step under the same key
and farolim revises one card and adds the step to its timeline, instead of piling up
messages. A retry that changes nothing is counted, not repeated.
{ status: "running" }→ created
142ms
{ status: "running" }→ updated
96ms
{ status: "success" }→ updated
118ms
{ status: "success" }→ deduplicated
64ms
Each post says how loudly it should arrive. Routine updates revise the card without a banner and without moving it, so a deploy that checks itself every thirty seconds stays where you left it. And a job that promised to report back and went quiet says so on its own.
"notify": "silent"
Revised in place. No banner, no reordering.
quiet
"notify": "normal"
Moves to the top and shows a notification.
banner
"notify": "high"
The same, with a sound.
banner + sound
"expects_update_within": 600
A promise. Miss it and the card turns stale.
stale
The server stores the post once, and every app shows the same card. The Mac app hears it over a WebSocket. The iPhone is pushed by the server itself, not relayed through your Mac, so the banner arrives with the laptop shut. What a producer marked silent never buzzes the phone. A web app for any browser is in progress.
curl -sS --max-time 5 \ https://farolim.dev/api/events \ -H "Authorization: Bearer $FAROLIM_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "topic": "acme/deploys", "status": "failure", "notify": "high", "title": "Deploy failed web 4f2c1a9", "body": "exit 1 at migrate" }' || true
{"op": "created", "revision": 1}
Every documented request carries a timeout and swallows its own failure, so a slow or unreachable server can’t fail the job that posted.
--max-time 5 … || true
An authenticated post that fails validation is stored with its raw body and shows up in the app, so a typo costs visibility, not data.
422 invalid_topic_path
Producer tokens can post but can’t read. The Mac app’s token reads but can’t write. A build runner never sees your stream.
src_live_… / cli_live_…
A deploy that failed at 19:54 failed. A later success is a new fact, not a correction. Nothing is marked resolved on your behalf.
status: failure
Not everything that reports is a script you wrote. A service that sends its own JSON is mapped onto a card, field by field. A tool that only knows how to post to Discord, Slack, ntfy or Gotify posts here with nothing but a new URL.
Point a service’s webhook at farolim, Shopify’s new orders for one, and choose which of its fields become the title, the status and the body. Nothing about the sender has to change.
title ← "Order \(.name)"
Uptime Kuma, Grafana, Proxmox, Watchtower and most of what you self-host can already post to one of these. Paste a farolim URL where theirs was and leave the rest; the message arrives as a card. Mattermost targets work too, because they send Slack’s shape.
discord · slack · ntfy · gotify
farolim is in development for macOS and iPhone. Leave an email and you’ll get one message, on release day.