Skip to content
riveerxdPublic

About

Android uptime monitoring with no server and no account. Checks run on your phone; an outage arrives as a full-screen page with a looping alarm instead of a notification you swipe away.

Topics

Resources

Stars

68 stars

Watchers

1 watching

Forks

Repository files navigation

Nightbell: the dashboard worst-first, an urgent alert, and a monitor detail

Uptime monitoring that lives on your phone and actually wakes you up.

Android Kotlin Jetpack Compose minSdk 26 targetSdk 36 Version on F-Droid Latest release Tests Apache 2.0

Get it on F-Droid   Download APK   Docs   Report a bug

nightbell.app  ·  Why  ·  What it watches  ·  Health reports  ·  Alerts  ·  Widget  ·  How it works  ·  Install

Point it at anything that answers over HTTP, at one element on a rendered web page, at a Prometheus server, at your service's own health report, at a GitHub repository, at a bare host by ping or TCP port, or at an IMAP inbox, and it watches it, charts it, and gets loud when it breaks. No server, no account, no middleman. The phone in your pocket does the checking, and the only host it contacts that you did not choose is a connectivity probe you can change or switch off.


Why

I kept missing outages. The monitoring services I tried either wanted a subscription to text me, or sent a notification that looked exactly like a marketing email and got swiped away with one. I wanted the thing that tells me production is down to be impossible to confuse with anything else.

So urgent alerts in Nightbell arrive as a red card that behaves like an incoming call, with a looping alarm that keeps going until you acknowledge it, and it wakes the screen if the phone is locked.

The urgent alert: a red heads-up with Acknowledge, Re-check and Mute

Note

Three buttons, because at 3am you want to decide one thing: Ack silences it, Re-check runs the check again there and then, and the crossed bell mutes that monitor for an hour.


What it watches

Kind What it does
Status check Hit a URL, assert on the status code. Exact, any 2xx, a range, or any response at all.
Request and response Pick the method, set headers and a body, then assert on what comes back. Contains, does not contain, equals, regex, or a JSON field by path.
Page element Loads the real page in an embedded WebView and watches DOM nodes you picked by tapping them. Existence, text, or an attribute.
Prometheus Reads an exporter's metrics, a PromQL query, or an Alertmanager's firing alerts.
Health endpoint Reads a report your service writes about itself: which part is in trouble, the rule that decided it, and what it saw. Signs in the way your own clients do, single sign-on included.
GitHub repository Polls one repository and reports what changed about it: stars, issues, releases, pull requests and comments.
Ping ICMP echo to an IPv4 or IPv6 host, without root. Latency, packet loss, and down when nothing answers.
TCP port Connects to one port and hangs up. For databases, SSH, VPN endpoints, anything with no HTTP to ask.
Mailbox Signs in to an IMAP inbox and tells you what arrived, louder for the senders or subjects you name. Works with Roundcube's server, Gmail and most others.

Page-element monitors watch any number of nodes per page and resolve them all against one page load, so watching six costs about what watching one costs. The expensive part is booting the WebView, not the assertions. If the thing you want sits behind a cookie banner or an age gate, press through it once in the live preview and every check afterwards arrives on the other side of it.

The repository kind is the odd one out, and worth saying why it belongs here anyway. Every other kind answers whether something is up; this one answers what happened. They share the cadence, the store, the alert policy and the notification plumbing, which is most of a monitor, so the alternative was a second app carrying a second copy of all of it.

Each track switches on its own: every star or only the milestones, issues narrowed by keyword or by author, releases with or without prereleases, pull requests, and comments with bot accounts filtered out. Its history lists what it saw rather than how fast GitHub answered, so a row reads "13 stars, was 12" or "Issue #6 opened" and runs of checks that found nothing collapse into one line. A digest mode holds changes for an hour or a day, for a repository that has grown past the point where a plus one is news.

A personal access token is optional. Without one you get GitHub's 60 requests an hour per IP, which a couple of repositories on a fifteen-minute cadence fit inside comfortably; with one you get 5,000, and an unchanged check answers 304 without spending any of that budget. The token is stored on the device, never logged, never put in a notification, and left out of an export unless you say otherwise.

Setting one up is a four-step wizard: pick a kind, point it somewhere, say what "healthy" means, and choose a cadence. Everything is changeable later.

Monitors that answer one question can be grouped, and a group is one card with one verdict. "Is Nightbell up" was never a question about the website or about the repository, it was a question about both, and the dashboard used to answer it one row at a time with you doing the and-ing. A group says it directly: all 2 operational, or 1 of 2 is down. The worst member decides it, and pausing one member never decides it unless every member is paused, because a pause you set last week must not be able to hide an outage.

The new-monitor wizard: What to watch, Target, Expectations, Cadence and alerts

Certificates

The expiry date arrives free with every HTTPS handshake the checker already performs, so Nightbell reads it and keeps it on its own track, measured in days rather than minutes. A certificate about to lapse is the one outage you can see coming, and it is not an outage yet: the site is up and will stay up for days, so this never wakes anyone.

Servers no public CA vouches for are watchable too. Pinned key records the key on the next successful check and requires exactly that key afterwards, which covers a NAS, a homelab box behind a private CA, and a Tor hidden service. It is stronger than trusting the system store rather than weaker: a CA can be talked into issuing for a name it should not, and no amount of that produces the key the monitor already recorded. Certificates from CAs you installed on the phone yourself are trusted as well.

Your service's own health report

A status code tells you the server answered. It does not tell you that card payments have been failing for four minutes while everything else is fine. Only your service knows that, so the Health endpoint monitor lets it say so.

Your service serves one JSON document: an overall verdict, the parts that make it up, the rule each part was judged by with the value it measured, and a short log of what it saw. The verdict is what pages. The rest is what you read when it does, so at 3am you can tell a real outage from a flaky probe without opening a server log.

The endpoint can stay behind the same sign-in as the rest of your API: a fixed header, basic auth, or single sign-on through your own web app. If the monitor's sign-in lapses it says "Not checking" instead of paging you about an outage that is not there.

The format, the field limits, how responses are read and a drop-in ASP.NET Core writer are in docs/health-format.md. An existing ASP.NET health check that answers plain Healthy works before you write any JSON.


Getting the alerts to actually arrive

This is the part every phone monitoring app gets wrong, so it is worth being specific about.

Warning

Android will happily let an app think it is alerting you while delivering nothing. Nightbell needs four grants, and there is a screen on first launch that walks you through them.

The first-launch setup screen: notifications, unrestricted battery, full-screen alerts, and Do Not Disturb access
Grant What breaks without it
Notifications Nothing is posted at all.
Unrestricted battery Android can refuse the service that owns the page and its repeat loop.
Full-screen alerts A page on a locked phone cannot wake the screen. It waits on the lockscreen.
Do Not Disturb access Bedtime mode silences urgent pages completely, which is exactly when you want one.

Two of those show a dialog in place. The other two are Special App Access toggles, and Android exposes no API to request them, so the screen deep-links straight to each toggle and re-checks when you come back. It cannot be one tap and it does not pretend to be. It is four taps instead of hunting three settings sections.

Once they are granted, the alert policy is yours to shape: what counts as down, how it sounds, the haptic pattern, how hard it escalates, and when to stay quiet.

Settings: the default alert policy, haptic styles, escalation and quiet hours

Tip

Urgent pages follow your ringer switch by default, so vibrate mode gets haptics only. There is a setting to override that if you want a pager that answers to nothing.

Outages can also go to a team: Microsoft Teams, Slack, Discord, Google Chat, ntfy, Gotify, Telegram, or any URL with a body you write yourself. Each webhook runs the same threshold and cooldown as the phone, retries when the receiver is down, and can ignore your quiet hours so the channel still hears at 3am. The details are in docs/reference.md.


The home-screen widget

Worst monitor first, tap a row to open it, tap the cog to reconfigure it. Every piece of the header switches off independently: the mark, the word Nightbell, the "1 of 6 is down" summary, the cog. That is because "make it clean" means different things to different people, and one flag for all four meant losing the summary to keep the branding.

Monitors flow into columns. A widget dragged flat has spare width and no height, so instead of pushing monitors below the fold and counting them in "+4 more", they move sideways:

The widget tall in one column, spilling into two columns, and flat in a single wide row

Columns are chosen from the size the launcher reports, capped by width, because no number of monitors justifies a column too narrow to read a name in. Below about 150dp per column the trailing latency is dropped so the name keeps its room, and a widget too short for a footer and a monitor drops the footer rather than clipping both. Columns: Auto in the widget settings, or pin it to 1, 2 or 3.


How it works

One CheckEngine runs a check, folds the result into persisted state, and decides whether to interrupt you. Six alert tracks come off that, deliberately separate, so a bug in Nightbell never gets reported as your website being down, and a certificate expiring next Tuesday never gets reported as an outage tonight.

flowchart LR
    M["Monitors<br/>HTTP · request · page element · Prometheus · health report · repository · ping · TCP port · mailbox"] --> E["CheckEngine"]
    E -->|folds each result| DB[("State<br/>one JSON doc")]
    E --> A1["Down and recovery"]
    E --> A2["Degraded on latency"]
    E --> A3["Urgent paging"]
    E --> A4["Checker health"]
    E --> A5["Certificate expiry"]
    E --> A6["Repository activity"]
    A3 --> PG["Full-screen red page<br/>looping alarm until you ack"]
    style A3 fill:#2F6BFF,stroke:#2F6BFF,color:#ffffff
    style PG fill:#FF4D57,stroke:#FF4D57,color:#ffffff
    style DB fill:#11151f,stroke:#2F6BFF,color:#ffffff
Loading

State is one JSON document in DataStore, which makes the whole store trivially exportable. Checks run on WorkManager by default, or on a foreground service if you turn on strict mode and want a cadence Android will not batch away.

The longer version

docs/reference.md has the full account: the four tracks and why the checker's health is one of them, count-based sample retention, the certificate track, the light-theme re-pick, and the reasoning behind the awkward parts. It is the file to read before changing anything load-bearing.


Install

F-Droid

Get it on F-Droid

Listed since 13 August 2026. Take this one if you have the choice: it is the only channel that updates itself.

It is also a reproducible build. F-Droid compiles Nightbell from this source on their own machines, compares the result against the APK attached to the GitHub release, and publishes mine only because the two match byte for byte. So the F-Droid download and the direct download are the same file with the same signature: either one updates the other, and you can move between them without uninstalling and losing your monitors. What it costs to keep that true is docs/FDROID.md, and the choice is one directional, so read it before changing anything a build depends on.

Obtainium

Point Obtainium at the repository and it watches Releases for you:

https://github.com/riveerxd/nightbell

The APK on its own

QR code linking to nightbell.app/download

nightbell.app/download redirects to the current release, so it is the link to scan off a phone or paste somewhere a versioned URL would rot. Every build is also in Releases and in artifacts/ in this repo:

curl -fL -o nightbell.apk https://nightbell.app/download && adb install -r nightbell.apk

That reads the redirect rather than naming a version, so it does not go stale. The version it resolved to is in the filename inside artifacts/ if you want the file by name instead.

Nightbell is not on IzzyOnDroid and not on Google Play. Play is not a "not yet": the foreground service declares specialUse rather than dataSync, because dataSync is capped at six hours a day from API 34 and that cap would quietly break the one guarantee strict mode exists to make. Getting it through review would mean justifying the subtype.

Coming from an older version

Important

3.0.0 does not update a 2.x install. The app was called Pulse until 3.0.0 and its application id moved with the name, from me.river.pulse to me.river.nightbell. Android identifies an app by that id, so 3.0.0 installs beside the old one with an empty data directory. No signing key or manifest setting changes that.

To carry your monitors across: export the JSON from Settings in 2.4.3, install 3.0.0, import it, then uninstall Pulse. Placed widgets and your notification channel settings do not survive, because a launcher stores a widget provider as a fully-qualified ComponentName and channel grants belong to the old package. Full detail in docs/MIGRATION_3.0.0.md.

A direct APK is signed with my own key, so Play Protect will ask you to confirm the first time. 3.0.1 onward updates in place as usual.

3.0.1 changed the signing key. The old certificate's subject read CN=Pulse Monitor, and a subject cannot be edited without issuing a new certificate. Android refuses an update signed by a different key, so if you installed 3.0.0 you have to uninstall it before installing 3.0.1. Export first.

Build

Needs JDK 17 and an Android SDK with API 36. local.properties wants sdk.dir=/path/to/Android/Sdk.

./gradlew :app:assembleDebug          # debuggable
./gradlew :app:assembleRelease        # minified, needs keystore/keystore.properties
./gradlew :app:testDebugUnitTest      # 1323 JVM tests

Release builds are signed from keystore/keystore.properties, which is gitignored along with the key itself. Without it the release task still builds, just unsigned.

Tests: 1323 JVM + 586 on-device

1323 JVM tests cover the pure logic: the alert state machines, escalation, quiet-hours arithmetic, assertions, the latency baseline, backup round-trips, and the widget's column arithmetic, which decides whether a monitor is visible at all and can otherwise only be exercised by dragging a widget around a home screen. 586 instrumented tests cover the parts that need a real Android: notifications, channels, the foreground service, the WebView element checker, widgets, the launcher icon's transparency, and a suite that drives a genuine connection-refused outage all the way to a red page on screen.

# one class at a time, the emulator does not enjoy the whole suite at once
adb shell am instrument -w -e class me.river.nightbell.UrgentPageEndToEndTest \
  me.river.nightbell.debug.test/androidx.test.runner.AndroidJUnitRunner

The urgent path has a habit of breaking in ways only a device shows: the alarm taking half a minute to stop after acknowledging, a custom notification layout getting squashed vertically, colorisation being silently dropped. None of that is visible from reading the code, so those cases got tests that were checked against the broken version first.

Known limitations

Important

Quiet hours still suppress urgent pages unless you turn on the bypass. That default is wrong and is the next thing I want to fix properly, with a migration rather than a flipped constant.

  • Element checks need a WebView per page load. They are the slow kind of check.
  • Repository monitors share GitHub's unauthenticated budget of 60 requests an hour per IP until you add a token. Several repositories on a short cadence will find the ceiling.
  • Strict mode costs battery and a permanent notification. That is Android's price for a real cadence, not a design choice.
  • No CI. The test commands above are run by hand.

Licence

Apache 2.0, see LICENSE. Same licence as everything it depends on (androidx, Compose, OkHttp, kotlinx), so there is nothing awkward to reason about if you want to fork it or lift a piece of it.

Do what you like with it. Keep the notice, and say what you changed.


Built to be impossible to confuse with a marketing email. If it saved you an outage, a ⭐ is appreciated.

About

Android uptime monitoring with no server and no account. Checks run on your phone; an outage arrives as a full-screen page with a looping alarm instead of a notification you swipe away.

Topics

Resources

Stars

68 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages