Appearance
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.jsonorpackage.json— asserted bytests/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 invite.config.js); no font is fetched from a CDN at runtime. Regression-tested againstresources/css/app.css. - No third-party embeds by default. The block builder's
embedblock is not rendered (its saved URL is preserved, but no iframe is emitted) unlessprivacy.allow_third_party_embedsis on (off by default) — an embedded third party could track a site's visitors. Profile custom blocks strip iframes entirely and confine images toself/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 afterprivacy.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-onlyaudit_log(security/legal hold). - PII-scrubbed logs.
App\Support\Logging\PiiScrubbingProcessorredacts IPs and emails from log lines (on by default viaprivacy.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\AccountExporterproduces 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\AccountPurgerruns the deletion; every user-owned table cascade-deletes on user delete (sites, domains, widgets, buttons, webrings, follows, notifications, disputes, scan items, trash, consent) — verified bytests/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_daysclock (imaginook:purge-expired).
Retention summary
| Data | Retention |
|---|---|
| Analytics beacons (raw) | pruned after the rollup window; only day-bucketed aggregates persist |
| Consent-record IPs | truncated at write; nulled after ip_retention_days (default 180d) |
| Deletion receipts / exports | retention_days (default 30d) |
| Session IPs | for the session lifetime; deleted on logout / account delete |
| Audit log | retained (append-only, legal hold); actor anonymized on account delete |