Monitors the door at Noisebridge and tells you whether it's open or closed.
A Raspberry Pi reads a magnetic sensor on the door and pushes state changes to a cache server. The cache keeps the latest state and fans updates out to chat integrations such as Discord and Zulip.
| 1 | Pi (door sensor) ──webhook──> Cache ──webhook──> Discord / Zulip |
| 2 | | |
| 3 | polls Pi <-+ |
| 4 | | |
| 5 | +──webhook──> Pi relay ──webhook──> Home Assistant |
| Directory | What it is |
|---|---|
pi/ | Raspberry Pi OS base with laptop-built Noisebell deploy |
remote/ | Server-side services (cache, RSS, Discord, Zulip) |
hosts/noisebell-do/ | Standalone DigitalOcean NixOS host for the remote services |
secrets/ | Shared agenix-encrypted secrets and recipient rules |
Each directory has its own README with setup and configuration details.
For hosted deployment, this repo exports nixosConfigurations.noisebell-do, a small DigitalOcean NixOS host that imports noisebell.nixosModules.default. The host provides deployment-specific values like domains and the Pi address, while the Noisebell module itself points agenix at the encrypted files in secrets/ and consumes the decrypted runtime files on the target machine.
Useful commands:
./scripts/deploy-do [jet@noisebell-do] redeploys the DigitalOcean remote host./scripts/nhs redeploys the old Hetzner host using the local checkout as the flake inputscripts/deploy-pios-pi.sh pi@100.66.45.36 redeploys the Raspberry Pi OS machinescripts/share-grafana-public-dashboard jet@noisebell-do repairs or prints the deterministic public-safe Grafana dashboard linkThe full Home Assistant relay workflow is documented in pi/README.md. Public hosting, Cloudflare Tunnel, firewall, and Grafana sharing details are documented in docs/hosting.md.
Everything CI gates on is a Nix derivation, so a runner needs Nix and nothing else — no
Rust install, no cross toolchain setup, no cargo invocations of its own.
| 1 | nix build -L --max-jobs auto --cores 0 .#ci |
.#ci is a link farm over every entry in checks.x86_64-linux, which is what makes the
run parallel: Nix sees the checks as independent derivations and schedules them across all
available cores at once, rather than running fmt, then clippy, then tests, then builds.
| Check | What it covers |
|---|---|
fmt | cargo fmt --check |
clippy | --all-targets with --deny warnings |
nextest | the workspace test suite |
noisebell-{cache,rss,discord,zulip} | x86_64 release binaries |
noisebell-pi{,-relay} | aarch64 glibc, cross-compiled |
noisebell-pi{,-relay}-static | aarch64 musl, statically linked |
Individual checks build on their own, which is the faster loop when iterating:
| 1 | nix build -L --max-jobs auto --cores 0 \ |
| 2 | .#checks.x86_64-linux.clippy .#checks.x86_64-linux.nextest |
nix flake check also works and additionally evaluates the NixOS configurations, but it
skips the aarch64-linux outputs, so it is the slower and less complete of the two.
The flake pins a nightly toolchain via selectLatestNightlyWith, which walks back from the
newest nightly until it finds one that ships every component the build asks for — a
partially published nightly cannot break CI. Nightly buys two things:
-Z threads parallelises the rustc frontend within a single crate. Cargo already
builds independent crates in parallel; this fills in the tail of the dependency graph,
where a workspace this narrow otherwise compiles one crate on one core.rustc-codegen-cranelift-preview) replaces LLVM as the codegen backend for
the dev profile only. It emits object code much faster and the code it emits is slower,
which is the right trade for clippy and tests. Release binaries — the ones that actually
ship to the Pi and the DO host — stay on LLVM.Cross-compilation for both Pi targets comes from the same toolchain carrying the aarch64
std; only the C toolchain and the cargo target environment differ between the glibc and
musl variants. Each target does one shared dependency build for both of its binaries, and
that build is scoped to -p noisebell -p noisebell-relay so host-only crates never enter
the cross graph.
The build closure is 1207 store paths totalling 4.1 GB, most of it the nightly toolchain,
the aarch64 cross toolchains, and vendored crates. Roughly 0.4 GB of that substitutes
straight from cache.nixos.org (including both cross C toolchains, so there is no gcc
bootstrap); another ~0.2 GB is Rust nightly tarballs from static.rust-lang.org that are
unpacked and patchelfed locally.
A disposable VM therefore wants ~16 GB of disk — closure plus cargo target directories
during the build — and 16 GB of RAM if you run with --max-jobs auto --cores 0, since that
can put several dependency builds in flight at once with a full complement of rustc
processes in each. Halve --max-jobs if memory is tighter than that.
Every run from a fresh VM redoes all of it. If this runs often, pointing the runner at a
binary cache (attic, cachix) or giving it a persistent /nix turns most of the work into a
download, and an unchanged tree into a no-op.
The DigitalOcean host runs Prometheus, Loki, Grafana, Alloy, node_exporter, and blackbox_exporter via hosts/noisebell-do/observability.nix. Grafana provisions Noisebell Full Debug for authenticated operators and Noisebell Public for externally shared, Prometheus-only status.
https://grafana-noisebell.extremist.software/ through Cloudflare Tunnel, login requiredhttps://grafana-noisebell.extremist.software/public-dashboards/6e6f69736562656c6c7075626c696330http://noisebell-do:9090/ over Tailscalehttp://noisebell-do:3100/ over TailscaleThe Pi deploy script enables persistent journald, installs prometheus-node-exporter, and installs noisebell-loki-journal.service to ship Pi journal logs to Loki on the DO host.
Prometheus is the source of truth for regular time-based data: scrape health, host CPU/memory/disk/uptime, DO-to-Pi poll counts and last results, GPIO state, Pi hardware readings, webhook counters, and retry counters. Loki/journald is reserved for sparse event logs that should be readable in chronological order: service start/stop, door state changes, cache state changes, Pi offline/online transitions, auth or rate-limit rejections, webhook retries/failures, stale events, and GPIO read errors. Routine successful polls, unchanged poll results, metrics scrapes, and badge/image/status reads are intentionally not logged at INFO.