This is the full developer documentation for Northplane # Northplane > Monitoring and alarming in one static binary — from the first ping to the phone call that wakes someone up. Install in five minutes One binary, one container, or a Compose stack with automatic TLS. Open `/setup`, create the admin, add a host. [Quickstart →](/docs/getting-started/quickstart/) Monitor anything 17 built-in checks, Nagios plugins, SNMP polling and traps, the `np-agent`, heartbeats, discovery, business services and SLAs, dashboards and reports. [Monitoring →](/docs/monitoring/hosts-and-services/) Alarm everyone Phone, SMS, MQTT, ESPA, e-mail and webhooks in — voice calls with IVR, SMS, push, ntfy, Slack, Teams and tickets out. On-call schedules, escalation chains, durable retries, full audit. [Alarming →](/docs/alarming/overview/) API-first, AI-ready Every capability is a REST endpoint with RBAC and tenants; the UI, the `np` CLI, the AI agent chat and the MCP server all use it. [API reference →](/docs/reference/api-overview/) ## Find your way [Section titled “Find your way”](#find-your-way) [I operate an instance](/docs/administration/configuration/)Configuration reference, authentication, storage, TLS, upgrades, security hardening. [I run production](/docs/deployment/overview/)Deployment variants, the CI/CD pipeline, provisioning, operations runbook and the verified environment inventory. [I integrate with it](/docs/reference/api-overview/)REST conventions, ingest webhooks, SSE stream, the OpenAPI reference, the four CLIs and the MCP server. [I work on the code](/docs/development/setup/)Dev loop, tests, backend and frontend architecture, release process, and how these docs are built. [I use the UI](/docs/ui/navigation/)Every page, dialog and Admin tab explained, with keyboard shortcuts and the tenant switcher. [I want the concepts first](/docs/concepts/architecture/)Architecture, object model, checks and states, events, alerts and incidents, tenancy, federation. ## At a glance [Section titled “At a glance”](#at-a-glance) | | | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Server** | `northplaned` — one static Go binary with the React UI, this manual, Swagger UI and the MCP server embedded | | **Storage** | SQLite (default, zero-ops) or PostgreSQL; NP-TSDB for metrics; durable outbox; tamper-evident audit chain | | **Inputs** | built-in checks, Nagios plugins, `np-agent`, SNMP v1/v2c/v3 polling and traps, heartbeats, webhooks, Alertmanager, IMAP, MQTT, ESPA 4.4.4 / ESPA-X, phone (Twilio or Asterisk), SMS | | **Outputs** | voice with IVR acknowledgement, SMS, e-mail, mobile push (FCM/APNs), Web Push, ntfy, Slack, Teams, webhooks, MQTT, ServiceNow / Jira / Zendesk tickets | | **Control plane** | REST API with RBAC, tenants, sites (federation), API tokens, OIDC / LDAP, YAML config bundles, `np` CLI | | **AI** | agent chat over 10 LLM provider types with policy gates and approvals; MCP server for Claude, Cursor, VS Code and friends | | **Runs as** | binary + systemd, Docker (distroless), Docker Compose with Caddy TLS, or behind your own reverse proxy | These pages are shipped inside every Northplane binary at `/docs/` and mirror the version they came with. The public showcase instance is [doktrace.com](https://doktrace.com) (its docs: [doktrace.com/docs](https://doktrace.com/docs/)). Machine-readable copies for AI assistants: [`llms.txt`](/docs/llms.txt), [`llms-full.txt`](/docs/llms-full.txt). # Demo mode > northplaned serve --demo and NORTHPLANE_DEMO seed a complete, idempotent showcase — hosts, checks, alarm chain, on-call, BPI, dashboard, report, two demo users — guarded against real data and kept in its own data directory. Demo mode seeds a self-contained showcase environment into the default tenant so you can click through a populated instance: real built-in checks against loopback and public targets, a passive job with a heartbeat, the full notification/escalation/on-call stack, a business-service tree with an SLA, a dashboard, a scheduled report, inbound event sources, a recurring downtime and two demo users. Seeding only writes configuration — the scheduler and executor then run the checks live. ## Enabling it [Section titled “Enabling it”](#enabling-it) | How | Behaviour | | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `northplaned serve --demo` | seeds **unconditionally** on every start (idempotent). Optional flags: `--demo-snmp host:161` (target of the SNMP demo checks, default `127.0.0.1:161`) and `--demo-traps udp://:9162` (listen address of the demo SNMP-trap source). This is what `make dev` and the e2e suite use. | | `demo: true` in `config.yaml` or `NORTHPLANE_DEMO=true` | seeds on start, but only after the **real-data guard** passes (below). This is the demo/real switch of the production stacks. | Seeding runs before the HTTP listener comes up; a failure is fatal (`demo seed: …`). The log reports what happened: ```text demo: user ready name=demo-operator email=operator@demo.local password=operator-demo-2026! role=operator demo: user ready name=demo-viewer email=viewer@demo.local password=viewer-demo-2026! role=viewer demo: hint msg="passive service demo-batchjob & heartbeat demo-cron have no live feeder — …" demo: hint msg="channel demo-email points at a mock SMTP sink on 127.0.0.1:2525 and demo-hook at http://127.0.0.1:18081/hook — …" demo: hint msg="event-source demo-hook-in uses authMode=token with secretRef \"demo-hook-in-token\" — …" demo: environment seeded counts=map[alert-rule:2 business-service:4 channel:2 …] ``` ## Demo users [Section titled “Demo users”](#demo-users) | Login | Password | Role | Can | | -------------------------------------------- | --------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `operator@demo.local` (name `demo-operator`) | `operator-demo-2026!` | `operator` | read everything, create and edit hosts/services, ack/resolve/raise alerts, incidents, downtimes, silences, on-call, dashboards, reports — but **no** Admin tabs and no config documents (templates, rules, channels need `config:write`). | | `viewer@demo.local` (name `demo-viewer`) | `viewer-demo-2026!` | `viewer` | read only. | The demo does **not** create an administrator. The admin is the break-glass account that `northplaned serve` seeds on every start unless `NP_DEFAULT_ADMIN_DISABLED` is set (`admin@localhost` with a generated password in the log, or `NP_DEFAULT_ADMIN_EMAIL` / `NP_DEFAULT_ADMIN_PASSWORD`) — see the [Quickstart](/docs/getting-started/quickstart/#2-create-the-admin-account). Demo users close /setup The demo users are local accounts, so after `--demo` the first-run `/setup` page is closed even if you disabled the default-admin seeding. If you run `NP_DEFAULT_ADMIN_DISABLED=1 northplaned serve --demo` you have no admin at all; create one headlessly like the e2e suite does — `northplaned bootstrap-admin` for a `*:*` token, then `POST /api/v1/users` with `roles: ["admin"]` — or keep the default-admin seeding on. ## What is seeded [Section titled “What is seeded”](#what-is-seeded) Every artefact is named `demo-…`, labelled `demo=true`, and lives in the default tenant. | Kind | Names and key settings | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Templates | `demo-host-base` (host: interval 30 s, retry 10 s, 2 attempts, timeout 10 s), `demo-web-service` (service: 60 s, timeout 10 s) | | Hosts (folder `/demo`) | `demo-gateway` (`127.0.0.1`, `builtin:icmp`, 15 s; labels `role=gateway`, `site=demo`); `demo-web` (`builtin:https` against `https://example.org`, parent `demo-gateway`, template `demo-host-base`); `demo-dns` (`builtin:dns -H example.org`, parent `demo-gateway`); `demo-snmp-device` (`builtin:snmp` sysUpTime against the `--demo-snmp` target, 30 s) | | Services | `demo-snmp-ifwalk` (`snmp-walk` ifOperStatus, 60 s) and `demo-tls` (`tls-cert example.org:443 -w 21 -c 7`, every 6 h) on `demo-snmp-device`; `demo-web-latency` (`https -w 1.0 -c 3.0`, 30 s) on `demo-web`; `demo-batchjob` (passive, `stalenessAfter` 10 m, contact group `demo-ops`, `notifyOn` critical+recovery) on `demo-gateway` | | Heartbeat | `demo-cron` — expect every 5 m, grace 1 m, severity warning | | Contacts, group | `demo-alice` (`alice@demo.local`, e-mail), `demo-bob` (`bob@demo.local`, webhook + e-mail), group `demo-ops` | | Channels | `demo-email` (SMTP `127.0.0.1:2525`, from `northplane@demo.local`, `allowPlaintext`), `demo-hook` (webhook `http://127.0.0.1:18081/hook`) | | Alert group | `demo-storm` (group by host, 5 m window, min count 3) | | Escalation policy | `demo-escalation`: step 0 → `demo-ops` by e-mail; +15 m unless acked → `demo-bob` by webhook | | Alert rules | `demo-critical` (CEL: hard `state_change` to CRITICAL/DOWN; severity critical; title `demo: {{ .event.object }} is {{ .event.state }}`; policy `demo-escalation`; group `demo-storm`; sets label `demo=true`), `demo-heartbeat-rule` (heartbeat rule on `demo-cron`, every 5 m, warning) | | On-call schedule | `demo-oncall` (Europe/Vienna; layer `primary`, weekly alice → bob, anchored Monday 2026-01-05 08:00) | | Business services | root `demo-webshop` (rule worst, SLA 99.9 % monthly) with leaves `demo-webshop-web`, `demo-webshop-dns`, `demo-webshop-gateway` bound by selectors such as `role=web,demo=true` | | Dashboard | `demo-overview` (shared): counters, problems, metric chart of `demo-web-latency` (`time`, 3 h), BPI `demo-webshop`, table with selector `demo=true` | | Report | `demo-availability`: availability over 30 days for `demo=true`, folder `/demo`, schedule `daily@07:00`, e-mailed to alice, keep 7 | | Event sources | `demo-hook-in` (webhook, token auth, `secretRef: demo-hook-in-token`), `demo-traps` (SNMP trap listener on the `--demo-traps` address, community `public`, severity warning), `demo-imap` (IMAP `127.0.0.1:3143`, **disabled**) | | Downtime | `demo-batchjob-nightly`: fixed, next 03:00 Europe/Vienna for 1 h, `RRULE FREQ=DAILY;BYHOUR=3;BYMINUTE=0` | | Users | `demo-operator`, `demo-viewer` (above) | What you will see after a minute: `demo-gateway` UP (if ICMP works for the server’s user), `demo-web`, `demo-dns`, `demo-tls` and `demo-web-latency` OK when the host has internet access, the SNMP objects CRITICAL/UNKNOWN unless `--demo-snmp` points at a reachable SNMP agent, `demo-batchjob` turning UNKNOWN (stale) after 10 minutes, and the `demo-cron` heartbeat reported missing right away — which opens a warning alert through `demo-heartbeat-rule`, so the alarm pipeline has something to show. ### Parts that need a helping hand [Section titled “Parts that need a helping hand”](#parts-that-need-a-helping-hand) The seeder writes configuration only; a few pieces point at infrastructure it does not start: * `demo-email` and `demo-hook` deliver to a mock SMTP sink on `127.0.0.1:2525` and a webhook sink on `127.0.0.1:18081`. Nothing listens there by default, so their deliveries fail, retry with backoff and end up under **Admin → Dead letters** — a realistic demonstration of the outbox, but run a sink (any SMTP test server, any HTTP echo) on those ports if you want green deliveries. * `demo-hook-in` authenticates inbound webhooks with the secret `demo-hook-in-token`, which is not created. Store it (`PUT /api/v1/secrets/demo-hook-in-token`, or **Admin → Secrets**) and then `POST /api/v1/ingest/demo-hook-in` with `Authorization: Bearer `. * `demo-batchjob` and `demo-cron` have no feeder. Submit a result (`POST /api/v1/results` with `{"results":[{"host":"demo-gateway","service":"demo-batchjob","state":0,"output":"batch ok"}]}`) and beat the heartbeat (`POST /api/v1/heartbeats/demo-cron/beat`), both with a token holding `objects:write`, to watch them recover. (The log hint names `/checks/results`; the real path is `/api/v1/results`.) * The SNMP demo wants an SNMP agent: `--demo-snmp 10.0.0.1:161` targets a real device with community `public`; traps sent to the `--demo-traps` port (`9162/udp`, publish it in Docker) show up as events. ## Idempotency and the real-data guard [Section titled “Idempotency and the real-data guard”](#idempotency-and-the-real-data-guard) * **Idempotent.** Re-running the seed updates in place: configuration resources are upserted by name, objects are matched by kind, host and name, and ids are derived deterministically from the names (SHA-256-based, UUID-shaped), so cross-references such as BPI parents stay valid. Existing demo users are reported again instead of failing. You can leave `--demo` on permanently. * **Guarded.** With `demo: true` / `NORTHPLANE_DEMO=true` the server first checks whether the default tenant already contains **any host without the label `demo=true`** (up to 5000 hosts; a query error counts as “real data”). If so it logs `NORTHPLANE_DEMO is set but this database already holds real (non-demo) hosts — skipping demo seeding to protect production data; use a dedicated data dir/volume for the demo, or unset NORTHPLANE_DEMO` and starts without seeding. The explicit `--demo` flag bypasses the guard — do not use it on a production data directory. * **No teardown command.** Demo artefacts are easy to find (label `demo=true`, prefix `demo-`, the Objects page filter `demo=true`), but the clean way to get rid of them is the one the production stacks use: a separate data directory you can delete. ## Demo and real data directories in the production stack [Section titled “Demo and real data directories in the production stack”](#demo-and-real-data-directories-in-the-production-stack) The CI-managed stacks under `deploy/` treat `NORTHPLANE_DEMO` as a switch that also selects the data directory inside the same volume: deploy/.env (rendered by the deploy workflow, excerpt) ```ini NORTHPLANE_DEMO=true NORTHPLANE_DATA_DIR=/var/lib/northplane/demo # false → /var/lib/northplane/real ``` Demo mode uses `/var/lib/northplane/demo`, real mode `/var/lib/northplane/real`, so flipping the switch never mixes the datasets and each side keeps its own database, events, TSDB and `secret.key`. The GitHub variable `NORTHPLANE_DEMO` (and the `demo` dropdown of the manual Deploy run: `repo-default` / `true` / `false`) controls it; the public showcase instance has run in real mode since 2026-08-20 with its demo directory kept alongside. Details: [CI/CD](/docs/deployment/ci-cd/) and [Operations](/docs/deployment/operations/). For a hand-run container the same idea is `-e NORTHPLANE_DEMO=true -e NORTHPLANE_DATA_DIR=/var/lib/northplane/demo`, or simply a second named volume. ## Development and tests use it too [Section titled “Development and tests use it too”](#development-and-tests-use-it-too) * `make dev` starts the backend with `-demo` (set `NP_DEV_DEMO=0` to skip) and prints the demo credentials; the generated break-glass admin password appears in the `[api]` log lines. * The Playwright end-to-end suite (`make e2e`) boots an isolated `northplaned serve --demo` with `NP_DEFAULT_ADMIN_DISABLED=1`, mints a token with `bootstrap-admin`, creates its own admin through `POST /api/v1/users`, and pins the browser locale to `de-DE` — so the demo data is what the e2e tests click through ([Testing](/docs/development/testing/)). * The CI `e2e` job does the same against every commit. ## Related [Section titled “Related”](#related) * [Quickstart](/docs/getting-started/quickstart/) and [First steps](/docs/getting-started/first-steps/) * [Configuration](/docs/administration/configuration/) — the `demo` key and `NORTHPLANE_DEMO` * [CLI: northplaned](/docs/reference/cli-northplaned/) — `serve --demo`, `--demo-snmp`, `--demo-traps` * [Storage](/docs/administration/storage/) — data directory layout and backups # First steps > Orientation after the install — the UI in ten minutes, creating objects in the UI and with a bundle, templates, a minimal channel → contact → escalation policy → rule chain, your first API token and the np CLI, and installing np-agent. You have a running instance and an admin login ([Quickstart](/docs/getting-started/quickstart/)). This page is the guided tour that follows: where things are in the UI, how to create objects properly, how the alarm chain fits together, and how to talk to the API. ## The UI in ten minutes [Section titled “The UI in ten minutes”](#the-ui-in-ten-minutes) The sidebar has sixteen entries; labels follow your browser language (German or English — there is no in-app switch). The important stops, in the order you will use them: | Page | What it is for | | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Overview (Übersicht)** `/` | Four KPI tiles (hosts up, services OK, active problems, open alerts), the open problem list, a service-status donut, open incidents, who is on call now, the last 20 events. **Wallboard** (`/?wallboard=1`) is the same page without chrome, refreshing every 10 s. | | **Problems (Probleme)** | Every object in a hard non-OK state, with hover actions **Ack (Quittieren)**, **Downtime** and **Check now (Jetzt prüfen)**; a checkbox includes acknowledged/downtime objects. | | **Objects (Objekte)** | All hosts and services. Two filter boxes: a **label selector** (`env=prod,role in (db,cache)`) and a **full-text** search over name and output, plus kind and state selects; filters live in the URL so views are linkable. Buttons **New host**, **New service**, **Batch add (Massenanlage)**. Click a row for the detail page with **Overview / History / Configuration** tabs. | | **Alerts (Alarme)** | Open and acknowledged alerts with **Ack** and **Resolve**; **Trigger alarm (Alarm auslösen)** raises a manual alert through an escalation policy. **Incidents** groups alerts; **Events** is the raw, filterable event log with an NDJSON export. | | **Alert rules (Alarm-Regeln)** `/alerting` | Tabs **Alert rules** (with a tester), **Groups**, **Escalations** (with a simulator), **IVR menus** — the alarming configuration. | | **On-Call (Bereitschaft)** | Schedules with layers, 14-day timeline, overrides, ICS export, who is on duty now. | | **Dashboards**, **Business services**, **Reports** | Grid dashboards with 11 widget types; BPI trees with SLA budgets; scheduled availability/SLA/alert/on-call/audit reports. | | **Maintenance (Wartung)** | Silences and downtimes (fixed, flexible, recurring RRULE). | | **Templates** | Tabs **Templates**, **Check commands**, **Time periods**. | | **Discovery** | CIDR scans and one-click adoption of the suggestions. | | **AI agent (KI-Agent)** | The agent chat workspace (needs a provider connection). | | **Admin (Administration)** | 21 tabs: Users, Roles, Contacts, Contact groups, Channels, Event sources, Webhooks, Heartbeats, Tenants, Sites, Secrets, API tokens, MCP, Agents, Dead letters, Config bundles, Audit log, AI approvals, AI providers, System health, Appearance. | Useful everywhere: * **Ctrl/⌘ K** opens the command palette: jump to pages, search objects by name, open the Wallboard or the API docs. **Ctrl/⌘ I** toggles the assistant sidebar. Two-key chords `g o`, `g p`, `g h`, `g a`, `g e` go to Overview, Problems, Objects (hosts), Alerts, Events. * The sidebar’s **Refresh (Aktualisierung)** select (5 s–60 s or off, default 30 s) controls how often the live lists poll. The UI polls; it does not hold an SSE connection. * **Admin → Appearance (Darstellung)** sets the instance-wide colour theme (31 to choose from) and light/dark mode; every user sees the same branding. * An admin with `admin:tenants` sees a **tenant switcher** at the top of the sidebar. The full map of every page and dialog is in the [User interface](/docs/ui/navigation/) section. ## Create objects [Section titled “Create objects”](#create-objects) ### In the UI [Section titled “In the UI”](#in-the-ui) **Objects → New host (Host anlegen)** opens a dialog with four tabs: 1. **Basics (Basis)** — Name (unique per tenant, cannot be renamed later), Folder (`/` by default, e.g. `/prod/web`), Address, Labels (key/value chips). For a service: Host. 2. **Check (Prüfung)** — the check command as *kind + remainder*: `builtin` (e.g. `icmp`, `http`, `tcp`, `dns`, `snmp` — the field suggests all 17), a *named check command* from the catalog, `exec` (a Nagios plugin under `pluginsDir`), `agent:exec` (run by `np-agent`), or `passive`. A new object starts as `passive`, so pick a kind. Then Arguments (one per entry), Templates, and the scheduling box: Interval (60 s), Retry interval (15 s), Max attempts (3), Timeout (30 s), Check period (`24x7`). 3. **Notifications (Benachrichtigungen)** — contact groups and contacts notified directly on hard changes, which states notify (`notifyOn`), notification period. 4. **Advanced (Erweitert)** — parents (host reachability), check/notification/flap-detection overrides, threshold mode, staleness deadline and text for passive objects, zone, custom vars (`$_HOSTKEY$` macros), a Markdown runbook. **Batch add (Massenanlage)** creates many objects at once, one per line in the grammar `name address [tmpl,tmpl] [k=v,k=v]`, with a shared folder, check command (default `builtin:icmp`) and mode `partial` or `all-or-nothing`; the dialog previews and validates before it posts to `POST /api/v1/objects:batch`. Field-by-field reference: [Hosts and services](/docs/monitoring/hosts-and-services/). ### With a bundle and `np apply` [Section titled “With a bundle and np apply”](#with-a-bundle-and-np-apply) Everything the dialog does is also a YAML document. A **bundle** is a multi-document YAML file (`---` separated) of `kind` / `metadata` / `spec` documents; kinds are applied in dependency order (templates before hosts before services), and re-applying the same bundle is a no-op. This one creates a template, a host and two services: web-01.yaml ```yaml kind: Template metadata: { name: linux-base } spec: kind: host spec: checkCommand: builtin:icmp interval: 30s maxCheckAttempts: 2 --- kind: Host metadata: name: web-01 folder: /prod labels: { env: prod, role: web } spec: address: 10.0.0.10 templates: [linux-base] --- kind: Service metadata: name: https host: web-01 spec: checkCommand: builtin:http args: ["-u", "https://10.0.0.10/", "--insecure", "-w", "1", "-c", "3"] --- kind: Service metadata: name: ssh host: web-01 spec: checkCommand: builtin:tcp args: ["-p", "22"] ``` Note the shape of the `Template` document: its `spec` is the template resource itself, so the inheritable object settings sit one level deeper under `spec.spec`. Host and Service documents put the object spec directly under `spec`. Apply it with the CLI (needs an API token with `config:write`, see [below](#create-an-api-token-and-use-np)): ```bash np apply -f web-01.yaml --dry-run # would apply create Template/linux-base … np apply -f web-01.yaml # applied create Host/web-01 … np export > everything.yaml # canonical bundle of the whole tenant ``` The same YAML can be pasted into **Admin → Config bundles (Config-Bundles)**, which shows the plan (create/update/delete with field diffs) and applies it in a second step. Fields absent from a bundle are left unmanaged; `--prune` deletes what the bundle no longer contains. Full format, kinds and semantics: [Config bundles](/docs/administration/config-bundles/). ### Templates [Section titled “Templates”](#templates) A template is an `ObjectSpec` fragment that objects (and other templates) inherit. Resolution is `built-in defaults ⊕ templates in declared order (later wins) ⊕ the object's own spec`; `vars` are merged key by key, list fields are replaced wholesale. The object detail page shows the resolved result under **Configuration → Effective configuration** together with the template chain, and the API returns it from `GET /api/v1/objects/{id}/effective-config`. Manage templates, reusable named check commands (`exec`/`builtin`/`agent`/`passive` with `$ARGn$`) and time periods under **Templates**. Details: [Templates](/docs/monitoring/templates/) and [Object model](/docs/concepts/object-model/). ## A minimal alarm chain [Section titled “A minimal alarm chain”](#a-minimal-alarm-chain) State changes alone do not notify anyone. Northplane notifies through a chain of four resources — channel → contact → escalation policy → alert rule. The minimal version, in the UI: 1. **Channel** — **Admin → Channels (Kanäle) → Create (Anlegen)**. Type `ntfy`, name `ntfy`, **Enabled (Aktiv)** on, Server URL `https://ntfy.sh`, a private topic name. Save and click **Send test (Test senden)**. (Any other type works the same; e-mail needs `provider`, `host`, `from`, credentials — see [Channels](/docs/alarming/channels/).) 2. **Contact** — **Admin → Contacts (Kontakte) → Create**. Name `alice`, E-Mail, optional phone (E.164, for SMS/voice), time zone. Preferences (which channel types at which times and severities) are optional when the policy names its channels explicitly, as below. 3. **Escalation policy** — **Alerting → Escalations (Eskalationen) → Create**. Name `default`; one step: after `0s`, notify **Contact** `alice`, Channels `ntfy`. Add a second step `after 15m`, **unless acked**, to a contact group or the on-call schedule with `voice`/`sms` later. Save; **Simulate (Simulieren)** shows who would be paged when. 4. **Alert rule** — **Alerting → Alert rules (Alarm-Regeln) → New rule (Regel anlegen)**. Name `critical`, source **CEL match**: ```text event.type == "state_change" && event.stateType == "hard" && (event.state == "CRITICAL" || event.state == "DOWN") ``` Severity `critical`, Escalation `default`, optional Title `{{ .event.object }} is {{ .event.state }}`. **Test rule (Regel testen)** replays the last 24 h of events and lists the alerts that would open. 5. **Try it** — **Alerts → Trigger alarm (Alarm auslösen)**: title, severity, escalation policy `default` → the step fires immediately and ntfy shows the alert. Or break the `https` service (point `-u` at a closed port): after `maxCheckAttempts` × `retryInterval` (3 × 15 s by default) the state goes hard CRITICAL, the rule opens an alert, the chain starts. **Ack** stops the chain; the **Events** page shows the `alert_opened`, `escalation` and `notification` records, and **Admin → Dead letters** collects deliveries that failed permanently. The same chain as a bundle: alarm-chain.yaml ```yaml kind: Channel metadata: { name: ntfy } spec: type: ntfy enabled: true # required — a channel without it is disabled config: { url: https://ntfy.sh, topic: northplane-7f3a9c2d } --- kind: Contact metadata: { name: alice } spec: email: alice@example.org timeZone: Europe/Vienna --- kind: EscalationPolicy metadata: { name: default } spec: steps: - after: 0s notify: { contact: alice } channels: [ntfy] - after: 15m unlessAcked: true notify: { contact: alice } channels: [email] # needs an enabled email channel --- kind: AlertRule metadata: { name: critical } spec: match: 'event.type == "state_change" && event.stateType == "hard" && (event.state == "CRITICAL" || event.state == "DOWN")' severity: critical title: "{{ .event.object }} is {{ .event.state }}" escalationPolicy: default ``` Three things that trip up first-time setups: * **Channels are selected by type, not by name.** A step or preference says `ntfy` or `email`, and the notifier uses the first *enabled* channel of that type in name order. Keep one enabled channel per type unless you know why not. * **`enabled` is not defaulted.** Channels and event sources created through the API or a bundle without `enabled: true` are disabled; the UI sets it for you. * **A step’s `channels` list overrides the contact’s preferences completely**, including their time-period and minimum-severity gating. Leave the list empty to route by preferences. Suppression (downtimes, silences, flapping, dependencies), incidents, ack paths and the full pipeline: [Alarming overview](/docs/alarming/overview/). ## Create an API token and use `np` [Section titled “Create an API token and use np”](#create-an-api-token-and-use-np) Browser sessions use a cookie; everything else — `np`, `np-agent`, scripts, MCP clients, federation edges — authenticates with an API token `np_` + 48 hex characters, sent as `Authorization: Bearer np_…`. * **Admin → API tokens (API-Tokens)**: Name plus a comma-separated list of scopes (default `objects:read,alerts:read`). The token is shown **once**. Scopes are `resource:action` permissions with wildcards: `objects:read,alerts:read` for a read-only client, `objects:write` for an agent, `config:write` for `np apply`, `*:*` for an admin automation. The table lists prefix, scopes and last use; **Revoke (Widerrufen)** deletes. Via the API: `POST /api/v1/api-tokens` also takes `roles`, `ipBind` CIDRs and `expiresAt` ([API tokens](/docs/administration/api-tokens/)). * **Headless**: `northplaned bootstrap-admin -config /etc/northplane/config.yaml` (on the server, against the same data directory) mints a token named `bootstrap-admin` with scope `*:*` and prints it once; it refuses if that token already exists. Minting any token closes the `/setup` page. Then point the CLI at the instance: ```bash export NP_SERVER=https://monitoring.example.net # default: https://localhost:8443 export NP_TOKEN=np_0123456789abcdef… np doctor # /system/info + /system/health, works without a token np get hosts # STATE NAME HOST LABELS np get problems np get alerts np describe # object JSON + effective config np apply -f web-01.yaml --dry-run np ack -m "looking into it" np oncall ``` Global flags (`--server`, `--token`, `--json`, `--insecure`) must come **before** the command. A development server speaks plain HTTP on loopback, so use `--server http://127.0.0.1:8443` there. `np -h` or `np help` prints usage (`np --help` is rejected as an unknown flag). Every command maps to one or two API calls — the table is in [CLI: np](/docs/reference/cli-np/); the raw API is browsable on the instance at `/api/docs` and documented in the [API overview](/docs/reference/api-overview/): ```bash curl -s -H "Authorization: Bearer $NP_TOKEN" "$NP_SERVER/api/v1/hosts?limit=5" ``` ## Install an agent [Section titled “Install an agent”](#install-an-agent) `np-agent` runs on the monitored host and **pushes** results to `POST /api/v1/results` every `interval` (60 s): a host heartbeat plus the services `load`, `memory`, `disk /` (one per configured mount), `processes`, `network` on Linux/macOS (`cpu` instead of `load`/`network` on Windows), and any local Nagios plugins you list under `checks:`. No inbound port on the host is needed. The server **does not create objects from agent results** — results for an unknown host or service are rejected (`unknown host …`, `unknown object …`). The Admin → Agents tab says the host “appears automatically”; it does not. Create the host and the services you want first, as passive objects with a staleness deadline so a silent agent turns them UNKNOWN: agent-web-01.yaml ```yaml kind: Host metadata: { name: web-01, labels: { agent: "true" } } spec: address: 10.0.0.10 checkCommand: passive stalenessAfter: 3m --- kind: Service metadata: { name: load, host: web-01 } spec: { checkCommand: passive, stalenessAfter: 3m } --- kind: Service metadata: { name: memory, host: web-01 } spec: { checkCommand: passive, stalenessAfter: 3m } --- kind: Service metadata: { name: "disk /", host: web-01 } spec: { checkCommand: passive, stalenessAfter: 3m } --- kind: Service metadata: { name: processes, host: web-01 } spec: { checkCommand: passive, stalenessAfter: 3m } --- kind: Service metadata: { name: network, host: web-01 } spec: { checkCommand: passive, stalenessAfter: 3m } ``` Then, on **Admin → Agents**: 1. Install the binary on the host with the tab’s one-liner (`curl … install.sh | sh`; set `NP_BINARIES=np-agent` to skip the server and CLI), or take `np-agent` from the release tarball or your source build and put it in `/usr/local/bin` (Windows: `np-agent.exe` from the zip). 2. **Create token** with the host name filled in — it mints a token with exactly the scope `objects:write` and pastes it into the `agent.yaml` shown below it. 3. Write `/etc/northplane/agent.yaml` (Windows: `C:\ProgramData\northplane\agent.yaml`): /etc/northplane/agent.yaml ```yaml server: https://monitoring.example.net token: np_… hostname: web-01 # must equal the Host object's name; default: OS hostname interval: 60s disk: ["/"] # insecure: true # only for a self-signed server certificate ``` 4. Start it with the unit snippet from the tab (`systemctl enable --now np-agent`, a launchd plist on macOS, `sc.exe create np-agent …` on Windows) or by hand: `np-agent -config /etc/northplane/agent.yaml`. The log line `np-agent: started host=web-01 …` appears, and within a minute the objects leave **PENDING**. A wrong token shows as `submit failed, buffering … err="HTTP 401"` on the agent and nothing on the server. The agent keeps up to 10 000 results in memory while the server is unreachable and replays them. Pull mode (the server hands out `agent:exec:` checks; needs `objects:read` too and a `pullAllow` list on the agent) and the NCPA-style listener mode are described in [Agent](/docs/monitoring/agent/). ## Where to go next [Section titled “Where to go next”](#where-to-go-next) * [Demo mode](/docs/getting-started/demo-mode/) — seed a complete showcase to click through. * [Hosts and services](/docs/monitoring/hosts-and-services/) and [Built-in checks](/docs/monitoring/builtin-checks/) — every field and every check flag. * [Alarming overview](/docs/alarming/overview/), then [Event sources](/docs/alarming/event-sources/), [Escalation policies](/docs/alarming/escalation-policies/), [Contacts and on-call](/docs/alarming/contacts-and-oncall/), [Voice and IVR](/docs/alarming/voice-and-ivr/). * [Users, roles and permissions](/docs/administration/users-roles-permissions/) and [Authentication](/docs/administration/authentication/) — before you invite colleagues. * [Security](/docs/administration/security/) — the hardening checklist for anything that faces a network. * [Agent chat](/docs/ai/agent-chat/) and [MCP server](/docs/ai/mcp-server/) — when you want an assistant on top of the API. # Installation > Every way to install Northplane in depth — release tarball and install.sh, northplaned init with systemd, the distroless Docker image, Docker Compose with bundled Caddy, building from source — plus the platform matrix, the PostgreSQL option, file locations, upgrading and uninstalling. Northplane is one static binary (`northplaned`) plus the `np` CLI and the `np-agent` host agent. Pick the variant that matches how you run services; they all produce the same server with the same data layout, so you can start with one and move to another later. | Variant | Best for | TLS | You get | | ----------------------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------- | ---------------------------------------------------------- | | Release tarball (+ `northplaned init`, systemd) | VMs and bare metal, Linux/macOS | your certificate pair, or a reverse proxy with `trustProxy` | `northplaned`, `np`, `np-agent` in `/usr/local/bin` | | Docker image | container hosts, Kubernetes, quick trials | your certificate pair, a proxy, or `NORTHPLANE_TLS_INSECURE=true` locally | `northplaned` + `np` in a distroless image (no `np-agent`) | | Docker Compose with bundled Caddy | a single box that should just have HTTPS | automatic (Let’s Encrypt or an internal CA) | Northplane + Caddy, optional PostgreSQL | | Build from source | development, unreleased versions, other platforms | as above | `bin/northplaned`, `bin/np`, `bin/np-agent`, `bin/np-gen` | Releases live on [GitHub](https://github.com/myfoxit/northplane/releases), the container image on GHCR (`ghcr.io/myfoxit/northplane`); both are public — no login, no token. ## Install the binaries or the image [Section titled “Install the binaries or the image”](#install-the-binaries-or-the-image) * Release tarball ### Release assets [Section titled “Release assets”](#release-assets) Releases are tagged `v*`. Each release carries: | Asset | Contents | | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- | | `northplane__linux_amd64.tar.gz`, `northplane__linux_arm64.tar.gz` | `northplaned`, `np`, `np-agent`, `LICENSE` | | `northplane__darwin_amd64.tar.gz`, `northplane__darwin_arm64.tar.gz` | `northplaned`, `np`, `np-agent`, `LICENSE` | | `northplane__windows_amd64.zip` | `np.exe`, `np-agent.exe`, `LICENSE` — **no** `northplaned` (it needs Unix process groups for plugin execution) | | `checksums.txt` | `sha256sum` lines for every asset | The asset name keeps the leading `v` of the tag (`northplane_v1.2.0_linux_amd64.tar.gz`) while the version baked into the binaries strips it (`northplaned version` → `northplaned 1.2.0`). Binaries are static (`CGO_ENABLED=0`), so they run on any glibc or musl distribution. ### `install.sh` [Section titled “install.sh”](#installsh) The one-line installer resolves the newest release through the GitHub API, downloads the matching tarball and `checksums.txt`, verifies the SHA-256 and installs `northplaned np np-agent`: ```bash curl -fsSL https://raw.githubusercontent.com/myfoxit/northplane/main/install.sh | sh ``` What it does, exactly: * Supports Linux and macOS on `x86_64`/`amd64` and `aarch64`/`arm64`; needs `curl`, `tar` and `sha256sum` or `shasum`. * Installs into `/usr/local/bin`. If that is not writable it uses `sudo` (the password prompt comes from `/dev/tty`, so this also works in the `curl … | sh` form); without `sudo` it falls back to `~/.local/bin` (created if missing, with a note if it is not on `PATH`). * Picks the newest release; if only pre-releases exist it takes the newest of those. * Is safe to re-run: existing binaries are replaced. * Writes no configuration and no service unit; it ends with the two next steps — `northplaned serve` for a loopback trial and `sudo northplaned init` + `systemctl enable --now northplaned` for a service. | Variable | Effect | | ------------------------- | ---------------------------------------------------------------------------- | | `NP_VERSION=v1.2.3` | install that release instead of the newest (`1.2.3` is accepted too) | | `NP_INSTALL_DIR=/opt/bin` | install directory (no fallback to `~/.local/bin` when set) | | `NP_BINARIES="np-agent"` | install a subset — what the **Admin → Agents** tab shows for monitored hosts | Manual equivalent (any release, any platform from the matrix): ```bash tag=v1.2.0 os=linux arch=amd64 curl -fsSLO "https://github.com/myfoxit/northplane/releases/download/${tag}/northplane_${tag}_${os}_${arch}.tar.gz" curl -fsSLO "https://github.com/myfoxit/northplane/releases/download/${tag}/checksums.txt" grep " northplane_${tag}_${os}_${arch}.tar.gz\$" checksums.txt | sha256sum -c - # macOS: shasum -a 256 -c - tar -xzf "northplane_${tag}_${os}_${arch}.tar.gz" sudo install -m 0755 northplaned np np-agent /usr/local/bin/ northplaned version ``` A first trial needs no configuration: `northplaned serve` listens on `127.0.0.1:8443` (plain HTTP is allowed on loopback) and stores data under your user’s data directory. For a permanent install continue with [Set up as a service](#set-up-as-a-service-with-northplaned-init). * Docker image ### Image facts [Section titled “Image facts”](#image-facts) | Item | Value | | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Image | `ghcr.io/myfoxit/northplane` — tags `latest`, `main-<12-char sha>` (pushed for every green CI run of `main`), and for releases `X.Y.Z` and `X.Y`; every tag is multi-arch (`linux/amd64` + `linux/arm64`) | | Base | `gcr.io/distroless/static-debian12:nonroot` — **no shell**, runs as uid/gid **65532** | | Contents | `/usr/local/bin/northplaned`, `/usr/local/bin/np` (no `np-agent`) with the UI and this documentation embedded | | Defaults | `ENV NORTHPLANE_DATA_DIR=/var/lib/northplane`, `ENV NORTHPLANE_LISTEN=:8443`, `VOLUME /var/lib/northplane`, `EXPOSE 8443`, `ENTRYPOINT ["/usr/local/bin/northplaned"]`, `CMD ["serve"]` | | Health | probe from outside: `GET /healthz` → `ok`, `GET /readyz` → JSON with subsystems (the image has no shell for a `HEALTHCHECK`) | Because the listener is bound to all interfaces, the container **refuses to start** unless one of these is set: `NORTHPLANE_TLS_CERT_FILE` + `NORTHPLANE_TLS_KEY_FILE` (PEM, readable by uid 65532), `NORTHPLANE_TRUST_PROXY=true` (a TLS-terminating proxy in front that sets `X-Forwarded-Proto`), or `NORTHPLANE_TLS_INSECURE=true` (development only). The error is `no TLS configured on a non-loopback listener — set tls.certFile/keyFile, or trustProxy behind a TLS-terminating proxy, or tls.insecure for dev`. ### Run it [Section titled “Run it”](#run-it) ```bash docker run -d --name northplane --restart unless-stopped \ -p 8443:8443 \ -v northplane-data:/var/lib/northplane \ -v /etc/northplane/certs:/certs:ro \ -e NORTHPLANE_TLS_CERT_FILE=/certs/fullchain.pem \ -e NORTHPLANE_TLS_KEY_FILE=/certs/privkey.pem \ -e NORTHPLANE_BASE_URL=https://monitoring.example.net \ ghcr.io/myfoxit/northplane:latest ``` * Any `config.yaml` key can be set through its `NORTHPLANE_*` environment variable (see [Configuration](/docs/administration/configuration/)); a file mounted at `/etc/northplane/config.yaml` is picked up automatically because that path wins whenever it exists. * A bind-mounted data directory must be writable by uid 65532 (`chown 65532:65532 /srv/northplane`). A named volume inherits the ownership from the image. * The secrets-at-rest master key defaults to `/var/lib/northplane/secret.key` inside the volume. The production stacks mount a host-side key read-only and point at it with `NORTHPLANE_SECRET_KEY_FILE=/etc/northplane/secret.key`; if that path turns out unusable the server logs `configured secretKeyFile unusable — falling back to the data directory`. Back the key up — without it encrypted secrets are unreadable ([Secrets](/docs/administration/secrets/)). * Publish additional ports only for listeners you enable: `9162/udp` (SNMP traps), `2023` (ESPA), `8123` (ESPA-X), `4573` (FastAGI) — see the ports table in the [Deployment overview](/docs/deployment/overview/). * Set `NP_DEFAULT_ADMIN_EMAIL` / `NP_DEFAULT_ADMIN_PASSWORD` to choose the break-glass admin, or `NP_DEFAULT_ADMIN_DISABLED=1` to use `/setup`; otherwise read the generated password from `docker logs northplane` ([Quickstart](/docs/getting-started/quickstart/#2-create-the-admin-account)). * To run `np` from the image: `docker exec northplane /usr/local/bin/np --server https://127.0.0.1:8443 --insecure --token np_… get hosts` (`http://` when the container runs with `NORTHPLANE_TLS_INSECURE=true`; `--insecure` only skips certificate verification for a certificate that does not match `127.0.0.1`). Build your own image from a checkout with `make docker` (tags `northplane:`, default `1.0.0-dev`) or `docker build --build-arg VERSION= -t northplane .`. The Dockerfile builds the UI (Node 22), the documentation and the Go binaries in separate stages. * Docker Compose + Caddy ### The bundled stack [Section titled “The bundled stack”](#the-bundled-stack) The repository root ships `docker-compose.yml` and `caddy/Caddyfile`: Northplane on the Compose network only, Caddy publishing 80/443 and terminating TLS. docker-compose.yml (repository root, abbreviated) ```yaml services: northplane: image: ghcr.io/myfoxit/northplane:latest # build: . # uncomment to build from source instead of the published image restart: unless-stopped environment: NORTHPLANE_LISTEN: ":8443" NORTHPLANE_TRUST_PROXY: "true" # Caddy terminates TLS and sets X-Forwarded-* NORTHPLANE_BASE_URL: "https://${DOMAIN:-localhost}" # NORTHPLANE_STORAGE_DSN: "postgres://np:np@db:5432/northplane?sslmode=disable" volumes: - northplane-data:/var/lib/northplane expose: - "8443" caddy: image: caddy:2-alpine restart: unless-stopped depends_on: [northplane] ports: ["80:80", "443:443"] environment: DOMAIN: "${DOMAIN:-localhost}" volumes: - ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro - caddy-data:/data - caddy-config:/config healthcheck: test: ["CMD", "wget", "-qO-", "http://northplane:8443/healthz"] interval: 30s timeout: 5s retries: 3 volumes: northplane-data: caddy-data: caddy-config: ``` caddy/Caddyfile ```text # DOMAIN=localhost (default) → Caddy issues an internal self-signed cert. # DOMAIN=monitoring.example.net (public DNS → this host) → automatic Let's Encrypt. {$DOMAIN:localhost} { reverse_proxy northplane:8443 } ``` 1. Get the two files (clone the repository or copy them) and log in to GHCR — or uncomment `build: .` to build the image from the checkout. 2. Local trial: `docker compose up -d` → `https://localhost` with Caddy’s internal CA (accept the browser warning once). Production: `DOMAIN=monitoring.example.net docker compose up -d` with an A record pointing at the host and ports 80/443 reachable — Caddy obtains and renews a Let’s Encrypt certificate. `NORTHPLANE_BASE_URL` follows `DOMAIN` automatically. 3. First login: the stack sets `NP_DEFAULT_ADMIN_DISABLED: "1"`, so `/setup` is open — create the admin there. For unattended installs replace that line with `NP_DEFAULT_ADMIN_EMAIL` / `NP_DEFAULT_ADMIN_PASSWORD` before the first start (or drop it to get a seeded `admin@localhost` whose generated password appears once in `docker compose logs northplane`). 4. Watch it: `docker compose ps`, `docker compose logs -f northplane`, `docker compose logs -f caddy` (ACME activity). Caddy’s healthcheck probes `http://northplane:8443/healthz` every 30 s because the distroless Northplane container cannot probe itself. Optional PostgreSQL: uncomment the `db` service (`postgres:16`, volume `pg-data`), set `NORTHPLANE_STORAGE_DSN` and add `db` to `depends_on` — see [PostgreSQL](#postgresql-instead-of-sqlite). The `deploy/` directory holds the CI-managed production variants of the same idea: `deploy/docker-compose.yml` + `deploy/Caddyfile` (bundled Caddy with `DOMAIN`, a bare-IP `https://{$SERVER_IP}` site with an internal certificate, `ACME_EMAIL`, a host-side `secret.key` bind mount, separate data directories for demo and real mode) and `deploy/docker-compose.vm.yml` (no Caddy — an external proxy terminates TLS). They are documented in [Docker Compose deployment](/docs/deployment/docker-compose/) and [Proxmox VM](/docs/deployment/proxmox-vm/). ## Set up as a service with `northplaned init` [Section titled “Set up as a service with northplaned init”](#set-up-as-a-service-with-northplaned-init) `northplaned init` turns a bare binary into a permanent install. Run it as root on the target host: ```bash sudo northplaned init # --dir /etc/northplane --data /var/lib/northplane --user northplane sudo systemctl enable --now northplaned ``` | Flag | Default | Meaning | | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | | `--dir` | `/etc/northplane` as root; `~/.config/northplane` (macOS: `~/Library/Application Support/northplane`) otherwise | configuration directory | | `--data` | `/var/lib/northplane` as root; `~/.local/share/northplane` / `$XDG_DATA_HOME/northplane` (macOS: `~/Library/Application Support/northplane`) otherwise | data directory written into the config | | `--user` | `northplane` | system account the service runs as (created when missing — root on Linux only) | It creates both directories (0750) and writes three files; it **refuses to overwrite** an existing `config.yaml` (` exists — refusing to overwrite`): | File | Mode | Content | | --------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `/config.yaml` | 0640 | the commented bootstrap template: `listen: "127.0.0.1:8443"`, `dataDir`, `secretKeyFile`, empty `storage.dsn` (= SQLite), empty `tls`, commented `oidc`/`ldap`/`federation` blocks, `ai.provider: none`, `backup.target: ""`. Shown verbatim in [Configuration](/docs/administration/configuration/). | | `/secret.key` | 0600 | 32 random bytes as 64 hex characters — the AES-256-GCM master key for secrets at rest. **Back it up.** | | `northplaned.service` | 0644 | the systemd unit below — written straight to `/etc/systemd/system/` when `init` runs as root on a Linux host with systemd, otherwise next to the config for manual use | As root on Linux, `init` additionally creates the locked system user (`useradd --system --no-create-home --shell nologin`), hands it the configuration directory, `config.yaml`, `secret.key` and the data directory, and installs the unit — so the printed next step is literally `systemctl enable --now northplaned`. On other systems (or without `useradd`) it prints what is left to do by hand. /etc/systemd/system/northplaned.service (generated) ```ini [Unit] Description=Northplane monitoring server Documentation=https://github.com/myfoxit/northplane After=network-online.target Wants=network-online.target [Service] ExecStart=/usr/local/bin/northplaned serve -config /etc/northplane/config.yaml Restart=on-failure RestartSec=2 User=northplane Group=northplane StateDirectory=northplane NoNewPrivileges=yes ProtectSystem=strict ProtectHome=yes PrivateTmp=yes ReadWritePaths=/var/lib/northplane [Install] WantedBy=multi-user.target ``` `ExecStart` uses the path of the binary that ran `init`. There is deliberately no `WatchdogSec`: `northplaned` does not speak `sd_notify`, and a watchdog without keep-alives would restart the service every interval. The command ends with: ```text next steps: 1. review /etc/northplane/config.yaml (listen/TLS, storage backend, OIDC) 2. systemctl enable --now northplaned 3. open http://127.0.0.1:8443/setup in the browser to create the admin account (or headless: northplaned bootstrap-admin -config /etc/northplane/config.yaml) ``` A complete first start on a systemd host: 1. Install the binaries (installer, tarball or source build) and run `sudo northplaned init`. 2. Review `/etc/northplane/config.yaml`. To serve the network set `listen: ":8443"` **and** either `tls.certFile`/`tls.keyFile` or `trustProxy: true` behind a TLS-terminating proxy; the loopback default exists so that plaintext is never exposed by accident. Set `baseUrl` to the public URL (used in notification links, ack links and OIDC redirects). Keys and defaults: [Configuration](/docs/administration/configuration/), TLS options: [TLS and proxy](/docs/administration/tls-and-proxy/). 3. Start it and watch the log: ```bash sudo systemctl enable --now northplaned journalctl -u northplaned -f ``` Environment variables such as `NP_DEFAULT_ADMIN_DISABLED=1` or `NP_DEFAULT_ADMIN_PASSWORD=…` go into a drop-in (`sudo systemctl edit northplaned` → `[Service]` / `Environment=…`). 4. Create the admin: open `/setup` (only open while no local user and no API token exist and the default-admin seeding is disabled — see [Quickstart](/docs/getting-started/quickstart/#2-create-the-admin-account)), or run `sudo -u northplane northplaned bootstrap-admin -config /etc/northplane/config.yaml` for a headless `*:*` token. If `secret.key` is not readable by the service user (for example after moving files by hand), the server warns `configured secretKeyFile unusable — falling back to the data directory` and generates a second key under `/var/lib/northplane/secret.key` — workable, but then the key in `/etc/northplane` is not the one in use. No SIGHUP reload exists: configuration changes need `systemctl restart northplaned`. Shutdown is graceful (SIGTERM, 30 s budget for in-flight requests and workers). ## Build from source [Section titled “Build from source”](#build-from-source) Prerequisites: **Go 1.25** and **Node.js 22** (npm). The Go build is CGO-free (pure-Go SQLite), so no C toolchain is needed. ```bash git clone https://github.com/myfoxit/northplane.git && cd northplane make all # = make web (UI) + make docs (this manual) + make build ./bin/northplaned version ``` | Target | What it does | | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `make web` | `npm ci` + Vite build in `web/`, copies `web/dist` to `internal/web/dist` (embedded via `go:embed`) | | `make docs` | `npm ci` + Astro/Starlight build in `docs/` (fails on broken links), stages `docs/dist` into `internal/docs/dist` | | `make build` | `go build -ldflags "-X main.version=$(VERSION)"` → `bin/northplaned`, `bin/np`, `bin/np-agent`, `bin/np-gen`; `VERSION` defaults to `1.0.0-dev` | | `make docker` | builds the container image `northplane:$(VERSION)` | | `make test` / `make race` | `go vet` + `go test` (CI runs the race detector) | | `make dev` | hot-reload development loop: Vite on `:5173`, auto-rebuilt backend on `127.0.0.1:8443`, demo data seeded (`NP_DEV_DEMO=0` to skip) | A plain `go build ./cmd/northplaned` also works, but it embeds whatever `internal/web/dist` is committed (which may be stale) and no documentation — `/docs/` then answers `501 documentation not embedded in this build — run make docs`. Release-style static cross builds: ```bash CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -trimpath \ -ldflags "-s -w -X main.version=1.2.0" -o northplaned ./cmd/northplaned ``` Cross-compiling `northplaned` for Windows is not supported (CI skips it); `np` and `np-agent` build for `windows/amd64`. The development workflow (worktrees, lint, tests, e2e) is described in [Development setup](/docs/development/setup/). ## Platform matrix [Section titled “Platform matrix”](#platform-matrix) | | Linux amd64 / arm64 | macOS amd64 / arm64 | Windows amd64 | | --------------- | ------------------------------------------------------------------------------ | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `northplaned` | yes — tarball, container image, source | yes — tarball, source | **no** (not built: plugin execution needs Unix process groups) | | `np` | yes | yes | yes (`np.exe` in the release zip) | | `np-agent` | yes — load, memory, disk, processes, network, plugins | yes — same as Linux | yes (`np-agent.exe`) — memory, disk, CPU, processes, plugins; no load or network collectors; runs as a console process under `sc.exe` | | `install.sh` | yes | yes | no — use the release zip | | Service manager | systemd unit from `northplaned init`; agent unit snippet in **Admin → Agents** | launchd plist snippet for the agent in **Admin → Agents** | `sc.exe create` snippet for the agent | | Container | `linux/amd64`, `linux/arm64` | via Docker Desktop (Linux image) | via Docker Desktop (Linux image) | `builtin:icmp` uses unprivileged datagram ICMP first and falls back to a raw socket; on Linux without root or `cap_net_raw` it reports `UNKNOWN - icmp socket: …`. Nagios plugins for `exec:` checks are looked up under `pluginsDir`, auto-detected from `/usr/lib/nagios/plugins`, `/usr/lib64/nagios/plugins`, `/usr/local/libexec/nagios`, `/opt/homebrew/libexec`, then `/plugins`. ## PostgreSQL instead of SQLite [Section titled “PostgreSQL instead of SQLite”](#postgresql-instead-of-sqlite) SQLite (default, `storage.dsn: ""`) is the fully supported and CI-green backend and needs nothing else. PostgreSQL is selected by a DSN: config.yaml ```yaml storage: dsn: "postgres://np:secret@db:5432/northplane?sslmode=require" eventRetentionMonths: 12 ``` or `NORTHPLANE_STORAGE_DSN=postgres://…`. Facts to know before you choose it: * Driver `pgx`; schema migrations run automatically on every start (also by `northplaned migrate`). Pool: 16 open / 8 idle connections. Events are stored in monthly partitions; the `janitor` enforces `eventRetentionMonths` nightly. * The **NP-TSDB stays on local disk** under `/tsdb` regardless of the relational backend — the data directory (and its `secret.key`) is still required and still needs backups. * `northplaned backup` does not dump PostgreSQL; the manifest records the schema version and a note that relational backup is the operator’s job (`pg_dump`/PITR). The TSDB is still copied. * Known caveat: the audit-log chain verification (`POST /api/v1/audit:verify`, `np audit verify`) fails on PostgreSQL because `jsonb` normalises the stored JSON that the row hash was computed over. The CI job for PostgreSQL is non-blocking for this reason. * Moving an existing SQLite install: stop the server, run `northplaned storage migrate --to "postgres://…" -config /etc/northplane/config.yaml`, point `storage.dsn` at the target, start again (offline copy; the TSDB is untouched). The Compose file has a commented `postgres:16` service ready to uncomment. Details, table layout and sizing: [Storage](/docs/administration/storage/). ## Where files live [Section titled “Where files live”](#where-files-live) | What | Location | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Config file | `-config `; default `/etc/northplane/config.yaml` as root; for other users `/etc/northplane/config.yaml` **if it exists**, else `~/.config/northplane/config.yaml` (Linux) / `~/Library/Application Support/northplane/config.yaml` (macOS). A missing file is fine — defaults and `NORTHPLANE_*` variables apply. | | Data directory (`dataDir`) | `/var/lib/northplane` as root; `$XDG_DATA_HOME/northplane` or `~/.local/share/northplane` (Linux); `~/Library/Application Support/northplane` (macOS); `/var/lib/northplane` in the container. Holds `core.db` (+ `-wal`/`-shm`), `events-YYYYMM.db` segments, `tsdb/`, `artifacts/`, optionally `plugins/` and the fallback `secret.key`. | | Secret key | `secretKeyFile` from the config (`northplaned init` writes `/secret.key`); fallback `/secret.key`, generated on first start. | | Binaries | `/usr/local/bin/{northplaned,np,np-agent}` (tarball/installer), `bin/` (source build), `/usr/local/bin/{northplaned,np}` in the image. | | Agent config | `/etc/northplane/agent.yaml` (root or if it exists), else `~/.config/northplane/agent.yaml`; Windows `C:\ProgramData\northplane\agent.yaml` — see [Agent](/docs/monitoring/agent/). | ## Upgrading [Section titled “Upgrading”](#upgrading) Upgrades are in-place: replace the binary (or pull the new image tag) and restart; pending schema migrations are applied automatically on start and the embedded UI and docs are always the matching version. Back up `secret.key` and the data directory (or run `northplaned backup`) first, and read [Upgrades](/docs/administration/upgrades/) for rollback notes per variant. ## Uninstalling [Section titled “Uninstalling”](#uninstalling) There is no uninstall script. Remove what the variant created: ```bash # systemd install sudo systemctl disable --now northplaned sudo rm /etc/systemd/system/northplaned.service && sudo systemctl daemon-reload sudo rm /usr/local/bin/northplaned /usr/local/bin/np /usr/local/bin/np-agent sudo rm -r /etc/northplane /var/lib/northplane # config, secret.key, database, TSDB sudo userdel northplane # docker run docker rm -f northplane && docker volume rm northplane-data # docker compose (also removes the Caddy volumes) docker compose down -v ``` Agents are removed on their hosts the same way (`systemctl disable --now np-agent`, `/etc/northplane/agent.yaml`, the binary); revoke their API tokens under **Admin → API tokens**. # What is Northplane? > Northplane is a single-binary monitoring and alarm server — checks, SNMP, agents, escalation, voice/SMS/push, an AI agent and an MCP server — with the UI, the API and the documentation embedded in one static executable. Northplane is a monitoring **and** alarming server in one static binary. It polls hosts and services (built-in checks, Nagios plugins, SNMP, an optional host agent), turns state changes and external events into alerts, escalates those alerts to the people on call over phone, SMS, push, e-mail, chat and ticket systems, and records everything in an append-only event log with a hash-chained audit trail. Everything is driven through one REST API; the web UI, the `np` CLI, the AI agent chat and the MCP server are all clients of that API and share the same roles and permissions. ![The Overview page of a demo instance: KPI tiles, problems, service status, open incidents and who is on call](/docs/_astro/overview.BG0ZV0u2_Zc8WIg.webp) The server binary is called `northplaned`. It ships with the React UI, the Swagger UI and this documentation embedded, uses SQLite and its own time-series store (NP-TSDB) by default, and needs no external services to run. PostgreSQL, a TLS-terminating proxy, OIDC/LDAP and AI providers are optional additions, not prerequisites. ## Who it is for [Section titled “Who it is for”](#who-it-is-for) * **Operators and sysadmins** who want Nagios-style monitoring (active checks, plugins, SNMP, dependencies, soft/hard states, downtimes) without an external database, web server or message broker to look after. * **On-call engineers** who need reliable alarming: escalation policies, on-call schedules, voice calls with IVR, SMS, push to the Northplane alarm app, acknowledgement from the phone, and an outbox with retries and dead letters so a notification is never silently lost. * **Integrators and developers** who want an API-first system: OpenAPI 3.1 spec, RFC 9457 errors, declarative YAML config bundles, webhooks in and out, a typed CLI, MCP for AI assistants. * **Control rooms and factories**: inbound alarms over ESPA 4.4.4, ESPA-X, MQTT, IMAP and Asterisk/FastAGI; outbound MQTT; wallboards; business-service trees with SLAs. Northplane assumes Linux fluency but no prior knowledge of the product — this section takes you from zero to a monitored host with a working alarm chain. ## The one-binary idea [Section titled “The one-binary idea”](#the-one-binary-idea) `northplaned` contains the scheduler, the check executor, the result pipeline and state machine, the alerting engine, the escalation engine, the notifier with its outbox, every inbound listener (SNMP traps, IMAP, MQTT, ESPA, FastAGI), the report scheduler, the MCP server, the UI and the docs. A fresh install looks like this: ```bash northplaned serve # northplane: listening addr=127.0.0.1:8443 scheme=http storage=sqlite objects=0 ai=false ``` Configuration is deliberately minimal: `config.yaml` (or `NORTHPLANE_*` environment variables) holds only what must exist before the API is reachable — listen address, data directory, TLS, storage DSN, OIDC/LDAP, federation. Every other object — hosts, services, templates, channels, rules, policies, schedules, dashboards — is managed through the API, the UI or YAML bundles, and can be exported again as a bundle. Three more binaries come with it: | Binary | Role | | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `np` | CLI — a thin client of the public API (`np get hosts`, `np apply -f bundle.yaml`, `np ack …`). | | `np-agent` | Host agent for Linux, macOS and Windows: pushes load/memory/disk/process/network results and local plugin output over HTTPS (no inbound ports), optionally pulls checks from the server or listens NCPA-style. | | `np-gen` | Developer scaffolding for new resource kinds; not needed to run Northplane. | ## At a glance [Section titled “At a glance”](#at-a-glance) | Aspect | What you get | | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Server | `northplaned`, static Go binary (CGO-free), Linux/macOS amd64+arm64; also a distroless container image. Default listen `127.0.0.1:8443`; plaintext is refused on non-loopback listeners unless TLS is configured, a trusted proxy terminates TLS, or `tls.insecure` is set for development. | | UI | Embedded React SPA at `/`; German or English from the browser language; 31 colour themes; wallboard mode; command palette (Ctrl/⌘ K). | | Storage | SQLite by default (`core.db` in the data directory, WAL mode, monthly event segments) or PostgreSQL via `storage.dsn`. Schema migrations run automatically. | | Metrics | NP-TSDB, embedded under `/tsdb`: perfdata of every check, raw samples 30 days, 5-minute aggregates 400 days, 1-hour aggregates 5 years. | | Checks | 17 built-in checks (ICMP, TCP, HTTP/HTTPS, TLS certificate, DNS, SMTP, IMAP, NTP, SSH banner, SNMP get/walk, NRPE, agent, HTTP flow …), any Nagios plugin via `exec:`, passive results, `np-agent`, SNMP traps, heartbeats, network discovery. | | Alarming | Event sources (webhook, Alertmanager, e-mail, SNMP trap, MQTT, ESPA/ESPA-X, Twilio voice and SMS, Asterisk), CEL alert rules, incidents, escalation policies, on-call schedules, contacts, IVR menus, 13 channel types (e-mail, SMS, voice, push, ntfy, Slack, Teams, webhook, MQTT, ServiceNow, Zendesk, Jira, generic ticket), outbox with retries and dead letters. | | API | REST under `/api/v1`, OpenAPI 3.1 at `/api/openapi.json`, Swagger UI at `/api/docs`, RFC 9457 problem details, ETag/If-Match versioning, SSE event stream, NDJSON exports, YAML config bundles. | | CLI | `np` (uses `NP_SERVER` / `NP_TOKEN`). | | Agent | `np-agent` (push, pull and listener modes; token-authenticated HTTPS). | | AI | Agent chat page (`/agent`) and sidebar with 10 provider types (Anthropic, OpenAI, Google, xAI, Mistral, DeepSeek, Groq, OpenRouter, Ollama, OpenAI-compatible), tool policy with approvals, incident summaries. | | MCP | Streamable HTTP at `/mcp` and stdio via `northplaned mcp`; 22 tools, 3 prompts; same RBAC as the API. | | Identity | Local users (argon2id), OIDC (code + PKCE), LDAP/AD sync, API tokens (`np_…`), roles and permissions, tenants, federation sites (edge instances pulling config from a main instance). | | Operations | `/healthz`, `/readyz`, `/metrics` (OpenMetrics self-metrics), structured JSON logs, hash-chained audit log, `northplaned backup`, dead-man URL. | | Licence | MIT. | ## Feature tour [Section titled “Feature tour”](#feature-tour) ### Monitoring [Section titled “Monitoring”](#monitoring) * **Objects** — hosts and services with folders, labels and label selectors; templates with multi-inheritance and an “effective config” view; UUIDv7 ids; optimistic locking with `If-Match`. * **Checks** — `builtin:` in-process checks, `exec:` for Nagios/Monitoring plugins with full perfdata parsing, `agent:exec:` executed by `np-agent`, `passive` for results pushed through the API, named check commands with `$ARGn$` and custom-variable macros. * **State machine** — interval/retry scheduling with deterministic splay, soft and hard states, host UP/DOWN/UNREACHABLE with parent reachability, flapping detection, freshness/staleness for passive objects, acknowledgements, check-now. * **SNMP** — `snmp` and `snmp-walk` checks (v1/v2c/v3) plus an SNMP trap receiver that turns traps into events and alerts. * **Nagios compatibility** — `northplaned import nagios` converts an existing Nagios/Icinga configuration into a bundle with a deviation report; NRPE client built in. * **Heartbeats, discovery, maintenance** — dead-man inputs with grace periods, CIDR scans with suggestions, downtimes (fixed, flexible, recurring via RRULE), silences, time periods. * **Metrics, dashboards, business services, reports** — NP-TSDB charts on every object, dashboards with 11 widget types and a wallboard mode, BPI trees with worst/best/quorum/weighted rules and SLA budgets, scheduled availability/SLA/alert/on-call/audit reports delivered by e-mail. ### Alarming [Section titled “Alarming”](#alarming) * **Inputs** — event sources for webhooks, Prometheus Alertmanager, e-mail (IMAP), SNMP traps, MQTT, ESPA 4.4.4 and ESPA-X, inbound Twilio voice and SMS, Asterisk FastAGI; manual alarms from the UI, the API, the alarm app or an IVR menu. * **Rules** — CEL expressions over the event (`event.type`, `event.state`, `event.labels.*`, `event.payload.*`), Go templates for titles and dedup keys, pending-for, auto-close, label injection (`np.sound`, `np.volume` for the alarm app), heartbeat rules. * **Escalation** — policies with timed steps, “unless acked”, repeats, on-call schedules with layers and overrides (and a backup person), contact groups, ticket and webhook actions; timers are persisted and survive restarts. * **Outputs** — e-mail (SMTP/sendmail/Resend/SES), SMS and voice (Twilio, Asterisk AMI, generic HTTP gateways) with DTMF acknowledgement, push (Web Push, FCM, APNs) for the Northplane alarm app, ntfy, Slack, Teams, webhooks with HMAC signatures, MQTT, ServiceNow/Zendesk/Jira/generic tickets with auto-close. * **Acknowledge from anywhere** — UI, `np ack`, API, signed ack links, SMS keyword, IVR digit, DTMF during a call, the app. Snooze re-arms the chain later. * **Reliability** — outbox with exponential backoff, dead-letter queue with replay, supervised workers, suppression by downtime/silence/flapping/dependencies with re-arm, every delivery attempt recorded as an event. ### Platform [Section titled “Platform”](#platform) * **API-first** — every capability is a documented endpoint; the UI never does anything the API cannot. Tenants (`X-Northplane-Tenant`), roles with `resource:action` permissions, API tokens with scopes, expiry and IP binding, secrets at rest (AES-256-GCM) referenced as `$SECRET:name$`. * **Config as code** — multi-document YAML bundles with plan/apply/export/prune, applied through the API, `np apply` or the Admin UI; the same mechanism distributes configuration to federated edge sites. * **Deployment** — one binary with systemd, a distroless container, or a Compose stack with a bundled Caddy for automatic TLS; SQLite or PostgreSQL; backups with `northplaned backup`. * **Observability of the monitor itself** — health and readiness endpoints, OpenMetrics, structured logs, audit chain verification, outbound dead-man pings. ### AI and API [Section titled “AI and API”](#ai-and-api) * **Agent chat** — a chat workspace with tool use against the live instance, per-user or shared provider connections, approval flow for mutating tools, budget and redaction settings. * **MCP server** — connect Claude Code, Claude Desktop, Cursor, VS Code, Windsurf, Codex or Gemini CLI to your instance over HTTP or stdio with a scoped token; tools respect the token’s RBAC. * **Typed clients** — the UI’s TypeScript types are generated from the OpenAPI spec (`openapi-typescript`); you can do the same for your own integrations. ## How the documentation is organised [Section titled “How the documentation is organised”](#how-the-documentation-is-organised) | Section | Read it when you want to … | | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Getting started** (this section) | install Northplane, log in, add the first objects, try the demo. | | **Concepts** | understand the architecture, the object model, checks and states, events, alerts and incidents, tenancy and federation. | | **Monitoring** | configure hosts and services, built-in checks, plugins, the agent, SNMP, discovery, heartbeats, metrics, dashboards, business services, reports, maintenance and templates. | | **Alarming** | build the alarm pipeline: event sources, rules, channels, voice/IVR, mobile push, contacts and on-call, escalation, acknowledgement, reliability, outgoing webhooks. | | **AI & MCP** | use the agent chat and connect MCP clients. | | **User interface** | find your way around every page, dialog and Admin tab. | | **Administration** | configure the server, authentication, users and roles, tenants, tokens, secrets, TLS, storage, bundles, branding, observability, upgrades and security. | | **Deployment** | choose and run a deployment variant, Compose, Proxmox, CI/CD, provisioning, operations, environments. | | **Reference** | look up every subcommand of `northplaned`, `np`, `np-agent`, `np-gen`, the API conventions and the generated REST reference. | | **Development** | build, test and extend Northplane, and edit these docs. | | **Project** | see the roadmap and known issues. | Every running instance serves this manual at `/docs/` and the interactive API reference at `/api/docs`, so the documentation always matches the version you run. ## Where to go next [Section titled “Where to go next”](#where-to-go-next) [Quickstart](/docs/getting-started/quickstart/)Docker, Compose or a single binary — running with a monitored host in five minutes. [Installation](/docs/getting-started/installation/)Every install variant in depth: tarball, systemd, Docker, Compose with Caddy, build from source. [First steps](/docs/getting-started/first-steps/)The UI in ten minutes, your first bundle, a minimal alarm chain, an API token, an agent. [Demo mode](/docs/getting-started/demo-mode/)Seed a complete showcase environment with one flag — and keep it away from real data. [Architecture](/docs/concepts/architecture/)Components, request path, workers, storage and the event bus. [Alarming overview](/docs/alarming/overview/)The pipeline from inputs to phone calls, with worked examples. [Configuration reference](/docs/administration/configuration/)Every config.yaml key, environment variable and default. [API overview](/docs/reference/api-overview/)Conventions, authentication, errors, pagination, SSE, curl examples. # Quickstart > Run Northplane with Docker, Docker Compose or the single binary, create the admin account, monitor your first host and HTTPS service, and send a test notification — in about five minutes. This page gets you from nothing to a running instance with one monitored host, one HTTPS service and a working notification channel. It deliberately takes shortcuts (plain HTTP on your own machine, a public ntfy topic); [Installation](/docs/getting-started/installation/) covers the production variants and [First steps](/docs/getting-started/first-steps/) continues with templates, the alarm chain, API tokens and the agent. You need one of: Docker, Docker Compose, or a `northplaned` binary for Linux/macOS (amd64/arm64). The one-line installer fetches the newest release tarball; the container image is `ghcr.io/myfoxit/northplane` — both public, no login needed. Building from source (`make all`) is the third option ([Installation](/docs/getting-started/installation/#build-from-source)). ## 1. Start the server [Section titled “1. Start the server”](#1-start-the-server) * Docker The image runs `northplaned serve` as the distroless `nonroot` user (uid 65532) with `NORTHPLANE_LISTEN=:8443` and `NORTHPLANE_DATA_DIR=/var/lib/northplane`. Because `:8443` is a non-loopback listener, the server **refuses to start** without TLS unless you explicitly allow plaintext for a local trial: ```bash docker run -d --name northplane \ -p 8443:8443 \ -v northplane-data:/var/lib/northplane \ -e NORTHPLANE_TLS_INSECURE=true \ -e NP_DEFAULT_ADMIN_DISABLED=1 \ ghcr.io/myfoxit/northplane:latest docker logs -f northplane ``` Open ****. The log shows `northplane: listening addr=:8443 scheme=http storage=sqlite` followed by `first run: open http://127.0.0.1:8443/setup to create your admin account`. * `NORTHPLANE_TLS_INSECURE=true` allows plain HTTP on the non-loopback listener. Without it the container exits with `no TLS configured on a non-loopback listener — set tls.certFile/keyFile, or trustProxy behind a TLS-terminating proxy, or tls.insecure for dev`. Never publish such a port beyond your machine — use the Compose stack or real certificates instead. * `NP_DEFAULT_ADMIN_DISABLED=1` keeps the interactive `/setup` page open (see [step 2](#2-create-the-admin-account) for why). * The named volume `northplane-data` holds the SQLite database, the event segments, the NP-TSDB and the auto-generated `secret.key`. A bind mount must be writable by uid 65532. * Docker Compose The repository root ships a `docker-compose.yml` with Northplane behind a bundled **Caddy** that terminates TLS, plus `caddy/Caddyfile`. Northplane itself runs with `NORTHPLANE_TRUST_PROXY=true` on the Compose network only; Caddy publishes 80/443. ```bash git clone https://github.com/myfoxit/northplane.git && cd northplane docker compose up -d docker compose logs -f northplane ``` Open ****. With `DOMAIN` unset, Caddy issues an internal self-signed certificate (your browser warns once); with `DOMAIN=monitoring.example.net docker compose up -d` and public DNS pointing at the host, Caddy fetches a Let’s Encrypt certificate automatically. The Compose file sets `NP_DEFAULT_ADMIN_DISABLED: "1"`, so the interactive **`/setup`** page is open on first start — create your admin account there. For unattended installs replace that line with a chosen `NP_DEFAULT_ADMIN_EMAIL` / `NP_DEFAULT_ADMIN_PASSWORD` pair **before** the first `up` (or remove it to get a seeded `admin@localhost` with a generated password in the logs). * Single binary Install the binaries with the one-line installer (or unpack the release tarball — it contains `northplaned`, `np`, `np-agent` and `LICENSE`) and start the server. No config file is needed for a trial: the defaults listen on the loopback interface in plain HTTP and put all data under your user’s data directory. ```bash curl -fsSL https://raw.githubusercontent.com/myfoxit/northplane/main/install.sh | sh NP_DEFAULT_ADMIN_DISABLED=1 NORTHPLANE_LOG_FORMAT=text northplaned serve ``` ```text northplane: listening addr=127.0.0.1:8443 scheme=http storage=sqlite objects=0 ai=false first run: open http://127.0.0.1:8443/setup to create your admin account ``` Open ****. * Default listen address is `127.0.0.1:8443`. Plaintext is allowed there because it is loopback; to serve the network you need `listen: ":8443"` **and** `tls.certFile`/`tls.keyFile` (or a TLS-terminating proxy with `trustProxy: true`) — see [TLS and proxy](/docs/administration/tls-and-proxy/). * Data directory: `/var/lib/northplane` as root, `~/.local/share/northplane` (or `$XDG_DATA_HOME/northplane`) as a normal Linux user, `~/Library/Application Support/northplane` on macOS. A `secret.key` for secrets-at-rest is generated there on first start. * Logs go to **stderr**, JSON by default; `NORTHPLANE_LOG_FORMAT=text` makes them readable. * The macOS/Linux tarballs are the only ones with `northplaned`; the Windows zip contains `np` and `np-agent` only. ## 2. Create the admin account [Section titled “2. Create the admin account”](#2-create-the-admin-account) There are two ways to get the first administrator, and which one you get depends on one environment variable: * **Interactive `/setup`** — the page is open only while the instance has **no local user and no API token**. On every start, `northplaned serve` also runs the *default-admin seeding*: unless `NP_DEFAULT_ADMIN_DISABLED` is set to any non-empty value (or `NP_DEFAULT_ADMIN_PASSWORD` is set to an empty string), it creates a local admin `admin@localhost` when no enabled local admin exists. That local user closes `/setup` before you ever see it — which is why the commands above set `NP_DEFAULT_ADMIN_DISABLED=1`. Fill in name, e-mail, a password of at least 12 characters and the confirmation; you are logged in as `admin` immediately. * **Seeded break-glass admin** — leave the seeding enabled and read the one-time log line `seeded default admin with a GENERATED password — save it now, it is not recoverable` (fields `email=admin@localhost`, `password=<32 hex chars>`), or choose your own credentials with `NP_DEFAULT_ADMIN_EMAIL`, `NP_DEFAULT_ADMIN_PASSWORD` and optionally `NP_DEFAULT_ADMIN_NAME`. Then log in at `/login`. The login page is German: **E-Mail**, **Passwort**, **Anmelden**. Headless alternative: `northplaned bootstrap-admin -config ` mints an API token with scope `*:*` (printed once) — creating any token also closes `/setup`. Details: [Authentication](/docs/administration/authentication/). ## 3. Add a host and an HTTPS service [Section titled “3. Add a host and an HTTPS service”](#3-add-a-host-and-an-https-service) 1. In the sidebar open **Objects (Objekte)** and click **New host (Host anlegen)**. On the **Basics (Basis)** tab enter Name `example-web` and Address `example.org`. 2. Switch to the **Check (Prüfung)** tab. A new object starts as `passive` (no active check), so set the check command kind to `builtin` and type `icmp` in the builtin-check field. Leave the interval and retry settings at their defaults (60 s, 15 s, 3 attempts, 30 s timeout) and **Save (Speichern)**. 3. Click **New service (Service anlegen)**: Name `https`, Host `example-web`. On the **Check** tab choose `builtin` / `http` and add the arguments one per entry: `-u`, `https://example.org/`, `-w`, `1`, `-c`, `3`. The built-in `http`/`https` check only uses TLS when `-S` is given or `-u` is a full `https://` URL, so pass the full URL. Save. 4. Both rows show **PENDING (AUSSTEHEND)** until the first result. The scheduler runs a new object within one interval; hover a row and click **Check now (Jetzt prüfen)** to force it. The host turns **UP** and the service **OK** with an output like `HTTP OK - 200 OK https://example.org/ in 0.123s, 1234 bytes, cert expires in 80d`. 5. Click the service row: the detail page shows state, last/next check, perfdata meters (`time`, `size`, `cert_days`) and, after a few results, a chart from the NP-TSDB. The **Configuration (Konfiguration)** tab shows the effective spec with every default resolved. ICMP needs privileges on Linux As an unprivileged Linux user `builtin:icmp` may report `UNKNOWN - icmp socket: … (unprivileged ICMP unavailable — run as root, grant cap_net_raw, or use builtin:tcp)`. Either grant the capability (`sudo setcap cap_net_raw+ep /usr/local/bin/northplaned`) or switch the host check to `builtin:tcp` with arguments `-p`, `443`. The container and a root-run binary are fine. The same two objects as a YAML bundle, for `np apply` or **Admin → Config bundles**: quickstart.yaml ```yaml kind: Host metadata: name: example-web spec: address: example.org checkCommand: builtin:icmp --- kind: Service metadata: name: https host: example-web spec: checkCommand: builtin:http args: ["-u", "https://example.org/", "-w", "1", "-c", "3"] ``` ## 4. Send a test notification [Section titled “4. Send a test notification”](#4-send-a-test-notification) 1. Open **Admin → Channels (Kanäle)** and click **Create (Anlegen)**. Choose Type `ntfy`, Name `ntfy`, keep **Enabled (Aktiv)** on, set Server URL `https://ntfy.sh` and a Topic nobody will guess, e.g. `northplane-7f3a9c2d`. Save. 2. In the channel row click **Send test (Test senden)**. The server posts a synthetic `info` alert titled `Test notification from Northplane ()` to the topic and the row shows `✓ sent`; a failure shows the transport error instead. 3. Open `https://ntfy.sh/northplane-7f3a9c2d` in another tab (or the ntfy app) — the message is there. ntfy.sh topics are public, so treat the topic name as a secret or run your own ntfy server. Any channel type can be tested the same way; for types that deliver to a *contact target* (e-mail, SMS, voice, push) the UI button sends without a target, so use the API with one: ```bash curl -X POST http://127.0.0.1:8443/api/v1/channels/ntfy:test-notification \ -H "Authorization: Bearer $NP_TOKEN" \ -H 'Content-Type: application/json' \ -d '{"target": "you@example.com"}' ``` (`$NP_TOKEN` is an API token from **Admin → API tokens** or `northplaned bootstrap-admin`; the endpoint needs `config:write`.) Channel reference: [Channels](/docs/alarming/channels/). ## 5. Where to go next [Section titled “5. Where to go next”](#5-where-to-go-next) * [First steps](/docs/getting-started/first-steps/) — the UI tour, templates, a complete channel → contact → escalation policy → rule chain, API tokens and `np`, installing `np-agent`. * [Demo mode](/docs/getting-started/demo-mode/) — `northplaned serve --demo` seeds a full showcase (hosts, checks, alerts, on-call, dashboard, report, two demo users) in a separate data directory. * [Installation](/docs/getting-started/installation/) — `northplaned init` with a systemd unit, TLS, PostgreSQL, Compose with Let’s Encrypt, building from source. * [Deployment overview](/docs/deployment/overview/) — which variant fits which environment, and the ports you may need to open. # Alerts and incidents > The alert entity and its life cycle (open, acked, snoozed, resolved, expired), deduplication keys, manual alarms, suppression order and re-arm, every acknowledgement path, incidents from rules and the correlator, and the np.* labels. An **alert** is the thing people get paged for. Rules turn events into alerts; escalation policies decide who is notified; acknowledging an alert stops the chain. An **incident** groups alerts that belong together — created by a rule, by the alarm-storm correlator, or by a human or the AI agent. This page is the mental model; the how-to pages are under [Alarming](/docs/alarming/overview/). ## From event to alert [Section titled “From event to alert”](#from-event-to-alert) ```text event (ingress | state_change | heartbeat_missed | incident_update) └─► alert rule matches (CEL `match`) ── or: heartbeat rule, manual POST /alerts └─► [pendingFor] condition must hold N seconds └─► UpsertAlert(dedupKey) ── existing open/acked alert? fold in, no new chain └─► alert_opened event ├─► rule.incident → own incident ├─► suppressed? (downtime / flapping / host down / silence) → wait, re-arm later └─► StartChain(escalationPolicy) → steps → notifications ``` A matching event is a **clear** rather than an open when `event.severity == "ok"`, `event.state` is `OK`/`UP`, or the payload carries `resolve: true`; a clear resolves the open/acked alert with the same dedup key. With `resolveOnOk` (default `true`) even a *non-matching* clear event resolves — so a rule that only matches `CRITICAL` still closes its alert on the next `OK` of the same object. Rule fields, CEL and templates are on [Alert rules](/docs/alarming/alert-rules/). ## The alert entity [Section titled “The alert entity”](#the-alert-entity) | Field | Meaning | | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | `id`, `tenantId` | | | `ruleId` | id of the rule that opened it; `"manual"` for API/phone/SMS/app alarms | | `objectId`, `incidentId` | optional links | | `status` | `open` → `acked` → `resolved`; `expired` when `autoCloseAfter` hit | | `severity` | `critical` \| `warning` \| `info` \| `ok` — from the rule, else the event | | `title` | rendered from the rule’s `title` template (default: event summary, else ` is `) | | `dedupKey` | see below | | `openedAt`, `ackedAt`, `ackedBy`, `resolvedAt`, `snoozedUntil` | life-cycle timestamps | | `payload` | the triggering event payload; for manual alarms `{summary, manual: true, by, via, escalationPolicy}` | | `labels` | event labels ⊕ the rule’s `setLabels` (⊕ later merges such as `recordingUrl`, `transcript`) | | `eventIds` | the last 50 triggering event ids | | `ticket` | `{channel, type, ref, url, autoClose}` once an escalation ticket action created one | API: `GET /api/v1/alerts` (filters `status`, `severity`, `objectId`, `ruleId`, `incidentId`, `since`; newest first; default 100, max 1000), `GET /api/v1/alerts/{id}`, `POST /api/v1/alerts` (manual), `POST /api/v1/alerts/{id}:ack|:resolve|:snooze`. Permissions: `alerts:read`, `alerts:write` (raise), `alerts:ack` (ack/resolve/snooze). ## Life cycle [Section titled “Life cycle”](#life-cycle) ```text ┌──────────── clear event / :resolve / DTMF 6 / incident :resolve ────────────┐ │ ▼ open ──ack──► acked ──────────────────────────────────────────────────────────────► resolved ▲ │ │ ▲ (final) │ │ │ └── :snooze {until}: acked + snoozedUntil, chain stopped │ └─────────┼───── autoCloseAfter (rule) ───────────────────────────────────────► expired │ │ (final) └────────────┘ snooze expires: back to open, chain restarts from step 0 ``` | Transition | Trigger | Effects | | ---------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | open → acked | any [ack path](#acknowledging-snoozing-and-resolving) | `ackedAt/ackedBy` set, escalation chain cancelled, sticky ack mirrored onto the object’s `check_state`, `ack` event, audit `alert.ack` | | open → acked (snoozed) | `POST :snooze {until}` | as ack plus `snoozedUntil`; object ack comment `snoozed until …` | | acked (snoozed) → open | every 5 s the engine re-opens alerts whose `snoozedUntil` has passed | acked fields cleared, sticky ack cleared, `escalation` event `snooze expired — alarm re-armed`, chain restarts **from step 0 with `openedAt` rebased to now** | | open/acked → resolved | clear event, `POST :resolve`, DTMF `6`, IVR/AGI resolve, `POST /incidents/{id}:resolve` | chain cancelled, `alert_resolved` event, rule-created incident auto-resolved when it was the last active alert, ticket auto-close job if `ticket.autoClose` | | open/acked → expired | rule `autoCloseAfter` elapsed since `openedAt` (checked every 5 s) | like resolve, status `expired` | Re-firing events for an already open or acked alert **fold in** (see dedup) and never restart the chain; only a snooze wake-up does. ## Deduplication keys [Section titled “Deduplication keys”](#deduplication-keys) The dedup key decides whether an event opens a new alert or updates an existing one. Default when the rule has no `dedupKey` template: 1. `/` when the event has an object; 2. else `/` when the normalised event carries one (Alertmanager fingerprints, SNMP `source/agent/trapOid`, mail Message-ID, ESPA-X call id, …); 3. else `/`. A custom `dedupKey` is a Go template over `{{ .event.* }}`, `{{ .object.id }}` and `{{ .rule.name }}`. Storage enforces a partial unique index on `(tenant, dedupKey)` for status `open`/`acked`; a re-fire raises the severity if the new one is higher (never lowers it), replaces title and payload with the newest event, appends the event id (last 50), and emits **no** new `alert_opened`. Heartbeat rules use `heartbeat/`; manual alarms use whatever `dedupKey` the caller sends (phone: `call/`, SMS: `sms/`, AGI: `agi/`). ## Manual alerts [Section titled “Manual alerts”](#manual-alerts) `POST /api/v1/alerts {title, message?, severity?, escalationPolicy?, labels?, objectId?, dedupKey?}` (permission `alerts:write`) creates an alert directly — the web **Trigger alarm** dialog, the alarm app, phone/IVR (`via: voice`, `asterisk-agi`) and SMS (`action: alert`) all use it. Manual alarms have `ruleId: "manual"`, default severity `critical`, must name an existing policy if they name one, emit `alert_opened` (fan-out only, never through rules) and start the chain at once. They **ignore suppression** by design: downtimes, silences and flapping do not hold a manual alarm back. A repeated trigger with the same `dedupKey` folds into the existing open/acked alarm and returns 200 instead of 201. ## Suppression and re-arm [Section titled “Suppression and re-arm”](#suppression-and-re-arm) Suppression is evaluated for **rule-created** alerts when they open and again every 5 s while they stay open and unacked. The checks run in this order; the first hit wins and its reason is recorded in a `notification` event with `status: "suppressed"`: | # | Condition | Reason string | | - | ---------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | | 1 | the alert’s object has `downtimeDepth > 0` | `object in downtime` | | 2 | the object is flapping | `object flapping` | | 3 | the object is a host in state UNREACHABLE | `host unreachable (parent down)` | | 4 | the object is a service whose host is hard non-UP | `host down` | | 5 | the object is a service whose host has `downtimeDepth > 0` | `host in downtime` | | 6 | an active downtime lists the object, or its selector matches the **alert labels** | `downtime ` | | 7 | an active silence whose selector (empty = all) matches the alert labels and whose `textRegex` (if set) matches the alert **title** | `silence ` | While suppressed the alert **exists** and is visible as open; only the escalation chain is withheld. If the rule has an escalation policy the alert is remembered and re-checked every 5 s: once nothing suppresses it any more (downtime deleted or expired, silence expired, flapping stopped, host back UP) the chain starts — an already elapsed `after` offset fires at the next 2 s escalation poll. Acked, resolved or expired alerts are forgotten. In-memory state The re-arm set and `pendingFor` drafts live in memory. After a restart, alerts that opened while suppressed are not re-armed automatically, and a pending condition starts counting again. Not part of suppression: object acknowledgements (a sticky ack on an object does not hold back new rule alerts; it only mutes direct object notifications) and notification periods. Downtimes and silences themselves are described on [Maintenance](/docs/monitoring/maintenance/); flapping and reachability on [Checks and states](/docs/concepts/checks-and-states/). ## Acknowledging, snoozing and resolving [Section titled “Acknowledging, snoozing and resolving”](#acknowledging-snoozing-and-resolving) | Path | How | Notes | | ------------------- | ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | Web UI | Alerts page / Problems page ack dialog | `POST /alerts/{id}:ack {comment}` | | API / CLI | `POST /api/v1/alerts/{id}:ack`, `np ack` | permission `alerts:ack`; only from `open` (otherwise 404) | | Ack link | `GET /api/v1/ack/{token}` from e-mails/pushes | HMAC-signed token `...`, valid 24 h, needs `baseUrl`; answers an HTML “Quittiert” page, acks only `open` alerts | | Alarm app | ack/snooze/resolve from the app | the app authenticates with its own API token (`alerts:ack`) and calls the same `:ack` / `:snooze` / `:resolve` routes | | SMS | reply starting with the source’s `ackKeyword` (default `ACK`) from a contact’s phone number | acks the newest open alert | | IVR (inbound call) | menu option `ack-alert` (default menu digit `3`) | Twilio `voice-inbound` or Asterisk FastAGI | | Outbound voice DTMF | press `4` during a Twilio/Asterisk alarm call (`6` = resolve) | `POST /api/v1/voice/gather/{token}` | | Snooze | `POST /alerts/{id}:snooze {until}` (future RFC 3339) | alert becomes `acked` with `snoozedUntil`; wake-up restarts the chain from step 0 | | Resolve | `POST /alerts/{id}:resolve`, DTMF `6`, IVR `resolve-alert`, incident resolve, clear event | final | Every path writes an `ack`/`alert_resolved` event and an audit entry (`alert.ack`, `alert.snooze`, `alert.resolve`). Note that `POST /alerts/{id}:ack` uses the caller’s **home** tenant and ignores `X-Northplane-Tenant` (the `:resolve` and `:snooze` routes honour the header). Walk-throughs for every path are on [Acknowledge and snooze](/docs/alarming/acknowledge-and-snooze/). ## Incidents [Section titled “Incidents”](#incidents) | Field | Meaning | | ----------------------------------------------------- | --------------------------------------------------------------------------- | | `id`, `tenantId`, `version` | `PUT /incidents/{id}` needs `If-Match` | | `status` | `open` \| `resolved` | | `severity`, `title`, `summary`, `impact`, `ticketUrl` | `summary` is written by humans or the AI (`POST /incidents/{id}:summarize`) | | `createdBy` | a user name, `correlation`, `rule:` or the AI agent | | `openedAt`, `resolvedAt` | | Three ways an incident comes into being: * **Rule-driven** — `incident: true` on a rule gives every alert it opens its own incident (`createdBy: rule:`); when the incident’s last open/acked alert resolves, the incident auto-resolves (`incident_update` with `status: resolved`). Only `rule:`-created incidents auto-resolve. * **Correlator (alarm storms)** — a bus subscriber sweeps every 10 s over `alert_opened` events of the last **120 s**; if at least **5** fresh alerts share one dominant `key=value` label pair, they are attached to an incident `Alarm storm: alerts sharing =` (severity critical, `createdBy: correlation`; an existing incident of a clustered alert is reused) and an AI summary job is queued when a provider is configured. Manual alarms participate because they emit `alert_opened` too. * **Manual / API / AI** — `POST /api/v1/incidents {title, severity?, summary?, impact?, ticketUrl?, alertIds?}`; this one publishes `incident_update {action: "created"}` **through the rules**, so a rule such as `event.type == "incident_update" && event.payload.action == "created"` can alarm on app-created incidents. `POST /incidents/{id}:resolve` resolves the incident and all its open/acked alerts (chains stopped, no event emitted); `:merge {sourceIds}` moves alerts into the target and resolves the sources. The [Incidents page](/docs/ui/alerts-incidents-events/) shows cards with AI summary and resolve actions; the AI side is on [Agent chat](/docs/ai/agent-chat/). ## Alerts vs. direct object notifications [Section titled “Alerts vs. direct object notifications”](#alerts-vs-direct-object-notifications) Objects can also notify **without** a rule: `spec.contacts` / `spec.contactGroups` are notified on hard state changes (gated by `enableNotifications`, `notifyOn`, `notificationPeriod`, object downtime and a sticky ack). Those deliveries go through the same outbox and produce `notification` events with an empty `alertId`, but they create no alert and have no escalation chain. Use rules + policies for anything that must be acknowledged or escalated; see [Contacts and on-call](/docs/alarming/contacts-and-oncall/). ## The `np.*` labels [Section titled “The np.\* labels”](#the-np-labels) A few labels on an alert are interpreted by outputs. They can come from a rule’s `setLabels`, the manual trigger dialog, an IVR option’s `labels` or the event itself. | Label | Consumer | Effect | | ------------------- | ---------------------- | -------------------------------------------------------------------------- | | `np.sound` | mobile push (FCM/APNs) | tone name (`np_klaxon`, `np_sirene`, `np_puls`); APNs `sound = .caf` | | `np.volume` | mobile push | `0.0`–`1.0` critical-alert volume (APNs, with overrideSilent) | | `np.overrideSilent` | mobile push | `"true"` → APNs critical interruption level, FCM high priority | | `np.tts` | voice channel | spoken text override for the alarm call | Details: [Mobile push](/docs/alarming/mobile-push/) and [Voice and IVR](/docs/alarming/voice-and-ivr/). ## Where to go next [Section titled “Where to go next”](#where-to-go-next) * [Alarming overview](/docs/alarming/overview/) — the end-to-end picture and recipes. * [Escalation policies](/docs/alarming/escalation-policies/) — steps, repeats, `unlessAcked`, persisted timers. * [Reliability](/docs/alarming/reliability/) — outbox retries, dead letters, what is in memory. * [Events](/docs/concepts/events/) — the event types referenced above. # Architecture > How the single northplaned process is built — request path, monitoring and alarming pipelines, background workers, storage, NP-TSDB, event bus, API-first design and security posture. Northplane is one Go binary, `northplaned`, that runs everything: the HTTP API, the embedded React UI and this documentation, the check scheduler and executor, the alarming engine, notification delivery, the SNMP-trap/MQTT/ESPA/FastAGI listeners, the MCP server, the federation edge worker and the housekeeping jobs. There is no message broker, no separate worker process and no external time-series database. State lives in one SQLite file (or a PostgreSQL database), monthly event segments and the bundled NP-TSDB under a single data directory. This page explains how the pieces fit together. The pages that follow in this section explain the data model ([Object model](/docs/concepts/object-model/)), the check life cycle ([Checks and states](/docs/concepts/checks-and-states/)), the [event stream](/docs/concepts/events/), the [alert and incident model](/docs/concepts/alerts-incidents/), [tenancy and RBAC](/docs/concepts/tenancy-rbac/) and [federation](/docs/concepts/federation/). ## Overview [Section titled “Overview”](#overview) One northplaned process: HTTP front door, worker pipeline, embedded storage. Every client speaks the same REST API. ## Detailed block diagram [Section titled “Detailed block diagram”](#detailed-block-diagram) ```text browser (React SPA) np CLI np-agent MCP clients (HTTP) webhooks · Alertmanager · Twilio · cron beats │ │ │ │ │ └────────────────┴─────────┴─────────────┴───────────────────────────┘ │ HTTPS 443 ┌─────────▼─────────┐ │ reverse proxy │ Caddy / nginx terminates TLS │ (optional) │ → trustProxy: true, listen :8443 └─────────┬─────────┘ │ http(s) :8443 ┌─────────────────────────────────────────▼──────────────────────────────────────────────────┐ │ northplaned — one Go process │ │ │ │ root mux /api/ /metrics /healthz /readyz /auth/ /login /setup /register /status/ │ │ /mcp /docs/ / (SPA + assets) │ │ wrapping securityHeaders → 30 s request deadline (streams exempt) → API middleware │ │ (X-Request-Id, panic recovery, metrics, auth np_… | np_session) │ │ → per route: CSRF → login required → RBAC permission → handler │ │ │ │ ┌─ monitoring ─────────────────────────────┐ ┌─ alarming ───────────────────────────────┐ │ │ │ catalog (in-memory effective config) │ │ ingress adapters: webhook, alertmanager, │ │ │ │ → scheduler (timing wheel, 1 s slots) │ │ traps (UDP), mailin (IMAP), mqttin, │ │ │ │ → executor (builtin pool / exec pool) │ │ espa (TCP), agi, telephony, beats │ │ │ │ → results → pipeline → state machine │ │ → events → alerting engine (CEL rules, │ │ │ │ → state_change events, NP-TSDB │ │ dedup, pendingFor, suppression) │ │ │ │ passive results: POST /api/v1/results, │ │ → alerts → escalation (persisted │ │ │ │ np-agent push, freshness probes │ │ timers) → notify → outbox → channels │ │ │ └──────────────────────────────────────────┘ │ correlator · webhook-dispatcher │ │ │ └──────────────────────────────────────────┘ │ │ │ │ event bus (in-memory channels) results 8192 · events 16384 · notifications 4096 │ │ subscribers: SSE hub, correlator, webhook dispatcher │ │ │ │ workers scheduler executor pipeline alerting correlator escalation notify traps mailin │ │ mqttin espa agi api-janitor webhook-dispatcher report-scheduler dead-man │ │ [ldap-sync] [federation-edge] [ai] — all supervised, restart after 1 s │ │ │ │ storage core.db (SQLite WAL) or PostgreSQL · events-YYYYMM.db segments / partitions │ │ NP-TSDB dataDir/tsdb · secret.key (AES-256-GCM secret store) · audit chain │ └────────────────────────────────────────────────────────────────────────────────────────────┘ ``` ## The process at a glance [Section titled “The process at a glance”](#the-process-at-a-glance) | Part | What it does | Where to read more | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- | | HTTP server | One listener (`listen`, default `127.0.0.1:8443`); TLS from `tls.certFile`/`tls.keyFile`, or plaintext behind a trusted proxy (`trustProxy`) or on loopback. | [TLS and proxy](/docs/administration/tls-and-proxy/) | | REST API | `/api/v1/...`, RFC 9457 errors, cursor pagination, `If-Match` versioning, OpenAPI 3.1 generated from the route registry. | [API overview](/docs/reference/api-overview/) | | Embedded UI | React single-page app compiled into the binary (`//go:embed`), served at `/`. | [Navigation](/docs/ui/navigation/) | | Embedded docs | This Starlight site, served at `/docs/` without login. | [Documentation](/docs/development/documentation/) | | Catalog | In-memory cache of every object with its resolved (effective) spec, template chain, command class and argv — the scheduler and pipeline never touch SQL on the hot path. | [Object model](/docs/concepts/object-model/) | | Scheduler | 86 400-slot timing wheel (1 s granularity, 250 ms tick), deterministic splay, priority lane for check-now and retries. | [Checks and states](/docs/concepts/checks-and-states/) | | Executor | Runs `builtin:` checks in-process (pool 1024) and `exec:` Nagios plugins as child processes (pool `execPoolSize`, default `min(256, 32×CPU)`); sends freshness probes for passive/agent objects. | [Builtin checks](/docs/monitoring/builtin-checks/), [Plugins and Nagios](/docs/monitoring/plugins-and-nagios/) | | Pipeline + state machine | Turns results into soft/hard states, host UP/DOWN/UNREACHABLE, flapping, events and TSDB samples; batches writes every 250 ms or 500 results. | [Checks and states](/docs/concepts/checks-and-states/) | | Alerting engine | Single goroutine consuming the event queue; CEL rules, dedup, `pendingFor`, suppression, heartbeats, auto-close, snooze wake-up on a 5 s tick. | [Alerts and incidents](/docs/concepts/alerts-incidents/), [Alert rules](/docs/alarming/alert-rules/) | | Escalation + notify | Persisted escalation timers (2 s poll), outbox with retries/DLQ (3 s poll), channel drivers. | [Escalation policies](/docs/alarming/escalation-policies/), [Reliability](/docs/alarming/reliability/) | | Listeners | SNMP trap receiver (UDP), IMAP poller, MQTT subscriber, ESPA/ESPA-X TCP, FastAGI for Asterisk, Twilio webhooks. | [Event sources](/docs/alarming/event-sources/) | | AI / MCP | Agent chat, approvals and the MCP server (stdio and `/mcp`) share one tool registry that re-checks REST permissions. | [Agent chat](/docs/ai/agent-chat/), [MCP server](/docs/ai/mcp-server/) | | Federation edge | Optional worker that pulls a config bundle from a main instance and reports status. | [Federation](/docs/concepts/federation/) | ## Request path [Section titled “Request path”](#request-path) 1. **Proxy (optional).** In the reference deployments Caddy terminates TLS on 443 and forwards to `northplane:8443` over plain HTTP; the server runs with `NORTHPLANE_TRUST_PROXY=true`, `NORTHPLANE_LISTEN=:8443` and `NORTHPLANE_BASE_URL=https://`. Without a proxy, give `northplaned` its own certificate pair. A non-loopback listener with neither TLS nor `trustProxy` nor `tls.insecure` refuses to start. See [TLS and proxy](/docs/administration/tls-and-proxy/). 2. **Root mux.** The server mounts the API handler at `/api/`, `/metrics`, `/healthz`, `/readyz`; the server-rendered auth pages at `/auth/`, `/login`, `/setup`, `/register`, `/status/`; the MCP Streamable-HTTP endpoint at `/mcp`; the documentation at `/docs/`; and the SPA at `/`. Every response passes through `securityHeaders` (nosniff, `X-Frame-Options: DENY`, CSP for non-API paths, HSTS on HTTPS) and a 30 s `http.TimeoutHandler`; the streaming paths `/api/v1/stream`, `/api/v1/events:export`, `/api/v1/ai/chat` and `/mcp` are exempt from the deadline. 3. **API middleware.** Assigns `X-Request-Id` (UUIDv7), recovers panics into `np:internal`, records `np_http_*` metrics and authenticates: `Authorization: Bearer np_…` resolves an API token (prefix lookup + argon2id verify, expiry, IP bind); otherwise the `np_session` cookie resolves a DB-backed session; otherwise the request is anonymous. 4. **Per route.** Cookie sessions with `Sec-Fetch-Site: cross-site` are rejected (`np:auth/csrf`); routes with a permission require a principal (`np:auth/required`) holding that permission (`np:auth/forbidden`); then the handler runs with the tenant from `X-Northplane-Tenant` (only for `admin:tenants` holders) or the principal’s own tenant. 5. **SPA gate.** An unauthenticated *document* navigation to `/` is redirected to `/login`; API calls are never redirected, they get a 401 problem document. The UI polls the API (no SSE) at the user’s refresh interval. Details and the full error catalog are in the [API overview](/docs/reference/api-overview/); the authentication flows are in [Authentication](/docs/administration/authentication/). ## The monitoring pipeline [Section titled “The monitoring pipeline”](#the-monitoring-pipeline) ```text catalog.Entry ──► scheduler (due) ──► executor ──► CheckResult ──► pipeline ├─ host mapping (UP/DOWN/UNREACHABLE) ├─ state machine (soft/hard, flapping) ├─ check_state upsert (batched) ├─ state_change / flapping events ──► event bus ├─ retry timer (soft) / parent cascade └─ perfdata ──► NP-TSDB ``` * Object create/update pushes the entry into the catalog and the scheduler; template, check-command and time-period changes reload the whole tenant catalog and re-schedule. * Active checks (`builtin:`/`exec:`) are dispatched from the wheel; passive and agent objects only get freshness probes when `stalenessAfter` is set; results for them arrive via `POST /api/v1/results`. * The pipeline keeps `check_state` rows in memory and flushes them in batches; failed flushes are re-queued. [Checks and states](/docs/concepts/checks-and-states/) explains the timing and state rules; [Metrics and NP-TSDB](/docs/monitoring/metrics-and-tsdb/) the perfdata path. ## The alarming pipeline [Section titled “The alarming pipeline”](#the-alarming-pipeline) ```text event sources ─┐ state_change ─┼─► event bus ─► alerting engine ─► alert (dedup, pendingFor, suppression) heartbeats ─┘ ├─► alert_opened event ─► correlator (storms → incident) incident_update (API) ├─► escalation chain (persisted timers) │ └─► notify ─► outbox ─► channels (email, SMS, voice, push, …) └─► webhook subscriptions (outgoing) ``` Every input becomes an `Event`; alert rules (CEL) decide what becomes an alert; escalation policies decide who is notified and when; channels deliver through the outbox with retries and a dead-letter queue. Manual alarms (API, phone, SMS, app) create alerts directly and bypass rules and suppression. Read [Alerts and incidents](/docs/concepts/alerts-incidents/) for the model and the [Alarming overview](/docs/alarming/overview/) for the end-to-end walk-through. ## Background workers [Section titled “Background workers”](#background-workers) `serve` starts every worker under a supervisor: a panic is logged as `server: background worker panicked; restarting` and the worker restarts after 1 s. On SIGINT/SIGTERM the HTTP server drains and workers get a 30 s shutdown budget. | Worker | Cadence | Job | | ------------------------------------------ | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `scheduler` | 250 ms tick | timing wheel, due objects → queue | | `executor` | continuous | runs builtin/exec checks, freshness probes | | `pipeline` | batch 250 ms / 500 results | results → state machine → events → TSDB | | `alerting` | event-driven + 5 s tick | rules, pending alerts, heartbeats, auto-close, suppression re-arm, snooze wake | | `correlator` | 10 s sweep | alarm storms → incident | | `escalation` | 2 s poll | due escalation steps (table `escalations`) | | `notify` | 3 s poll or bus wake-up | outbox delivery, retries, dead letters | | `traps`, `mailin`, `mqttin`, `espa`, `agi` | 30 s reconcile | listeners/pollers for the matching event-source types | | `api-janitor` | 30 s / 10 min / hourly | downtime depths + flexible downtimes; expired sessions + idempotency rows; nightly (02:00–03:59 local) TSDB maintenance + event retention, otherwise TSDB flush | | `webhook-dispatcher` | bus subscriber | outgoing webhook subscriptions → outbox | | `report-scheduler` | 10 s after start, then 1 min | scheduled reports | | `dead-man` | `deadManInterval` (1 m) | GET `deadManUrl`; skipped when the results queue exceeds 7000 | | `ldap-sync` | `ldap.syncInterval` (15 m) | only with `ldap.url` | | `federation-edge` | `federation.interval` (1 m) | only in `federation.mode: edge` | | `ai` | — | only when the AI service exposes a run loop | See [Observability](/docs/administration/observability/) for health endpoints, `/metrics` and logs. ## Storage [Section titled “Storage”](#storage) | Store | Default | Notes | | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Relational core | SQLite `/core.db`, WAL, pool 16, writes serialised in-process | `storage.dsn: postgres://…` switches to PostgreSQL (pgx, pool 16/8). Schema migrations (currently 9) run automatically on every open; `northplaned storage migrate --to` copies between backends offline. | | Events | `/events-YYYYMM.db` monthly segment files (SQLite) or `events_YYYYMM` range partitions (PostgreSQL) | Append-only; `storage.eventRetentionMonths` (default 12, `0` = forever) drops whole months nightly. | | Config documents | table `resources` (`tenant_id, kind, name, doc JSON, version`) | Templates, rules, channels, policies, schedules, dashboards, roles, sites, … — the same documents that YAML [config bundles](/docs/administration/config-bundles/) carry. | | Dedicated tables | `objects`, `object_labels`, `check_state`, `alerts`, `incidents`, `downtimes`, `silences`, `heartbeats`, `users`, `tenants`, `sessions`, `api_tokens`, `secrets`, `audit_log`, `outbox`, `escalations`, `idempotency`, `kv`, `push_subscriptions`, `report_archive`, `ai_*` | Hot-path and security-relevant data. | | Secrets | table `secrets`, AES-256-GCM, master key in `secret.key` | Referenced as `$SECRET:name$`; see [Secrets](/docs/administration/secrets/). | | Audit | `audit_log` with a SHA-256 hash chain | `POST /api/v1/audit:verify`, `np audit verify`; no purge. | | NP-TSDB | `/tsdb` | see below | | Backup | `northplaned backup` → `backup.target` | `VACUUM INTO` copy of `core.db`, event segments, TSDB tree, manifest; no periodic loop. | Everything about files, DSNs, retention and restore is on the [Storage](/docs/administration/storage/) page. ## NP-TSDB [Section titled “NP-TSDB”](#np-tsdb) Every check result’s perfdata (`'label'=value[UOM];warn;crit;min;max`) becomes samples in the embedded time-series store: one series per `(objectId, metric, unit)` plus `np_exec_time` per result. Raw samples are kept in a WAL and flushed into immutable two-hour blocks (Gorilla compression); 5-minute and 1-hour aggregates are built nightly; retention is hard-coded at **30 days raw, 400 days 5-minute, 5 years 1-hour**, with a cap of 100 000 series. `POST /api/v1/metrics/query` picks the finest tier with data for the requested range. The TSDB is backend-independent — it is not touched by `storage migrate` and is copied as a directory by `backup`. Details: [Metrics and NP-TSDB](/docs/monitoring/metrics-and-tsdb/). ## Event bus [Section titled “Event bus”](#event-bus) The bus is in-memory only (Go channels): `Results` (executor → pipeline, 8192), `Events` (ingress and pipeline → alerting engine, 16384, blocking — never dropped), `Notifications` (outbox wake-ups, 4096) and `AI` (256, dropped under load). Subscribers — the SSE hub (buffer 512), the correlator (1024) and the webhook dispatcher (1024) — see every event of every tenant and filter themselves; a slow subscriber loses messages and is flagged for resync. Persistence is *not* a bus feature: producers insert events into the event store before or while publishing, so the stored history is complete even when a live subscriber overflows. Engine and API lifecycle events (`alert_opened`, `ack`, `config`, …) are published *fan-out only*, i.e. they reach SSE, webhooks and the correlator but do not re-enter the alert rules. See [Events](/docs/concepts/events/). ## API-first [Section titled “API-first”](#api-first) There is exactly one way to read or change anything: the REST API under `/api/v1`. * The React UI and the `np` CLI are ordinary REST clients. The `np-agent` pushes results to `POST /api/v1/results` and pulls central checks from `GET /api/v1/agent/checks`. * Every route is registered through one helper that records method, path, summary, permission and request/response types; the OpenAPI 3.1 document (`GET /api/openapi.json`, `northplaned openapi`) and the TypeScript types of the UI are generated from that registry, and each operation carries `x-required-permission`. Swagger UI is served at `/api/docs`. * The AI tools and the MCP server (Streamable HTTP at `/mcp`, stdio via `northplaned mcp`) do not go through HTTP handlers — they call the store and services directly — but every tool checks the same permission name as the equivalent REST route and mutating tools go through a propose/approve gate; nothing is reachable through AI that the same principal could not do over the REST API. * YAML [config bundles](/docs/administration/config-bundles/) are the declarative form of the same documents: `np apply`, the Admin tab, the Nagios importer, the AI config tools and the federation edge all call one applier. ## Embedding [Section titled “Embedding”](#embedding) The binary embeds the UI build (`internal/web/dist`, `Cache-Control: immutable` for `/assets/*`), this documentation (`/docs/`, public, own CSP, pre-compressed) and the vendored Swagger UI. A build without the UI answers `501 UI not embedded in this build`; a build without the docs answers 501 at `/docs/`. Because the UI is part of the binary, server and UI can never be out of sync; the CI type-drift gate (`make types-check`) enforces that the generated TypeScript types match the OpenAPI document. ## Security posture (summary) [Section titled “Security posture (summary)”](#security-posture-summary) | Area | Behaviour | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Transport | TLS 1.2+ with a configured cert/key; plaintext only on loopback, with `tls.insecure` (dev) or behind a proxy with `trustProxy`. No ACME — terminate at Caddy/nginx. HSTS when HTTPS. | | Authentication | Local accounts (argon2id, passwords ≥ 12 chars, per-IP login throttle), OIDC (code + PKCE), LDAP/AD (sync + search-then-bind), API tokens (`np_` + 48 hex, hashed, scopes/roles, expiry, IP bind, shown once). | | Sessions | `np_session` cookie: HttpOnly, SameSite=Lax, Secure on HTTPS; 12 h or 30 d with “remember me”; DB-backed. Cross-site cookie requests to the API are rejected (`np:auth/csrf`); no CORS. | | Authorization | Permission strings `resource:action` with wildcards, per-route checks, built-in roles `admin`/`operator`/`viewer`/`ai-agent`, custom roles with includes and IdP group mapping. See [Tenancy and RBAC](/docs/concepts/tenancy-rbac/). | | Isolation | Every row is tenant-scoped; cross-tenant reads return 404; only `admin:tenants` may switch tenants with `X-Northplane-Tenant`. | | Secrets | AES-256-GCM secret store keyed by `secret.key` (0600); values are write-only through the API; `$SECRET:name$` references in channel/source/check config. | | Audit | Hash-chained audit log for every mutation and login; NDJSON export; integrity verification. | | Headers | `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `Referrer-Policy: same-origin`, CSP with `frame-ancestors 'none'` on non-API paths. | | Ingest | Per-source auth (`token`/`hmac`/`basic`/`none`), per-source rate limits (50 ev/s, burst 200), optional Twilio signature verification, SSRF guards in HTTP checks, discovery and webhooks. | | Unauthenticated by design | `/healthz`, `/readyz`, `/metrics`, `/api/openapi.json`, `/api/docs`, `/docs/`, `GET /api/v1/system/health`, `GET /api/v1/system/info`, public status pages — restrict at the proxy where needed. | | Be aware | `X-Forwarded-For` is not used (source IPs behind a proxy are the proxy’s address); role folder/selector scopes are stored but not enforced; system roles are editable through the API. | The hardening checklist lives on the [Security](/docs/administration/security/) page. ## Where to go next [Section titled “Where to go next”](#where-to-go-next) * [Installation](/docs/getting-started/installation/) and the [Deployment overview](/docs/deployment/overview/) for how the process is run in practice. * [Configuration](/docs/administration/configuration/) for every `config.yaml` key and environment variable mentioned here. * [Backend](/docs/development/backend/) for the Go package map behind these components. # Checks and states > Active, passive and agent checks; scheduling with interval, retry, attempts and splay; soft and hard states; host UP/DOWN/UNREACHABLE and reachability; the flapping algorithm; freshness; acknowledgements; dependencies; check-now; and the events a check emits. A check produces a **result** (state 0–3, output text, optional perfdata). The pipeline folds results into the object’s saved state using Nagios-style rules: a problem is *soft* until it has been confirmed `maxCheckAttempts` times, then *hard*; hosts map to UP/DOWN/UNREACHABLE; repeated state changes mark an object as *flapping*. This page explains those rules exactly. The check types themselves are documented on [Builtin checks](/docs/monitoring/builtin-checks/), [Plugins and Nagios](/docs/monitoring/plugins-and-nagios/), [Agent](/docs/monitoring/agent/) and [SNMP](/docs/monitoring/snmp/). ## Active, passive and agent checks [Section titled “Active, passive and agent checks”](#active-passive-and-agent-checks) | Class | `checkCommand` | Who runs it | Result source | | -------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------- | | active builtin | `builtin:` | `northplaned`, in-process (pool 1024) | `scheduler` | | active exec | `exec:` or a named CheckCommand of type `exec` | `northplaned`, child process (pool `execPoolSize`) | `scheduler` | | agent | `agent:exec:` or a named CheckCommand of type `agent` | `np-agent` on the host (pulled from `GET /api/v1/agent/checks`), pushed back as results | `agent` | | passive | `passive` or empty | anything that can `POST /api/v1/results` (scripts, np-agent collectors, NSCA-style bridges) | `passive` | Only the two active classes are dispatched by the scheduler. Passive and agent objects are never executed by the server; if they have `stalenessAfter` set, the server sends a periodic **freshness probe** instead (see [Freshness and staleness](#freshness-and-staleness)). Results with source `passive` or `agent` are treated as **hard immediately** (`maxCheckAttempts` is forced to 1 for them — the classic `passive_*_checks_are_soft=0`). Passive results are posted as `{"results":[{"host":"web01","service":"http","state":2,"output":"CRITICAL - … | t=1s"}]}`; omit `service` for a host result; `state` may be numeric (0–3) or symbolic (`OK`, `WARNING`, `CRITICAL`, `UNKNOWN`, `UP`, `DOWN`, `UNREACHABLE`). The first output line is split at the first `|` into text and perfdata; further lines become long output. Unknown objects are listed under `rejected`; the call returns 202. Passive host results: use 2 / CRITICAL for DOWN `DOWN` parses to the numeric value 1 and `UNREACHABLE` to 2. Host results are mapped with the same table as active checks (see below), where 1 (WARNING) counts as **UP**. Submit `2` or `CRITICAL` for a down host. ## Scheduling [Section titled “Scheduling”](#scheduling) The scheduler is a timing wheel with 86 400 one-second slots (a 24 h ring) ticked every 250 ms. | Parameter | Default | Rule | | --------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `interval` | `60s` | cadence of an active object; truncated to whole seconds and clamped to **1 s … 24 h** | | splay | — | deterministic offset `FNV-64a(objectId) mod interval`; the first due time is the next grid point `now.Truncate(interval) + splay`. No random jitter, stable across restarts | | `retryInterval` | `15s` | after a **soft** result from the scheduler, a one-shot timer triggers a recheck after `retryInterval` (the wheel cadence is unchanged). Passive/agent/freshness results never trigger retries | | `maxCheckAttempts` | `3` | attempts before a problem becomes hard (see next section) | | `timeout` | `30s` | context deadline for builtin checks; process-group kill for plugins (`UNKNOWN - plugin timed out after … (killed)`) | | `enableChecks: false` | — | object removed from the wheel; with `stalenessAfter` it becomes a freshness-probe entry | | check-now | — | `POST /api/v1/objects/{id}/check-now` (permission `checks:run`) puts the object on a priority lane (cap 256); it does **not** reset the regular cadence | Due times are drift-free (next = planned + interval, catching up after stalls). The output queue holds 4096 jobs; when it is full the entry is postponed by one second instead of blocking the wheel. `check_state.nextCheck` shows the next planned run. Every catalog change (object create/update/delete, template/check-command/time-period change) is pushed into the scheduler immediately; there is no reload command. ## Soft and hard states [Section titled “Soft and hard states”](#soft-and-hard-states) The state machine runs per result with `maxCheckAttempts` (≤ 0 → 3) and the flap thresholds from the effective spec. | Situation | Outcome | | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | result OK | state OK, **hard**, `attempt = 1`, `lastOk` set. If the previous state was a *hard* problem: **recovery** (hard change, `lastHardChange` set, sticky acknowledgement cleared). Recovery from a *soft* problem is silent — no hard change, hence no notification | | OK → problem | `maxCheckAttempts == 1` → hard immediately; otherwise **soft**, `attempt = 1` | | soft problem continues (same or other severity) | `attempt++`; when `attempt >= maxCheckAttempts` → **hard**, `attempt = maxCheckAttempts`, `lastHardChange` set | | hard problem → *different* problem severity (e.g. WARNING → CRITICAL) | immediate hard change, `attempt = 1`, `lastHardChange` set | | same hard problem continues | stays hard, `attempt = maxCheckAttempts` | Every result updates `output`, `longOutput`, `perfdata`, `latencyMs`, `execMs` and `lastCheck`. Only **hard** transitions drive direct object notifications and — through the `stateType == "hard"` condition you put in alert rules — alerts. `GET /api/v1/problems` lists hard non-OK states. ## Host states and reachability [Section titled “Host states and reachability”](#host-states-and-reachability) Hosts reuse the numeric state space as `UP=0`, `DOWN=1`, `UNREACHABLE=2`. A host check result is mapped before it enters the state machine: 1. result **OK or WARNING → UP**; **CRITICAL or UNKNOWN → DOWN** (the classic Nagios default: a slow ping is still up). 2. a DOWN host that lists `spec.parents` (host names) and whose parents are **all** non-UP in a **hard** state → **UNREACHABLE** (`allParentsDown`: at least one parent, none UP, none soft). 3. a hard host transition immediately schedules a check-now for every host that lists it as a parent, so dependents flip to DOWN/UNREACHABLE quickly. UNREACHABLE is deliberately quiet: the `state_change` event it emits carries severity `warning` instead of `critical`, rule-created alerts on an UNREACHABLE host are suppressed with reason `host unreachable (parent down)`, and service alerts are suppressed while the host is hard non-UP (`host down`) — see [Alerts and incidents](/docs/concepts/alerts-incidents/). There is no separate dependency resource; `parents` is the dependency graph. Severity mapping used for events: host UP → `ok`, DOWN → `critical`, UNREACHABLE → `warning`; service OK → `ok`, WARNING → `warning`, CRITICAL → `critical`, UNKNOWN → `warning`. ## Flapping [Section titled “Flapping”](#flapping) The detector keeps a 21-bit history per object; a bit is set when a result’s **raw** state differs from the previous raw state (soft/hard does not matter). The flap percentage is a weighted change rate with newer checks weighing more: ```text weight(i) = 0.8 + 0.4 · i / 20 i = 0 (oldest) … 20 (newest) flapPct = 100 · Σ weight(i)·changed(i) / Σ weight(i) ``` * flapping **starts** when `flapPct >= flapThresholdHigh` (default **50 %**), * flapping **stops** when `flapPct < flapThresholdLow` (default **25 %**), * `enableFlapDetection: false` (per object or template) disables it; turning it off while flapping emits a stop. Strict alternation gives \~100 %; 21 stable checks bring it back to 0. The pipeline emits `flapping_start` / `flapping_end` events (severity `info`); while `check_state.flapping` is set, rule-created alerts for the object are suppressed (`object flapping`) and direct object notifications are withheld. See [Maintenance](/docs/monitoring/maintenance/) for how suppression interacts with downtimes. ## Freshness and staleness [Section titled “Freshness and staleness”](#freshness-and-staleness) Passive and agent objects can declare `stalenessAfter`. The wheel then fires a **freshness probe** every `stalenessAfter`; the pipeline ignores it if `lastCheck` is younger than `stalenessAfter`, otherwise it applies a synthetic `UNKNOWN` result with `stalenessText` (default `UNKNOWN - check result is stale (freshness threshold exceeded)`). Implications, read straight from the implementation: * Detection latency lies between 1× and 2× `stalenessAfter` because the probe cadence is not re-armed from the last real result. * The synthetic result carries source `freshness`, which is **not** forced hard: with the default `maxCheckAttempts: 3` a stale object goes soft first and becomes hard after further probes. Set `maxCheckAttempts: 1` on passive objects if staleness should be hard at once. * The probe updates `lastCheck`, and no retry timer applies (retries are scheduler-sourced only). * A real result clears the condition on arrival (passive results are hard immediately). `heartbeat` resources are the simpler tool for “something should call in every N minutes” without an object — see [Heartbeats](/docs/monitoring/heartbeats/). ## Acknowledgements [Section titled “Acknowledgements”](#acknowledgements) There is no object-level ack endpoint. You acknowledge an **alert** (UI, `POST /api/v1/alerts/{id}:ack`, `:snooze`, ack link, SMS keyword, IVR digit, DTMF, app), and the API mirrors `ackedBy`/`ackComment` onto the object’s `check_state` when the alert has an `objectId`. The ack is **sticky**: it is cleared only on a hard recovery (or when a snooze wakes up). Effects on the object side: * acknowledged problems disappear from `GET /api/v1/problems` unless `includeHandled=true`; * direct object notifications for problems are skipped while acked (recoveries still go out); * on the alert side an ack ends the escalation chain. `Acknowledgement{sticky, expiresAt}` exists in the model but expiring acks are not implemented. All ack paths are listed on [Acknowledge and snooze](/docs/alarming/acknowledge-and-snooze/). ## Events emitted by a check [Section titled “Events emitted by a check”](#events-emitted-by-a-check) | Event | When | Payload (gist) | Severity | | --------------------------------- | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------- | | `state_change` | raw state changed **or** a hard state was entered — soft transitions are emitted too, with `stateType: "soft"` | `{object, host?, kind, fromState, toState, from, to, stateType, attempt, output, labels, metric}` | from the new state (host UNREACHABLE → `warning`) | | `flapping_start` / `flapping_end` | flap edges | `{object, flapPct, labels}` | `info` | That is why rules for “page me” conditions should test `event.stateType == "hard"`. The event catalogue is on [Events](/docs/concepts/events/). ## Timing and limits quick reference [Section titled “Timing and limits quick reference”](#timing-and-limits-quick-reference) | Item | Value | | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------- | | `interval` / `retryInterval` / `maxCheckAttempts` / `timeout` defaults | 60 s / 15 s / 3 / 30 s | | interval clamp | 1 s … 24 h, whole seconds | | wheel tick / slots | 250 ms / 86 400 | | queues | scheduler out 4096, priority 256, results 8192 | | pipeline batch | every 250 ms or 500 results | | exec pool / builtin pool | `min(256, 32 × CPU)` (config `execPoolSize`) / 1024 | | plugin stdout / stderr cap | 64 KiB / 16 KiB | | flap window / thresholds | 21 checks / 25 % low, 50 % high | | passive results | 202, unknown objects reported in `rejected`; 503 when the pipeline is stalled | # Events > The event model, the complete event type table with emitters and payloads, how events flow through the in-memory bus, persistence and retention in monthly segments, querying, the SSE stream, NDJSON export and the Events page. An **event** is the unit of history in Northplane. Every state change, every inbound alarm, every notification attempt, every ack, downtime, silence, escalation step and configuration change is appended to the event store and fanned out live. Alert rules read events; the SSE stream, outgoing webhooks and the correlator subscribe to them; the UI and reports query them. ## The event model [Section titled “The event model”](#the-event-model) ```json { "id": "0199a8c4-5e21-7b3c-9a0e-2f1d7c8b4e55", "tenantId": "00000000-0000-7000-8000-000000000001", "ts": "2026-08-23T10:15:00.123Z", "type": "state_change", "objectId": "0199a8c0-…", "severity": "critical", "payload": { "object": "db-01", "kind": "host", "from": "UP", "to": "DOWN", "stateType": "hard", "attempt": 3, "output": "CRITICAL - no reply from 10.0.0.5 within 5s", "labels": { "env": "prod" } } } ``` | Field | Type | Meaning | | ---------- | ----------------------------------------- | ----------------------------------------------------------------------------------------- | | `id` | string | UUIDv7 — time-ordered; used as pagination cursor and as the SSE `id:` / `Last-Event-ID` | | `tenantId` | string | every event belongs to exactly one tenant | | `ts` | RFC 3339 | event time (UTC) | | `type` | string | one of the types below | | `objectId` | string, optional | the monitored host/service, when the event concerns one | | `sourceId` | string, optional | the **event source id** for `ingress` events; the **heartbeat id** for `heartbeat_missed` | | `severity` | `critical` \| `warning` \| `info` \| `ok` | optional | | `payload` | JSON object | type-specific, see the table | Events are append-only: there is no update or delete endpoint, only retention. ## Event types [Section titled “Event types”](#event-types) | Type | Emitted by | Payload (gist) | Severity | | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------- | | `state_change` | pipeline — on every raw state change and whenever a hard state is entered (soft transitions included, with `stateType: "soft"`) | `{object, host?, kind, fromState, toState, from, to, stateType, attempt, output, labels, metric}` — `from`/`to` are labels such as `UP`, `CRITICAL`; `metric` is the first perfdata label | from the new state; host UNREACHABLE is forced to `warning` | | `flapping_start` / `flapping_end` | pipeline | `{object, flapPct, labels}` | `info` | | `ingress` | every event-source adapter: webhook, Alertmanager receiver, email/IMAP poller, SNMP-trap receiver, MQTT subscriber, ESPA / ESPA-X listeners, SMS inbound with `action: event` | the normalised event (`NormEvent`): `{source, receivedAt, dedupKey?, severity, summary, labels?, payload?, resolve?}`; `payload` archives the original body | set by the adapter mapping or the source’s default severity | | `heartbeat_missed` | alerting engine heartbeat sweep (every 5 s); the beat endpoint on recovery | `{heartbeat, labels, summary}`; on recovery additionally `resolve: true` | the heartbeat’s severity; `ok` on recovery | | `alert_opened` | alerting engine when a rule opens a new alert; `POST /api/v1/alerts` and the phone/SMS/AGI paths for manual alarms | `{alertId, title, severity, rule, labels}`; manual alarms add `via` and use `rule: "manual"` | the alert’s severity | | `alert_resolved` | alerting engine on a clear event; `POST /api/v1/alerts/{id}:resolve`, DTMF `6`, IVR/AGI resolve | `{alertId, title, rule}` or `{alertId, title}` (+ `by`, `via` for AGI) | `ok` | | `ack` | `POST /api/v1/alerts/{id}:ack` and `:snooze`, ack link, SMS keyword, IVR digit, DTMF `4`, AGI | `{alertId, by, comment}`; snooze: `comment: "snoozed until "`; link/SMS/IVR/DTMF: `{alertId, via: "ack-link"}`; AGI: `{alertId, by, via}` | `info` | | `escalation` | escalation engine per step firing, including repeats; alerting engine when a snooze expires | `{alertId, step, repeat, contacts: [names], channels}` (channels = the step’s override list, empty when contact preferences were used); wake-up: `{alertId, title, comment: "snooze expired — alarm re-armed", policy}` | `info` | | `notification` | notifier per delivery attempt (alerts and direct object notifications); alerting engine for alerts opened while suppressed | `NotificationRecord{alertId, stepIndex, repeat?, contactId?, contact?, channel, channelId?, target? (masked), status, attempt, error?, providerId?, latencyMs?}`; `status` ∈ `pending`, `sent`, `failed`, `dead`, `suppressed` (with `error` = suppression reason) | `info` | | `incident_update` | alerting engine (rule-created incident opened / auto-resolved), correlator (alarm storm), `POST /api/v1/incidents` | `{incidentId, alertId?, title, createdBy?, status}`; correlator: `{incidentId, title, alerts, cluster: "k=v"}`; API create: `{incidentId, title, summary, createdBy, status, action: "created", labels}` | incident severity; auto-resolve `ok`; correlator `critical` | | `downtime` | `POST /api/v1/downtimes` | `{downtimeId, comment, start, end}` | `info` | | `silence` | `POST /api/v1/silences` | `{silenceId, comment, expiresAt}` | `info` | | `config` | API on any configuration mutation (objects, templates, rules, channels, bundles, …) | `{kinds: ["host"]}`, `{kinds: ["alert-rule"]}`, … | `info` | | `system` | AI service when the monthly token budget warning fires | `{summary}` | `warning` | | `ai_action`, `comment`, `anomaly`, `forecast` | defined in the taxonomy but **not emitted** by any code path in this version | — | — | Two things to remember when writing rules: the alert engine receives only what is published *to* it (`ingress`, `state_change`, `flapping_*`, `heartbeat_missed`, and `incident_update` from `POST /api/v1/incidents`); engine- and API-generated lifecycle events (`alert_opened`, `ack`, `escalation`, `notification`, `config`, `downtime`, `silence`, …) are *fan-out only* so that they never re-enter the rules. And for `ingress` events the inner `payload` keys are hoisted to `event.payload.` in CEL (`event.payload.subject`, `event.payload.body`, …). See [Alert rules](/docs/alarming/alert-rules/). ## How events flow [Section titled “How events flow”](#how-events-flow) ```text producers bus (in-memory) consumers ───────────────────────────────────── ────────────────────── ─────────────────────────── pipeline (state_change, flapping) ──► Events queue (16384) ──► alerting engine (rules) ingress adapters (ingress) ──► │ heartbeat sweep (heartbeat_missed) ──► │ POST /incidents (incident_update) ──► │ ▼ fan-out engine/API lifecycle events ──────────► subscribers only ───────► SSE hub (512) (alert_opened, ack, notification, …) "FanoutOnly" correlator (1024) webhook dispatcher (1024) every producer also ──────────────────► event store (segments) — persisted before/while publishing ``` * The bus does not persist anything; producers insert into the event store themselves (best-effort, failures are counted in `np_events_dropped_total`), so a slow live subscriber never loses history. * The `Events` queue blocks producers when full rather than dropping; subscriber buffers drop and mark the subscriber for resync (the SSE stream then sends a `resync` frame). * There are no topics: every subscriber sees every event of every tenant and filters by tenant, type and selector itself. ## Persistence and retention [Section titled “Persistence and retention”](#persistence-and-retention) | Backend | Layout | | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | SQLite (default) | one file per month in the data directory: `events-YYYYMM.db` (+ `-wal`, `-shm`), own connection pool (4), indexes on `(tenant_id, ts)` and `(object_id, ts)`; cross-month queries fan out and merge in Go | | PostgreSQL | parent table `events … PARTITION BY RANGE (ts)`, child partitions `events_YYYYMM` created on demand with the same indexes | Retention is `storage.eventRetentionMonths` (default **12**, `0` = keep forever; file-only, no environment variable). The janitor enforces it once a night (between 02:00 and 03:59 local time) by deleting whole segment files or dropping whole partitions whose month is older than the cutoff. There is also a storage-level `PurgeEventPayloads(tenant, type, before)` that blanks payloads to `{}` for GDPR retention classes; it is not wired to any API route or scheduled job in this version. Events are included in `northplaned backup` (every `events-*.db` is copied, the current month last). See [Storage](/docs/administration/storage/). ## Querying events [Section titled “Querying events”](#querying-events) `GET /api/v1/events` (permission `events:read`) returns `{items, nextCursor}` newest first. | Query parameter | Meaning | | ---------------------- | ------------------------------------------------ | | `types` | comma-separated list of event types | | `objectId`, `sourceId` | exact match | | `severity` | exact match | | `from`, `to` | RFC 3339 window (unparseable values are ignored) | | `cursor` | the `id` of the last item of the previous page | | `limit` | default 200, max 1000 | Example: ```bash curl -s "https://np.example.com/api/v1/events?types=state_change,alert_opened&from=2026-08-23T00:00:00Z&limit=50" \ -H "Authorization: Bearer np_…" ``` `GET /api/v1/alerts/{id}` lists the triggering event ids of an alert (`eventIds`, last 50); reports and the Overview page’s “Recent events” card are built on the same query. ## Live stream (SSE) [Section titled “Live stream (SSE)”](#live-stream-sse) `GET /api/v1/stream` (permission `events:read`) is a Server-Sent-Events feed of everything fanned out on the bus for the caller’s tenant. Filter with `?types=a,b` and `?selector=