Introduces Personal, a third edition between Community and Pro, bought once, which lifts every cap and opens multi-host monitoring up to 20 remote machines. Authorization moves from a binary "is this Pro" check to an order, Community < Personal < Pro, resolved per capability. Also ships a security round on the agent gRPC plane and the HTTP surface, and fixes stale Swarm update findings, pods killed mid-migration, and two navigation regressions.
The Personal edition
Discussion #31 converged on the same finding from three users: Maintenant is useful in a homelab, Community gets narrow quickly there, and 29€/month makes no sense for a machine hosted at home. One of them was explicit: without an intermediate option, they would fork the project to lift the limits themselves.
Community caps never protected the business model. A company does not buy Pro because it reached ten endpoints, it buys Pro because it has to alert a team, run an on-call rotation and show a status page to its customers. The caps only created friction on personal use. Personal removes that friction without taking anything away from Pro, following a single rule: Personal sees everything, Pro warns others.
Personal (149€ once, for life, non-commercial) opens:
- every cap lifted: endpoints, heartbeats, certificate monitors, status components
- multi-host, up to 20 remote machines
- Email channel and advanced trigger filters
- CVE enrichment, per-container risk scoring, unified security posture
- incident management, changelog, resource history, OCSP stapling
- €59 per year for additional updates
Pro keeps what serves a team or addresses third parties: unlimited hosts, Slack and Microsoft Teams, alert escalation, per-entity routing, maintenance windows, subscriber notifications, status page branding, and the right to use Maintenant on behalf of others.
Community is unchanged in what it already did, and gains the Swarm and Kubernetes node and cluster views, which were the only gated part of a feature otherwise entirely open (discovery, services, workloads and pods were already free). It stays standalone: no remote agent enrollment.
Existing Pro keys are unaffected. An active license carrying no edition information resolves to Pro, exactly as before.
Ordered gating instead of a Pro flag
requireCapability replaces requirePro on the 65 guarded routes: 26 open at Personal, 29 at Pro, and the 10 Swarm and Kubernetes routes lose their guard entirely. A single capability registry (20 capabilities) and a single quota registry (5 capped resources, unlimited being -1) are read by the REST middleware, the MCP tools and GET /api/v1/edition alike, which is what keeps the three surfaces agreeing. A conformance suite walks the three editions against the twenty capabilities on both surfaces and asserts they match.
Refusals are now machine-readable and carry the capability, the resource, the limit and the edition that grants it:
403 EDITION_REQUIREDnaming the edition that opens the feature, so a Community user blocked on incidents is told Personal, not Pro403 QUOTA_EXCEEDEDon the four capped resources, heartbeats included, which used to answer with a different status and code409 HOST_LIMIT_REACHEDon agent enrollment
The interface no longer matches on an English sentence to build its message. Enrollment stays capped atomically, so concurrent enrollments can never exceed the limit, and the machine running the application is never counted.
The /pro-edition page becomes /editions, a three-column comparison highlighting the active edition (the old route redirects). Upsell banners are now shown to Community users only.
Personal update window
A Personal license is bought once and never expires. What is bounded is the right to new versions: one year, then ?59 per extra year.
- a build released inside the window stays licensed for life
- a build released after it keeps its edition for 30 days of grace, then falls back to Community for that binary alone
- going back to a covered version restores everything immediately, and costs nothing
The grace clock is anchored on the window end rather than on the build, so updating every month cannot restart it. Its state lives in <dataDir>/.maintenant-update-window (0600). Two statuses, update_window_grace (warning) and update_window_ended (critical), reach GET /api/v1/license/status and the interface. A throttled instance running in server mode still starts: refusing would take a whole fleet's monitoring down over an unpaid renewal.
Security
Remediation of an audit of the agent gRPC plane and the HTTP surface:
- Unauthenticated remote DoS. A crafted handshake against the NULL-keyed local sentinel reached
ed25519.Verifywith a malformed key, which panics, taking the whole process down. The sentinel and any key that is not 32 bytes are rejected up front, and panic-recovery interceptors now returnInternalinstead of killing the server. - agent_id spoofing. Events are dispatched under the authenticated agent identity instead of the client-controlled wire field, and any mismatch is rejected. A compromised agent can no longer forge or wipe another host's data.
- Enrollment tokens were stored in clear, so any copy of the SQLite file handed over replayable tokens. The column becomes a sha256 hash plus a 14-character prefix kept for display. Existing installs are rewritten in place on first boot, so outstanding tokens stay valid and nothing has to be reissued.
MAINTENANT_MCP=truewithout OAuth credentials now refuses to start instead of serving/mcpunauthenticated, since the reverse proxy is documented as letting that route bypass auth. Opt out withMAINTENANT_MCP_ALLOW_UNAUTHENTICATED=true.--mcp-stdio, which never listens, is unaffected.- Two hardcoded
Access-Control-Allow-Origin: *on the log streams are removed; they let any origin an operator visited read container logs on an unauthenticated deployment. - Security headers on every response: nosniff, Referrer-Policy, frame refusal, and a CSP whose
script-srccarries the sha256 of the inline theme bootstrap, computed at startup from the embedded index. The public status page stays embeddable./api/gets its own rate bucket (50 rps, burst 200), the public bucket being too tight for an ordinary dashboard load. - Go toolchain bumped to 1.26.5, and the frontend lockfile refreshed to clear six advisories, all in build and test tooling, none of it shipped in the bundle.
Bug fixes
- Stale update findings on Swarm. Cleanup only matched findings whose container name appeared in the current scan, and Swarm renames the task container on every
docker stack deploy, so a replaced task left a dead row on the Updates page for good, one per service per upgrade. Findings are now shed by container identity,update.resolvedis emitted so the alert engine resolves the right alert, and reads are filtered immediately rather than at the next scan. Purging archived containers also clears the tables keyed byexternal_id(image updates, CVEs, version pins, risk score history), which had no foreign key to cascade through. (#50) - Kubernetes pods killed mid-migration. Migrations run before the HTTP listener starts, so
/api/v1/healthstays silent for their whole duration and kubelet killed the pod after roughly 95 seconds, leaving the schema marked dirty and repeating on every restart. AstartupProbe(10 minutes of grace, configurable in the chart) is added to the raw manifest and the Helm chart, and the default PVC goes from 1Gi to 10Gi, since table rebuilds copy a table before dropping the original. - Containers entry missing on a Swarm node. The sidebar hid it as soon as one service was deployed, although the host still runs plain containers and the page worked by direct URL.
- Navigation silently emptied by one failed request. A single failed runtime status fetch left the store unloaded for the rest of the session, and every runtime-specific entry disappeared with no error shown. It is retried with exponential backoff, and the error surfaced.
--mode=serverexited at boot whatever the license said, so the documented multi-host setup could not be started that way. The check now runs after the license manager has resolved the edition.- Agent stream cut periodically behind a reverse proxy. The stream is a single gRPC request whose body never ends, so a proxy capping request duration severs it (Traefik v3:
respondingTimeouts.readTimeout, 60s by default). Documented in the agent setup guide with a matching troubleshooting entry.
Upgrade notes
- Automatic DB migration (27) indexes
containers(external_id), plus a one-time in-place rewrite of enrollment tokens into hashes. Tokens already issued stay valid. Back up the.dbfirst as usual. - MCP with
MAINTENANT_MCP=trueand no client id/secret no longer starts. Set the credentials, or setMAINTENANT_MCP_ALLOW_UNAUTHENTICATED=trueif the route is genuinely on a trusted network. - Kubernetes: apply the updated manifest or chart before upgrading, so the
startupProbeis in place, and check that the volume has room for a table rebuild. - Nothing to do for existing Pro keys. Editions are resolved from the license as before; a key with no edition information remains Pro.
Pull the new image to upgrade.