import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import { api, type PrivacyOverview } from "../api/client"; import { formatDateTime } from "../utils/date"; import { formatAgo } from "../utils/duration"; import { readableError } from "../utils/errors"; const CHANNEL_LABELS = { gotify: "Gotify", ntfy: "ntfy", smtp: "email (SMTP)", webhook: "webhook" } as const; /** SQLite timestamps ("2026-09-26 04:07:21") are UTC without a zone marker. */ function parseTime(value: string): Date { return new Date(value.includes("T") ? value : `${value.replace(" ", "T")}Z`); } export default function Privacy() { const [data, setData] = useState(null); const [error, setError] = useState(null); const [exporting, setExporting] = useState(false); useEffect(() => { api.privacy .overview() .then(setData) .catch((err) => setError(readableError(err))); }, []); async function download() { setExporting(true); setError(null); try { const blob = await api.privacy.exportOwnData(); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "homelab-manager-my-data.json"; a.click(); URL.revokeObjectURL(url); } catch (err) { setError(readableError(err)); } finally { setExporting(false); } } const retention = data?.retention; const outbound = data?.outbound; const enabledChannels = outbound ? (Object.keys(CHANNEL_LABELS) as (keyof typeof CHANNEL_LABELS)[]).filter((c) => outbound.channels[c].enabled) : []; return ( <>

Privacy

Homelab Manager runs on your own server, and what it stores stays there unless it's listed under{" "} Where data goes. It has no analytics or telemetry, loads no third-party scripts, fonts or trackers, and sets no tracking or advertising cookies. This page describes what it actually does and shows live values for this installation.
{error &&
{error}
} {data && (

About you

Name
{data.me.user.name ?? "—"}
Email
{data.me.user.email ?? "—"}
Role
{data.me.user.role}
Sign-in ID
{data.me.user.subject}
First signed in
{formatDateTime(parseTime(data.me.user.createdAt))}
Last signed in
{data.me.user.lastLoginAt ? formatDateTime(parseTime(data.me.user.lastLoginAt)) : "—"}
Recorded actions
{data.me.auditEntries} change{data.me.auditEntries === 1 ? "" : "s"} you made are in the audit log under your name
Your name, email and sign-in ID come from Authentik and are refreshed each time you sign in.
Your active sign-ins
{data.me.sessions.map((s, i) => ( ))} {data.me.sessions.length === 0 && ( )}
From Browser Last active Ends
{s.ip ?? "—"} {s.current && this browser} {s.userAgent ?? "—"} {formatAgo(s.lastAccess)} {s.expiresAt ? formatDateTime(new Date(s.expiresAt)) : "—"}
No active sign-ins found.
“Download my data” gives you these details and every audit-log entry made under your account as a file — only yours.
)}

What is stored

What Contains Kept
Accounts Sign-in ID, email and name (from Authentik), role, first and last sign-in. Until an administrator removes the account from the database — the app can change roles but has no delete-account function.
Sign-in sessions Your sign-in ID, email and name, the ID token Authentik issued (needed to sign you out there), and the IP address and browser name from the sign-in. 7 days after sign-in, or until you sign out or an administrator ends the session.
Audit log Who changed what: your name (or email) as it was at the time, the action, what it was done to, and details of the change. Also each time you sign in or out — with the IP address you came from — and when your account was first created. {retention?.enabled ? `Entries older than ${retention.retentionDays} days are deleted (checked every ${retention.intervalHours} h).` : "Indefinitely — automatic deletion is off (Settings → Logs)."}{" "} The name stays in old entries even if the account is removed.
Diagnostic log Each call to a DNS provider or integration: which service, what operation, success or failure, how long it took, any error text. It isn't about people. {retention?.enabled ? `Deleted after ${retention.retentionDays} days.` : "Indefinitely — automatic deletion is off."}
Server reports From each agent: hostname, IP addresses, CPU, memory and disk usage, listening ports with the program using them, and cron/systemd tasks including their commands — which can contain sensitive text. The latest report; removed with the server.
Secrets tracker Names, types, descriptions, expiry dates, notes, and a host to check for certificates. Never the secret values themselves. Until deleted.
Integration, DNS and notification credentials API tokens and passwords — including the Gotify/ntfy tokens, SMTP password and webhook secret — encrypted (AES-256-GCM) with a key held in the server's environment, not in the database. (If that key isn't set, notification credentials are kept unencrypted and the server says so at startup.) Until deleted. Settings → Backup exports include them, encrypted with a passphrase you choose.
Inventory IP addresses, cached DNS records, domain registrations, port notes, tags and maintenance windows. Until deleted.

Where data goes

  • Authentik — you sign in there. The app receives your name, email and sign-in ID; it never sees your password.
  • Your integrations {outbound && outbound.integrationTypes.length > 0 ? ` (${outbound.integrationTypes.join(", ")})` : ""} — API calls to systems you configured, to read status and, when someone with permission asks, to act (start a VM, run a template…).
  • DNS providers {outbound && outbound.dnsProviderTypes.length > 0 ? ` (${outbound.dnsProviderTypes.join(", ")})` : ""} — zones and records are read from, and changed at, the provider.
  • Notifications —{" "} {enabledChannels.length === 0 ? ( "no channel is switched on, so no alerts leave the server." ) : ( <> alerts are sent to{" "} {enabledChannels .map((c) => `${CHANNEL_LABELS[c]}${outbound!.channels[c].host ? ` (${outbound!.channels[c].host})` : ""}`) .join(", ")} . They can mention server names, addresses, domain names and secret names. If a channel points at a public service such as ntfy.sh, that service sees the text. )}
  • Domain registries — the {outbound?.domainsTracked ?? 0} domain name{outbound?.domainsTracked === 1 ? "" : "s"} tracked on the Domains page are sent to IANA and each registry's RDAP or WHOIS server to read the expiry date. Nothing but the names.
  • Certificate checks — for {outbound?.tlsCertificateChecks ?? 0} secret{outbound?.tlsCertificateChecks === 1 ? "" : "s"} with a host to check, the app connects to that host to read its certificate.
  • Skatteverket — only when an admin clicks "Import from Skatteverket" under Settings → Names, the app asks Skatteverket's open name statistics for the most common given names. The request carries a sex and a birth year, nothing about you or your servers.
  • Servers and agents — {outbound?.serversWithAgent ?? 0} of {outbound?.servers ?? 0} servers have reported in. Agents push their reports to the app; the app doesn't connect out to them, apart from port scans, which run only when an operator starts one and only against private addresses.
  • Nothing else. No telemetry, no update checks, no crash reports, no third-party analytics.

In your browser

  • One session cookie: scripts on the page can't read it, it's restricted from cross-site requests, it's marked secure over HTTPS, and it lasts 7 days.
  • One saved preference in this browser: whether you chose light or dark mode.
  • If you install the app to your device, it stores no pages or data for offline use.

Who can see what

  • Everyone signed in: the inventory and status pages, including server, IP, domain and secret-name details.
  • Operators and admins: also the audit log — who did what, and who signed in from where.
  • Admins only: the user list, everyone's active sign-ins (with IP and browser), the diagnostic log and settings.

Removing or limiting data

  • Sign out to end your session now. Administrators can also end anyone's on the Sessions page.
  • Audit and diagnostic logs can be deleted automatically after a set number of days under Settings → Logs {retention ? ` — currently ${retention.enabled ? `on, ${retention.retentionDays} days` : "off"}` : ""}.
  • Your account can only be removed by an administrator deleting it from the database; the app doesn't do that itself. Old audit entries keep your name until the log is purged.
  • A copy of your data is the “Download my data” button above.
); }