chore: add dpa, update terms and privacy

This commit is contained in:
Carl-Gerhard Lindesvärd
2026-03-03 10:59:45 +01:00
parent 83761638f2
commit 9e46099246
9 changed files with 751 additions and 80 deletions

View File

@@ -0,0 +1,498 @@
'use client';
import Image from 'next/image';
export default function DpaDownloadPage() {
return (
<div className="min-h-screen bg-white text-black">
{/* Print button - hidden when printing */}
<div className="sticky top-0 z-10 flex justify-end gap-3 border-gray-200 border-b bg-white px-8 py-3 print:hidden">
<button
className="rounded bg-black px-4 py-2 font-medium text-sm text-white hover:bg-gray-800"
onClick={() => window.print()}
type="button"
>
Download / Print PDF
</button>
</div>
<div className="mx-auto max-w-3xl px-8 py-12 print:py-0">
{/* Header */}
<div className="mb-10 border-gray-300 border-b pb-8">
<p className="mb-1 text-gray-500 text-xs uppercase tracking-widest">
OpenPanel AB
</p>
<h1 className="mb-2 font-bold text-3xl">Data Processing Agreement</h1>
<p className="text-gray-500 text-sm">
Version 1.0 &middot; Last updated: March 3, 2026
</p>
</div>
<p className="mb-8 text-gray-700 text-sm leading-relaxed">
This Data Processing Agreement ("DPA") is entered into between
OpenPanel AB ("OpenPanel", "Processor") and the customer identified in
the signature block below ("Controller"). It applies where OpenPanel
processes personal data on behalf of the Controller as part of the
OpenPanel Cloud service, and forms part of the OpenPanel Terms of
Service.
</p>
<Section number="1" title="Definitions">
<ul className="list-none space-y-2 text-gray-700 text-sm">
<li>
<strong>GDPR</strong> means Regulation (EU) 2016/679 of the
European Parliament and of the Council.
</li>
<li>
<strong>Controller</strong> means the customer, who determines the
purposes and means of processing.
</li>
<li>
<strong>Processor</strong> means OpenPanel, who processes data on
the Controller's behalf.
</li>
<li>
<strong>Personal Data</strong>, <strong>Processing</strong>,{' '}
<strong>Data Subject</strong>, and{' '}
<strong>Supervisory Authority</strong> have the meanings given in
the GDPR.
</li>
<li>
<strong>Sub-processor</strong> means any third party engaged by
OpenPanel to process Personal Data in connection with the service.
</li>
</ul>
</Section>
<Section number="2" title="Our approach to privacy">
<p className="mb-3 text-gray-700 text-sm leading-relaxed">
OpenPanel is built to minimize personal data collection by design.
We do not use cookies for analytics tracking. We do not store IP
addresses. Instead, we generate a daily-rotating anonymous
identifier using a one-way hash of the visitor's IP address, user
agent, and project ID combined with a salt that is replaced every 24
hours. The raw IP address is discarded immediately and the
identifier becomes irreversible once the salt is rotated.
</p>
<p className="mb-2 text-gray-700 text-sm">
The data we store per event is:
</p>
<ul className="mb-3 list-disc space-y-1 pl-5 text-gray-700 text-sm">
<li>Page URL and referrer</li>
<li>Browser name and version</li>
<li>Operating system name and version</li>
<li>Device type, brand, and model</li>
<li>
City, country, and region (derived from IP at the time of the
request; IP is then discarded)
</li>
<li>Custom event properties the Controller chooses to send</li>
</ul>
<p className="mb-3 text-gray-700 text-sm">
No persistent identifiers, no cookies, no cross-site tracking.
Because of this approach, the analytics data OpenPanel collects in
standard website tracking mode does not constitute personal data
under GDPR Art. 4(1). We provide this DPA for Controllers who
require it for their own compliance documentation and records of
processing activities.
</p>
<p className="mb-1 text-gray-700 text-sm font-semibold">
Session replay (optional feature)
</p>
<p className="text-gray-700 text-sm">
OpenPanel optionally supports session replay, which must be
explicitly enabled by the Controller. When enabled, session replay
records DOM snapshots and user interactions (mouse movements, clicks,
scrolls) using rrweb. All text content and form inputs are masked by
default. The Controller is responsible for ensuring their use of
session replay complies with applicable privacy law, including
providing appropriate notice to end users.
</p>
</Section>
<Section number="3" title="Scope and roles">
<p className="text-gray-700 text-sm leading-relaxed">
OpenPanel acts as a Processor when processing data on behalf of the
Controller. The Controller is responsible for the analytics data
collected from visitors to their websites and applications.
</p>
</Section>
<Section number="4" title="Processor obligations">
<p className="mb-2 text-gray-700 text-sm">
OpenPanel commits to the following:
</p>
<ul className="list-disc space-y-1 pl-5 text-gray-700 text-sm">
<li>
Process Personal Data only on the Controller's documented
instructions and for no other purpose.
</li>
<li>
Ensure that all personnel with access to Personal Data are bound
by appropriate confidentiality obligations.
</li>
<li>
Implement and maintain technical and organizational measures in
accordance with Section 7 of this DPA.
</li>
<li>
Not engage a Sub-processor without prior general or specific
written authorization and flow down equivalent data protection
obligations to any Sub-processor.
</li>
<li>
Assist the Controller, where reasonably possible, in responding to
Data Subject requests to exercise their rights under GDPR.
</li>
<li>
Notify the Controller without undue delay (and no later than 48
hours) upon becoming aware of a Personal Data breach.
</li>
<li>
Make available all information necessary to demonstrate compliance
with this DPA and cooperate with audits conducted by the
Controller or their designated auditor, subject to reasonable
notice and confidentiality obligations.
</li>
<li>
At the Controller's choice, delete or return all Personal Data
upon termination of the service.
</li>
</ul>
</Section>
<Section number="5" title="Controller obligations">
<p className="mb-2 text-gray-700 text-sm">
The Controller confirms that:
</p>
<ul className="list-disc space-y-1 pl-5 text-gray-700 text-sm">
<li>
They have a lawful basis for the processing described in this DPA.
</li>
<li>
They have provided appropriate privacy notices to their end users.
</li>
<li>
They are responsible for the accuracy and lawfulness of the data
they instruct OpenPanel to process.
</li>
</ul>
</Section>
<Section number="6" title="Sub-processors">
<p className="mb-3 text-gray-700 text-sm">
OpenPanel uses the following sub-processors to deliver the service:
</p>
<table className="mb-3 w-full border-collapse text-sm">
<thead>
<tr className="border border-gray-300 bg-gray-50">
<th className="border border-gray-300 px-3 py-2 text-left font-semibold">
Sub-processor
</th>
<th className="border border-gray-300 px-3 py-2 text-left font-semibold">
Purpose
</th>
<th className="border border-gray-300 px-3 py-2 text-left font-semibold">
Location
</th>
</tr>
</thead>
<tbody>
<tr>
<td className="border border-gray-300 px-3 py-2">
Hetzner Online GmbH
</td>
<td className="border border-gray-300 px-3 py-2">
Cloud infrastructure and data storage
</td>
<td className="border border-gray-300 px-3 py-2">
Germany (EU)
</td>
</tr>
<tr>
<td className="border border-gray-300 px-3 py-2">
Cloudflare R2
</td>
<td className="border border-gray-300 px-3 py-2">
Backup storage
</td>
<td className="border border-gray-300 px-3 py-2">EU</td>
</tr>
</tbody>
</table>
<p className="text-gray-700 text-sm">
OpenPanel will inform the Controller of any intended changes to this
list with reasonable notice, giving the Controller the opportunity
to object.
</p>
</Section>
<Section number="7" title="Technical and organizational measures">
<div className="space-y-4 text-gray-700 text-sm">
<div>
<p className="mb-1 font-semibold">
Data minimization and anonymization
</p>
<ul className="list-disc space-y-1 pl-5">
<li>
IP addresses are never stored. They are used only to derive
geolocation and generate an anonymous daily identifier, then
discarded.
</li>
<li>
Daily-rotating cryptographic salts ensure visitor identifiers
cannot be reversed or linked to individuals after 24 hours.
</li>
<li>
No cookies or persistent cross-device identifiers are used.
</li>
</ul>
</div>
<div>
<p className="mb-1 font-semibold">Access control</p>
<ul className="list-disc space-y-1 pl-5">
<li>
Dashboard access is protected by authentication and role-based
access control.
</li>
<li>
Production systems are accessible only to authorized
personnel.
</li>
</ul>
</div>
<div>
<p className="mb-1 font-semibold">
Encryption and transport security
</p>
<ul className="list-disc space-y-1 pl-5">
<li>All data is transmitted over HTTPS (TLS).</li>
</ul>
</div>
<div>
<p className="mb-1 font-semibold">
Infrastructure and availability
</p>
<ul className="list-disc space-y-1 pl-5">
<li>
All data is hosted on Hetzner servers located in Germany
within the EU.
</li>
<li>Regular backups are performed.</li>
<li>
No data leaves the EEA in the course of normal operations.
</li>
</ul>
</div>
<div>
<p className="mb-1 font-semibold">Incident response</p>
<ul className="list-disc space-y-1 pl-5">
<li>
We maintain procedures for detecting, reporting, and
investigating Personal Data breaches.
</li>
<li>
In the event of a breach affecting the Controller's data, we
will notify them within 48 hours of becoming aware.
</li>
</ul>
</div>
<div>
<p className="mb-1 font-semibold">Open source</p>
<ul className="list-disc space-y-1 pl-5">
<li>
The OpenPanel codebase is publicly available on GitHub,
allowing independent review of our data handling practices.
</li>
</ul>
</div>
</div>
</Section>
<Section number="8" title="International data transfers">
<p className="text-gray-700 text-sm leading-relaxed">
OpenPanel stores and processes all analytics data on Hetzner
infrastructure located in Germany. No Personal Data is transferred
to countries outside the EEA in the course of delivering the
service.
</p>
</Section>
<Section number="9" title="Data retention and deletion">
<ul className="list-disc space-y-1 pl-5 text-gray-700 text-sm">
<li>
<strong>Analytics events</strong> are retained for as long as the
Controller's account is active. No maximum retention period is
currently enforced. If a retention limit is introduced in the
future, all customers will be notified in advance.
</li>
<li>
<strong>Session replays</strong> are retained for 30 days and then
permanently deleted.
</li>
<li>
The Controller can delete individual projects, all associated data,
or their entire account at any time from within the dashboard. Upon
account termination, OpenPanel will delete the Controller's data
within 30 days unless required by law to retain it longer.
</li>
</ul>
</Section>
<Section number="10" title="Governing law">
<p className="text-gray-700 text-sm leading-relaxed">
This DPA is governed by the laws of Sweden and is interpreted in
accordance with the GDPR.
</p>
</Section>
{/* Exhibit A */}
<div className="mb-8 border-black border-t-2 pt-8">
<p className="mb-1 text-gray-500 text-xs uppercase tracking-widest">
Annex
</p>
<h2 className="mb-4 font-bold text-xl">
Exhibit A: Description of Processing
</h2>
<table className="w-full border-collapse text-sm">
<tbody>
<Row
label="Nature of processing"
value="Collection and storage of anonymized website analytics events (page views, custom events, session data). Optionally: session replay recording of DOM snapshots and user interactions."
/>
<Row
label="Purpose of processing"
value="To provide the Controller with website and product analytics via the OpenPanel Cloud dashboard. Session replay (if enabled) is used to allow the Controller to review user sessions for UX and debugging purposes."
/>
<Row
label="Duration of processing"
value="Analytics events: retained for the duration of the active account (no current maximum). Session replays: 30 days, then permanently deleted. All data deleted within 30 days of account termination."
/>
<Row
label="Categories of data subjects"
value="Visitors to the Controller's websites and applications"
/>
<Row
label="Categories of personal data"
value="Anonymized session identifiers (non-reversible after 24 hours), page URLs, referrers, browser type and version, operating system, device type, city-level geolocation (country, region, city). No IP addresses, no cookies, no names, no email addresses. If session replay is enabled: DOM snapshots and interaction recordings, which may incidentally contain personal data visible on the Controller's pages. All text content and form inputs are masked by default."
/>
<Row
label="Special categories of data"
value="None intended. The Controller is responsible for ensuring no special category data is captured via session replay."
/>
<Row
label="Sub-processors"
value="Hetzner Online GmbH (Germany) cloud infrastructure; Cloudflare R2 (EU) backup storage"
/>
</tbody>
</table>
</div>
{/* Signatures */}
<div className="border-black border-t-2 pt-8">
<p className="mb-1 text-gray-500 text-xs uppercase tracking-widest">
Execution
</p>
<h2 className="mb-6 font-bold text-xl">Signatures</h2>
<div className="grid grid-cols-2 gap-12">
{/* Processor - pre-signed */}
<div>
<div className="col h-32 gap-2">
<p className="font-semibold text-gray-500 text-xs uppercase tracking-widest">
Processor
</p>
<p className="font-semibold text-sm">OpenPanel AB</p>
<p className="text-gray-500 text-xs">
Sankt Eriksgatan 100, 113 31 Stockholm, Sweden
</p>
</div>
<SignatureLine
label="Signature"
value={
<Image
alt="Carl-Gerhard Lindesvärd signature"
className="relative top-4 h-16 w-auto object-contain object-left"
height={64}
src="/signature.png"
width={200}
/>
}
/>
<SignatureLine label="Name" value="Carl-Gerhard Lindesvärd" />
<SignatureLine label="Title" value="Founder" />
<SignatureLine label="Date" value="March 3, 2026" />
</div>
{/* Controller - blank */}
<div>
<div className="flex flex-col h-32 gap-2">
<p className="font-semibold text-gray-500 text-xs uppercase tracking-widest">
Controller
</p>
</div>
<SignatureLine label="Company" value="" />
<SignatureLine label="Signature" value="" />
<SignatureLine label="Name" value="" />
<SignatureLine label="Title" value="" />
<SignatureLine label="Date" value="" />
</div>
</div>
</div>
<div className="mt-12 border-gray-200 border-t pt-6 text-center text-gray-400 text-xs print:mt-4">
OpenPanel AB &middot; hello@openpanel.dev &middot; openpanel.dev/dpa
</div>
</div>
</div>
);
}
function Section({
number,
title,
children,
}: {
number: string;
title: string;
children: React.ReactNode;
}) {
return (
<div className="mb-8">
<h2 className="mb-3 font-bold text-base">
{number}. {title}
</h2>
{children}
</div>
);
}
function Row({ label, value }: { label: string; value: string }) {
return (
<tr className="border border-gray-300">
<td className="w-48 border border-gray-300 bg-gray-50 px-3 py-2 align-top font-semibold text-xs">
{label}
</td>
<td className="border border-gray-300 px-3 py-2 text-xs leading-relaxed">
{value}
</td>
</tr>
);
}
function SignatureLine({
label,
value,
}: {
label: string;
value: string | React.ReactNode;
}) {
return (
<div className="mb-3">
<p className="text-gray-500 text-xs">{label}</p>
<div className="mt-1 flex h-7 items-end border-gray-400 border-b font-mono">
{value}
</div>
</div>
);
}

View File

@@ -124,6 +124,7 @@ export async function Footer() {
<Link href="/sitemap.xml">Sitemap</Link>
<Link href="/privacy">Privacy Policy</Link>
<Link href="/terms">Terms of Service</Link>
<Link href="/dpa">DPA</Link>
<Link href="/cookies">Cookie Policy (just kidding)</Link>
</div>
</div>