Architecture and security in Tempo v1
📅 May 20, 2026 · Tempo 1.0.4 · Leo from Caereforge
A pass at what’s inside Tempo v1: enough to give you a mental model of the app, with enough detail on the security side to let you decide whether you trust it on your network. The architecture section is deliberately light. My goal is to describe the shape of the system, not publish a blueprint someone could clone in a weekend. The security section is more detailed, because that’s where a technical reader has earned the right to specifics.
Architecture, high-level
Tempo is a closed-source native Mac app, distributed as a signed and notarized DMG. The binary is small (~7 MB) and Universal (Apple Silicon and Intel). It runs as a single process: no helper daemon, no background service, no shared database accessed by multiple writers.
Internally, the codebase is split into a handful of layers that map roughly to function:
- An app lifecycle layer that owns the window, the menubar, the SwiftUI scene, and the cross-feature wiring.
- A core data layer that defines the canonical event and action shapes. Every source normalizes to this representation before storage.
- A persistence layer on SQLite. Every schema change ships as a numbered, sequential migration; no ad-hoc
ALTERin the running app, no “unknown key fallback” that absorbs whatever a payload happens to contain. Single file on disk under Application Support, backed up weekly (plus on-demand from Settings, to a path you choose). - An ingestion layer: HTTP listener that binds the LAN by default, accepts POST requests carrying JSON payloads, validates them, enforces per-source authentication, and writes through the persistence layer.
- A providers layer that bridges native macOS APIs (EventKit for Apple Calendar and Reminders, an opt-in CalDAV engine Fastmail-focused in v1.0.3) and surfaces their events alongside the inbound webhook signal.
- A UI layer in SwiftUI that renders the timeline, the agenda panel, the action panel, the score editor, and the settings.
On top of those layers sits the score abstraction. A score is a small JSON file that tells Tempo how a source should look in the timeline (color, severity rules, grouping policy, default actions). Scores live outside the binary, in a folder under Application Support: edit the JSON, the app picks up the change.
Scores are the boundary between Tempo’s generic event model and the specifics of your stack: the payload is input, the score is policy, your clicks are the only authority that fires anything.
That is the architecture in a few paragraphs. The deeper “why” of each layer (concurrency model, JSON column versus EAV, the stateful-vs-stateless event convention, the way the schema migration system bootstraps a brand-new database) is the kind of detail that doesn’t change anything for an end user, and I’d rather not publish a blueprint of internal trade-offs. The bundled scores are JSON files you can read directly. The user guide covers the user-facing shape of the model in depth.
Security posture
This is the section where a technical reader has earned specifics. I owe you those.
Network reach. Tempo’s ingestion listener binds 0.0.0.0 by default, not loopback. The rationale is concrete: the wedge audience runs Home Assistant, a NAS, UniFi controllers, monitoring stacks on dedicated hosts - not on the same Mac that runs Tempo. A loopback-only default would force every webhook source onto the Mac itself, or through some local SSH tunnel kludge. LAN-first is therefore a deliberate posture, not an oversight. If your setup doesn’t need it, Settings → Ingestion exposes a “Limit to loopback only” toggle that flips the listener to 127.0.0.1 immediately.
Per-provider tokens. Every sender (Kopia, UniFi, Home Assistant, your custom scripts) gets its own token, bound to a provider identifier prefix. Tokens are not shared, not derived from a master secret, and not bundled with the app. You generate them in Settings, paste them into the sending tool’s configuration, and revoke them independently. If one sender’s token leaks, the attacker can impersonate that sender only: they cannot pose as another provider, and cannot escalate to a Tempo-wide compromise.
Keychain-stored secrets. Tokens live in the macOS Keychain, scoped to the Tempo process. Not in UserDefaults, not in flat files, not in environment variables. Per-process access mediation is enforced by the OS, not by app-level discipline.
Input validation and rate limiting. Every accepted request passes a centralized validator: the schema is strict (unknown fields are rejected, not silently absorbed); every string field has a size cap; URL schemes used in action triggers are explicitly allowlisted (https, ssh, file, and a small handful more - no javascript:, no exotic schemes). Per-token rate limits prevent a misconfigured sender from drowning the database.
Audit log. Every accepted and every rejected request is recorded through the macOS unified logging system. The log captures enough metadata to reconstruct an incident (token used, source IP, response code, timestamp) but does not record the payload contents themselves. Export the diagnostic bundle from Settings → Help if you ever need to share it with me without sharing the data inside the events.
Local data, no telemetry. Tempo does not include any third-party analytics SDK. The app does not phone home. There’s no license server to check against - v1 is freeware. The database stays on your disk. Backup files stay on a path you choose: iCloud Drive, a mounted NAS, a local folder, all work uniformly because Tempo just writes to the path you give it.
Signing and notarization. The DMG is signed with Caereforge’s Apple Developer ID and notarized through Apple’s notarization service. Gatekeeper verifies authenticity on first launch automatically. If you’d like an extra integrity check from a terminal, the SHA-256 of each release is published on the changelog and on the downloads page.
Auto-update. Tempo updates via Sparkle, with EdDSA-signed appcasts. The update flow verifies the appcast signature, then the DMG signature, then notarization: three independent checks before anything writes to your Applications folder.
Distribution-level analytics, disclosed honestly. Cloudflare serves the downloads. The CDN records aggregate counts at the edge (downloads per day, broad country attribution, version requested). No IP retention, no cookies, no fingerprinting, no per-user tracking. The full disclosure is on the privacy page. The distinction between app-level telemetry (none) and distribution-level edge analytics (aggregate, server-side, no PII) is one I take seriously and would rather make explicit than gloss over.
Honest gaps in v1
A few things I am explicit about:
- TLS on the ingestion port is a v2 item. The v1 threat model treats LAN as a semi-trusted zone, which is consistent with how most homelab tooling positions itself; I would rather flag it explicitly than imply otherwise. If you need TLS today, a small Caddy or nginx in front of Tempo’s port works.
- No outbound automation, no bidirectional sync. Tempo reads events. It does not POST back to your sources. v2 may extend this behind explicit per-action trust gates; v1 deliberately does not.
- Closed source. v1’s binary is closed, which means you cannot audit the code. What I offer in compensation: notarized + signed binary, disclosed threat model (this post), scores as plain-text JSON you can read and modify, an audit log that records every request, and the scope-and-discipline post that lays out what Tempo will and will not become.
Security questions and feedback are welcome on the tempo-scores GitHub repo, alongside score contributions. The Discord (link in the menu) is where deeper conversations happen.
Leo from Caereforge