Skip to content

Privacy & data-rights model

Imaginook's privacy pledge is enforced in code, not just promised. This documents the posture and where each guarantee lives.

Zero third-party telemetry

  • No analytics/telemetry SDKs. There is no Sentry, Bugsnag, Google Analytics, Mixpanel, Hotjar, or similar in composer.json or package.json — asserted by tests/Feature/Privacy/SelfHostedAssetsTest.php.
  • First-party, cookieless analytics. Visitor counts come from a first-party, cookie-free beacon that stores only a day-bucketed salted hash (never a raw IP), honours DNT/GPC, and always returns 204.
  • Self-hosted fonts. All fonts are bundled at build time (@fontsource/* + a build-time-inlined Bunny font in vite.config.js); no font is fetched from a CDN at runtime. Regression-tested against resources/css/app.css.
  • No third-party embeds by default. The block builder's embed block is not rendered (its saved URL is preserved, but no iframe is emitted) unless privacy.allow_third_party_embeds is on (off by default) — an embedded third party could track a site's visitors. Profile custom blocks strip iframes entirely and confine images to self/data:.

Data minimization

  • No date of birth. The age gate is a regional self-attestation: the user affirms they meet the geo-aware floor (13 / 16, config/legal.php) and we store only that integer floor + the timestamp (age_affirmed_floor, age_affirmed_at) — never a birth date.
  • IP minimization. The analytics beacon never stores a raw IP. Consent-record IPs are truncated at write time (IPv4 → /24, IPv6 → /48 via App\Support\Privacy\IpMinimizer) and then nulled entirely after privacy.ip_retention_days (imaginook:minimize-consent-ips, scheduled daily) — the consent proof (document/version/timestamp) is kept, the locator is dropped. Raw IPs remain only where they are functionally required: sessions (device management, cascade-deleted) and the append-only audit_log (security/legal hold).
  • PII-scrubbed logs. App\Support\Logging\PiiScrubbingProcessor redacts IPs and emails from log lines (on by default via privacy.scrub_logs) — logs are a debugging aid, not a PII store.
  • Salted hashes, not raw identifiers. Abuse/DMCA reporters, widget flood detection, and the signup throttle all key off sha256(app_key | scope | ip) hashes, never a stored raw IP.

Data rights

  • Portable export (GDPR Art. 20). App\Support\Account\AccountExporter produces a machine-readable export of the account: profile, quota, sites+domains, buttons, widgets, webrings, follows, disputes, and consent records — deliberately excluding secrets (password hash, 2FA secret/recovery codes).
  • True erasure. An account delete is a real, complete removal — not the moderation "offline-not- deleted" model. App\Support\Account\AccountPurger runs the deletion; every user-owned table cascade-deletes on user delete (sites, domains, widgets, buttons, webrings, follows, notifications, disputes, scan items, trash, consent) — verified by tests/Feature/Privacy/DataRightsTest.php. Files and nginx host maps are torn down; the append-only audit trail is anonymized (actor_id → null, actor_label → 'deleted account') rather than deleted, for the legal-hold carve-out.
  • PII-free deletion receipts. Proof-of-deletion receipts kept after erasure contain no PII and are themselves expired on the privacy.retention_days clock (imaginook:purge-expired).

Retention summary

DataRetention
Analytics beacons (raw)pruned after the rollup window; only day-bucketed aggregates persist
Consent-record IPstruncated at write; nulled after ip_retention_days (default 180d)
Deletion receipts / exportsretention_days (default 30d)
Session IPsfor the session lifetime; deleted on logout / account delete
Audit logretained (append-only, legal hold); actor anonymized on account delete

AGPL-3.0 · built in the open.