Privacy Policy
Last updated: August 19, 2026
Last updated: 2026-08-21. This version is in force, and it is the version an account created on or after that date records agreement to.
This is the privacy policy for Harbor: the accessibility widget, the customer account system, and the Harbor website. It replaces a shorter policy that covered only the website and said nothing about the widget.
Most privacy policies are written to be defensible. This one is written to be checkable. Where a promise here is enforced by code rather than by our good intentions, the file that enforces it is named, so that the claim can be verified rather than believed. Where something is not built yet, it says so in those words rather than being described in the present tense — and where a statement about our own website could be checked with a browser rather than argued about, it was.
PRIVACY.md in the widget's repository is the technical companion to this
document: it describes the widget's network request in more detail than a
site-wide policy can. Where the two documents differ in specificity, that one
is the more precise account of the widget, and this one does not override it.
If either disagrees with the code, the code is the bug.
Find your own case first
There are three different relationships with Harbor and they involve different data. Read the one that is yours.
| You are | Go to | The short version |
|---|---|---|
| A visitor to a website that has Harbor installed. You did not choose us. | Section 1 | Your display settings stay in your own browser. We receive one request that tells us which site you are on, never which page, and we build no profile of you. |
| A Harbor customer with an account. | Section 2 | Your email address, the URLs of your sites, your plan, and — once scanning is live — what we found on your sites and what was changed. |
| A visitor to harboraccessibility.com. | Section 3 | Nothing is put on your device unless you open the booking widget yourself. What the contact form and that widget collect is set out there, measured in a browser rather than described from the code. |
Who we are
Harbor is operated by Harbor Accessibility Co.
- Registered address: 502 W 7th St STE 100, Erie, PA 16502, United States.
- Country of establishment and governing law: the United States. Harbor Accessibility Co. is a single-member limited liability company organised under the laws of the Commonwealth of Pennsylvania, and this policy is read under Pennsylvania law. ("Co." is the entity designator rather than a missing "LLC" — Pennsylvania permits it.)
- Privacy contact:
privacy@harboraccessibility.com. It reaches a person, which is the only reason to print an address in a document like this. - EU / UK representative under GDPR Article 27: none appointed, and the reason is stated rather than left blank. We are established only in the United States. Article 27 follows Article 3(2), which turns on whether a service is offered to people in the EU or UK, and Recital 23 says in terms that a website merely being reachable from the Union is not that. Today Harbor is priced only in US dollars, published only in English, contactable only at a US address and a US phone number, and the pages here that discuss the European Accessibility Act are addressed to US businesses selling into Europe rather than to European ones. We do not block anyone from signing up, we count where our customers actually are, and if that reading stops holding we will appoint representatives — two of them, because the UK has its own Article 27 — and say so here. If you are in the EU or UK, the address above reaches the same person either way.
- Data Protection Officer: none appointed. The Article 37 triggers are being a public authority, large-scale regular and systematic monitoring, or large-scale processing of special-category data. None of the three describes an account system holding an email address and a list of website names. If that changes, so does this line.
1. If you visited a website that uses Harbor
You never chose Harbor. A site you visited installed it, and a script from
cdn.harboraccessibility.com ran in your browser. This section is what that
means for you, and it is the one we have taken the most trouble over.
Your settings never leave your browser
Contrast, text size, line and letter spacing, motion, link and focus
visibility, the reading guide, the reader typeface: every choice you make in
the Harbor panel is written to localStorage in your own browser, under the
website's own domain, and is read back from there on your next visit.
It is never transmitted to us. We do not know whether you turned on high
contrast, and we could not find out. This is not a policy commitment we could
quietly reverse — it is how the widget is built (src/core.js reads and writes
the key harbor.v1; nothing in src/remote.js or src/analytics.js ever
reads it).
Because that storage belongs to the site's domain and not to ours, your settings also do not follow you between websites. Set high contrast on one site and the next site starts fresh. That is a real cost of not tracking you, and it is the honest reason to use your operating system's or browser's own accessibility settings where they exist — Harbor follows those by default.
The one request your browser makes to us
On each page view the widget asks our server which version to load and how the site has been configured:
GET https://api.harboraccessibility.com/v1/site/<site key>
That is the whole request. It carries:
- the site key, which identifies the website, not you. Every visitor to the same site sends the identical key.
- no cookies. The request is made with
credentials: "omit", we send noSet-Cookieand noAccess-Control-Allow-Credentialson it, so this route cannot set or return a cookie even by mistake. - no page URL, no page title, no referrer, beyond what your browser's own
Originheader carries — which is the site's domain, not the page you are reading. - nothing about your preferences.
What we keep from it: the site key and the host from the Origin
header — example.com, never a path, never a full URL. That is a fact about
a website, not about you. It is how we count how many sites are running Harbor
and how many are still running it next month. The function that does this is
originHost in edge/src/worker.js, and it takes new URL(origin).host and
nothing else; the record is written by recordInstall in the same file.
What a request unavoidably reveals, whatever we intend: your IP address and User-Agent reach our server, because that is how HTTP works. We do not log them, no feature reads them, and nothing we store contains them — but a request we receive is a request we could log, and telling you otherwise would be a promise about our conduct rather than about the software. Our hosting provider handles them at the network layer as part of delivering the request; see Sub-processors.
One place where that promise is mechanical rather than verbal: the flood
protection on our analytics endpoint is keyed on the account, not on the
caller's IP address. The obvious way to build a rate limiter is to key it on
CF-Connecting-IP, which our platform hands us for free — and a rate limiter
is exactly the sort of place where "we do not take the IP" quietly acquires an
exception nobody writes down. So it does not. See withinLimit in
edge/src/worker.js and the EVENT_LIMIT block in edge/wrangler.toml. The
cost of that choice is stated there too: a flood aimed at one site also spends
that site's own budget for the minute, and we accepted that in exchange for not
taking an IP.
The same rule is applied to the sign-in rate limiter, which is keyed on a
hashed email address rather than an IP (withinAuthLimit, edge/src/accounts.js).
Usage analytics: off unless the site owner turns it on
If — and only if — the site owner has an enabled plan and has switched usage analytics on, the widget reports which controls were used. It is off on the free tier and off by default.
When it is on, batched events are sent as the page is being left:
POST https://api.harboraccessibility.com/v1/events
{ "account": "site_…", "session": "<random, per-tab>",
"events": [ { "n": "setting_change", "k": "contrast", "v": "hc-dark", "t": … } ] }
- The event name comes from a fixed list of five —
panel_open,panel_close,setting_change,preset_apply,reset. A name outside that list is dropped by the widget and dropped again by the server, in two separate files (ALLOWEDinsrc/analytics.js,ALLOWED_EVENTSinedge/src/worker.js). - There is no free-text field anywhere in the payload. Setting values are
passed through
safeValue, which permits a short alphanumeric token and turns anything else into the literal stringother. This is deliberate: it means no text you typed into a page can leave by this route even by accident. - The per-tab session id is discarded on ingest, not stored. It exists so a
batch can be de-duplicated in flight. It is random, lives only in memory, and
is regenerated on every page load, so it cannot join two of your visits and
cannot follow you between sites. The server-side write (
recordEventsinedge/src/worker.js) simply does not include it — the field is absent from the stored row, with a comment saying why.
Never sent, in any configuration: the URL, the path, the page title, the referrer, your screen size, any location derived from your IP, any device fingerprint, or any value you typed.
What does not exist
- No cross-site profile. The site key is per site, so activity on one customer's website cannot be joined to another's — there is no identifier in common to join it on.
- No advertising identifier, no ad network, no data broker, no sale of personal information, and no sharing for cross-context behavioural advertising. None of that exists in the product, and we do not have a business model that would want it.
- No cookie from the widget, on any site. See Cookies and browser storage.
What that means for your rights
We hold nothing that identifies you as a visitor, so there is nothing of yours for us to show you or delete. That is a genuine benefit and also a genuine limitation, and both should be said: if you asked us to find your data, we would have no way of finding it, because there is no identifier in our stores that could ever point at one person.
If that ever stops being true, this policy changes first.
If you want the widget itself to do nothing: clear the site's storage in your browser, or use your browser's own accessibility and privacy settings. With nothing stored, the widget applies no styles at all.
For site owners installing Harbor
Installing Harbor means your visitors' browsers contact
api.harboraccessibility.com on every page view. Under GDPR and similar
regimes that is a processing activity you are responsible for disclosing,
and you should assume it belongs in your own privacy policy. We would rather
you over-disclose than discover later that we let you under-disclose.
2. If you have a Harbor account
What an account contains
An account is deliberately thin. Signup asks for two things — an email address
and a site URL — because everything else is better detected than reported. The
stored record (handleAuthStart in edge/src/accounts.js) is:
| Field | What it is |
|---|---|
email |
The address you signed up with, lowercased and trimmed. |
plan |
Your tier. Defaults to free. |
sites |
For each site: the site key we minted, the site's URL, and the date. |
signupUrl |
The URL you entered at signup. |
created |
The date the account was created. |
id |
An internal account id, never shown outside the dashboard. |
consent |
What you agreed to and what you were shown: the dated versions of the Service Terms and this policy, the timestamp, and your answer to the marketing question — including when the answer was no — with the exact wording of the question you were asked. |
What is deliberately not on that record: where you are. Our servers are told the country a request came from — our host works it out at the edge, and it arrives alongside a city, coordinates and a network operator we do not read. We need to know roughly where our customers are, for a reason set out under Who we are: whether we are obliged to appoint a European representative turns on whether we are in fact serving Europe, and that should be a number rather than an impression.
The obvious way to get that number is a country field on the record above. We
did not do that. Instead, when an account is created, a single country code is
written to a separate counting stream that carries no account id, no address,
no hash and no URL — one country and a 1, with nothing in the row that could
join it back to you or to the table above. Signing in afterwards adds nothing,
because a count of customers and a count of visits are different things to hold.
recordSignupRegion in edge/src/accounts.js is the whole of it, and
edge/test/accounts.test.mjs asserts what the row is not allowed to contain.
There is no password, because sign-in is by emailed link. That removes password storage, password reset, credential stuffing, and the whole class of breach that follows from any of them. It also means your mailbox is the key to your account: if someone else can read your email, they can sign in as you.
How the credentials are stored
- The email index is a peppered hash, not the address. We need to look an
account up by email address at sign-in. The lookup key is
SHA-256(secret pepper + address), and the pepper is a deployment secret that is not in the repository (emailKey,edge/src/accounts.js). A plain hash of an email address is reversible with a wordlist and an afternoon — an unpeppered index would be a mailing list wearing a hash. A dump of our key-value store alone does not reconstruct the address list. The address itself does live, in plain form, in the account record, which is only ever read by an authenticated request. - Sign-in links and sessions are stored hashed. What is stored is
SHA-256(token), which is enough to check a credential a caller already holds and not enough to mint one. A listing of our stored sessions is not a set of usable sessions. - A sign-in link expires in 15 minutes and may be opened at most five times, and is deleted from our store before a session is issued.
- We never confirm whether an address has an account. Both
/v1/signupand/v1/signinanswer202in every case — known address, unknown address, malformed address, rate-limited, mail provider down. "Does this address have an account here" is exactly the question a phishing list wants answered, so the endpoint does not answer it.
The session cookie
One cookie exists in the entire product. See Cookies and browser storage.
Scanning, findings, and the remediation log
Harbor's paid product scans your sites for accessibility problems, drafts fixes, and keeps a dated record of what was changed and who approved it. For customers who use it, that means we hold:
- The pages of your sites that we fetched, and what we found on them.
- Drafted fixes, and which of them you approved.
- The remediation log: what was wrong, what was done, who approved it, and when — exportable by you.
This is data about your website, and it will routinely contain content from your pages. Where a page of yours contains personal data, our copy of that finding contains it too.
A scan requires an account, and every scan is attributed to one. We do not offer anonymous scanning of a URL a stranger types in. Two of the reasons are about you and one is about us: an anonymous scanner is a crawler anybody could aim at anybody else's website from our address, and it has no key we could rate limit it on that is not an IP address — and every limiter in this service is deliberately keyed on an account or a hashed email rather than an IP. The cost is that we ask for an email address before showing you anything, which is more friction than the alternative. What we will not do is gate the findings: the account is free, takes no password and no card, and every finding is shown in full.
Status, stated plainly: the scanning and remediation service is operated by Harbor as a separate system from the widget described in Section 1, and it is not yet connected to the account system described above. No customer scan data is being collected through this account system today. This paragraph gets filled in before that changes, not after. When scanning goes live for customers it will say where the scan service runs, what it stores, how long it keeps it, and every additional sub-processor that receives page content — including any model provider used to draft a fix, which is a sub-processor like any other and gets named like one. We would rather leave a visible gap here than describe a system before it exists.
Emails we send you
- The sign-in link. Plain text, no tracking pixel, no HTML — an HTML
sign-in mail is the exact visual grammar of a phishing mail, and it is the
one message we send to someone who has no relationship with us yet
(
edge/src/mailer.js). It is sent through Postmark's transactional stream. - Scan alerts and reports, on the plans that include them.
- Marketing email — only if you asked for it. There is an unticked box at signup, separate from agreeing to the terms, and an account is created whether or not you tick it. We store what you chose, when you chose it and the exact wording you were shown, including when the answer was no: proving a box was left unticked is the same evidence problem in reverse. Every marketing message carries a one-click unsubscribe and our postal address, and withdrawing is as easy as giving it was. As of this version we send none, which is why the box exists now rather than later — consent is free to collect at signup and impossible to retrofit onto a list gathered without it.
A failure to send is logged with the provider's status code and never with the address — a server log line is not a place where the promises in this policy stop applying.
3. If you visited harboraccessibility.com
The marketing website is a separate thing from the widget and the dashboard, and until this version it was the part of this policy we could verify least. That has been fixed, and not by reading more code.
What the site does to your browser is measured, not described. The five
paragraphs below used to be five placeholders, on the grounds that the marketing
site lives in a different repository from the one this policy was drafted
against. Reading that repository turned out not to be the answer either: what
decides this section is what a real browser is actually holding after a real
page load, and two of the things it holds do not appear in the source at all —
one is injected by our host, and one was arriving through a third party's script
that the source merely referenced. scripts/storage-probe.mjs in the site's
repository loads a page, waits for the network to go quiet, and prints every
cookie, every storage key and every host contacted. What follows is its output.
On an ordinary page view, before you touch anything: nothing of ours is
written to your device — no cookie, no localStorage, no sessionStorage — and
the only hosts your browser contacts are our own origin and our host's analytics
beacon, described immediately below. The two web fonts are served from our
origin too; they used to come from Google Fonts, which meant Google saw your IP
address on every page load, and the policy said that did not happen, so it no
longer does.
Two things come from our host rather than from the page, and they are still ours to disclose:
- Cloudflare Web Analytics. Cloudflare injects a small beacon
(
static.cloudflareinsights.com) into the pages it serves for us. It counts page views and performance timings, it sets no cookie — measured, not assumed — and it cannot follow you to another site. Cloudflare is already our host and is named under Sub-processors. cf_clearance, which Cloudflare sets when its bot protection has challenged a request, to remember that the challenge was passed. That is about as strictly necessary as a cookie gets.
The contact form. The form at /contact posts to Formspree, which
delivers it to us as email and keeps a copy in our account there. It carries
what you typed and nothing added: your name, your email address, your message,
plus your organisation and your site's URL if you filled those in, and the
subject of the topic you arrived from. We clear handled enquiries out of
Formspree by hand, and nothing stays there beyond twelve months.
Booking a call. The Calendly widget loads when you click a button asking
to book a call — not before. That is deliberate and it is recent: until
2026-08-19 the widget's script sat in the site's <head> and loaded on every
page, which handed Calendly your IP address and the page you were reading
whether or not you had any intention of booking, and left one of their cookies
on your device. Now it waits for the click (src/calendly.ts in the site's
repository).
When you do click, you are handed to Calendly, and Calendly does not travel light. Opening the booking widget pulls in their own stack — a consent tool, an analytics pipeline, an error reporter, a payments script, a CAPTCHA — and sets their cookies on your device. We do not control that and will not pretend otherwise. What we control is that none of it happens until you ask for it, and that the button works as a plain link to Calendly if their script never arrives. What Calendly receives from an actual booking is your name, your email address, your time zone, and whatever you type into their form.
Website analytics beyond the beacon: none. No Google Analytics, no product analytics, no session recording, no heatmaps, no A/B testing tool.
Advertising and conversion tracking: none — and there was some. Until
2026-08-19 a Google Ads tag loaded on every page of this site and, before any
interaction at all, wrote _gcl_au and _gcl_ls to your device and set an IDE
cookie on doubleclick.net: an advertising identifier with a two-year expiry.
The first-touch attribution capture behind it, which stored click ids and
campaign tags in your browser indefinitely, is gone as well. Both were removed
rather than consented into, because the widget in Section 1 promises no
advertising ever and two Harbor properties should not need a paragraph
explaining why they disagree.
A cookie banner: not needed, and that is a measurement rather than an opinion. A banner is owed when a site puts something on your device that is not strictly necessary. On this site nothing is put on your device at all until you deliberately open a third party's booking widget. If we add anything that changes that, the banner arrives in the same deploy.
The widget's guarantees in Section 1 are not affected by any of this. The widget and the marketing site share a brand and nothing else: no identifier is common to both, and no data from one can be joined to the other.
Cookies and browser storage
The distinction between a cookie and browser storage matters here, so it is made rather than blurred.
Cookies: there is exactly one in the whole product.
| Name | Where | What it is | Lifetime |
|---|---|---|---|
__Host-harbor_session |
app.harboraccessibility.com (the dashboard) only |
Your signed-in session. Strictly necessary — without it you cannot stay signed in. | 30 days |
It is Secure, HttpOnly, SameSite=Lax, Path=/, and carries no Domain
attribute, which makes it host-only. That is what the __Host- prefix
enforces, and the browser refuses the cookie outright if any of those three
properties is missing — so the mistake cannot ship quietly. Host-only is the
point: a cookie scoped to .harboraccessibility.com would be attached to
every widget request from every installed page, which would hand us
something on every page view that we have gone to considerable trouble not to
have. See sessionCookie in edge/src/accounts.js.
The widget sets no cookies at all, on any site, in any configuration.
Browser storage used by the widget, written under the website's own domain and never transmitted to us:
| Key | What it holds | Why |
|---|---|---|
harbor.v1 |
Your display preferences on that site. | So your settings survive to your next visit, and are applied before the page first paints. |
harbor.site.v1 |
A cached copy of the site's Harbor configuration. | So the widget still works if our server is slow or unreachable, and so a returning visitor is not waiting on our network. It contains configuration for the site — no information about you. |
Both are readable and clearable by you, in your browser's own settings, like any other site storage.
Our position, which a lawyer should confirm rather than inherit: both keys are "strictly necessary" under ePrivacy / PECR Article 5(3), so a site owner installing Harbor does not need a consent banner for them. One stores a setting the visitor explicitly asked for; the other is a cache of the configuration needed to deliver that same setting when our server is slow or unreachable. Neither is used for any purpose beyond that, neither can be read by us, and neither identifies anybody. This one determines whether every customer needs a banner, so it is the one point on this page we would most rather have checked by someone other than us.
Sub-processors
Third parties that process data on our behalf.
| Sub-processor | What it does | What it receives |
|---|---|---|
| Cloudflare (Cloudflare, Inc., United States) | Hosting, CDN, key-value storage, Workers Analytics Engine. Everything in Sections 1 and 2 runs here, and it serves the marketing site and injects the cookieless beacon described in Section 3. | Everything described in this policy: the site-record requests, the usage-analytics events, account records, sessions. As the network layer, it necessarily handles visitor IP addresses in order to deliver requests. |
| Postmark, operated by ActiveCampaign, LLC (United States) | Transactional email only — the sign-in link. | The recipient's email address and the sign-in link. Nothing else; it is not used for marketing email or for any other message. |
| Formspree (Formspree, Inc., United States) | The contact form on the marketing site (Section 3). | What you typed into that form: your name, your email address, your message, and your organisation and site URL if you gave them. |
| Calendly (Calendly LLC, United States) | Booking a call from the marketing site — and only from the click that asks to book one (Section 3). | Your name, your email address, your time zone, and whatever you enter in their form. Their widget also loads their own third parties, which is described in Section 3 rather than hidden in this table. |
Verified in edge/wrangler.toml (the KV namespace, the harbor_installs and
harbor_events Analytics Engine datasets, the three Cloudflare-routed
hostnames), edge/src/mailer.js (Postmark is the only mail provider in the
code, decided 2026-08-18), and — for the last two rows — by loading the
marketing site in a browser and reading back every host it contacted.
Live since 2026-08-19: Postmark is configured in production and sign-in
links are sent through it. https://api.harboraccessibility.com/v1/health
reports whether the account system and the mailer are actually up, which is a
better answer than this sentence.
International transfers. Every sub-processor above is in the United States, and so are we. If you are in the UK or EU, using Harbor means a transfer out, and the mechanism is the standard one in each case: Cloudflare, ActiveCampaign (for Postmark), Formspree and Calendly each publish a data processing agreement incorporating the European Commission's Standard Contractual Clauses, with the UK Addendum for UK transfers. Account records live in Cloudflare's key-value store, which is distributed by design — a record is replicated to the edge locations that ask for it — so the honest answer to "where does it rest" is "on Cloudflare's network" rather than the name of one country.
Not engaged yet, and named here before they are. There is no payment processor, because no paid plan can be bought at all (see the Terms of Service). There is no sub-processor for scanning and remediation, because that service is not running for customers. Any model provider that receives customer page content in order to draft a fix would be a sub-processor like any other, and would appear in this table before it received anything.
We will keep this list current. Adding a sub-processor that receives customer personal data is a change we will notify, under Changes to this policy.
The aggregate accessibility dataset
We intend to publish an annual, aggregate, de-identified dataset and report on the state of web accessibility, drawn from what our scans find. This is disclosed here, in version one of this policy, on purpose: a permission of this kind cannot honestly be retrofitted later without going back and asking every existing customer again, so it is stated before the first account exists rather than added quietly afterwards.
What that means, precisely:
- Aggregate and de-identified only. Findings are counted, grouped and reported as statistics — "n% of sites in this category had unlabelled images", not "this site had unlabelled images".
- Individual customer sites are never named, never listed, and never identifiable from the published figures, whether directly or by combining published figures with each other.
- No page content is published. Not URLs, not screenshots, not extracted text.
- Nothing about your visitors is in it. The dataset is built from findings about websites, and the widget's visitor-side data — which, as Section 1 explains, does not identify anyone — is not part of it.
Why we want it: the existing public research covers the top million home pages. Nobody has measured the long tail of small sites, which is the part that actually gets sued and the part we work on. An annual aggregate is the most useful artifact this company could produce for the field, and it is a by-product of work we are already doing.
And you can decline it. A de-identified aggregate does not legally require
an opt-out, and there is one anyway, because a company whose entire argument is
"we tell you what we do" is a poor place to put a permission you cannot refuse.
Email privacy@harboraccessibility.com and your sites are excluded. Today that
is a flag we set by hand; later it is a switch in the dashboard. It is promised
now rather than once there is volume, precisely because honouring it by hand
costs nothing while there is none. Section 11 of the Terms of Service carries
the same commitment.
Change of control
If Harbor Accessibility Co. is merged, acquired, or sells substantially all of its assets, customer accounts and the data described in Section 2 transfer to the acquirer, who would be bound by this policy in respect of that data until they give you notice of a change.
This is stated plainly for the same reason as the clause above it: a business that says nothing about it and then transfers customer data anyway has misled its customers, and one that promises never to transfer it has made a promise it cannot keep through an acquisition. So it is here in version one.
The visitor-side data in Section 1 is not personal data and there is nothing in it to transfer that identifies anybody.
Notice, and when. We will tell you without undue delay after a change of control takes effect — by email to your account address and on the dashboard — and you then have 30 days to export your data and close the account.
Notice afterwards rather than beforehand is a deliberate choice, and it is worth saying why out loud. An obligation to warn an entire customer base before a deal closes collides with the confidentiality every deal has, and a clause that made the customer relationship awkward to transfer would be working against the reason this paragraph exists at all. What you are actually owed is to find out, and to be able to leave with your data. That is what this gives you.
Section 17 of the Terms of Service says this in the same words. If you ever find those two paragraphs disagreeing, the disagreement is the bug.
How long we keep things
| Category | Kept for | Source |
|---|---|---|
| Sign-in link (magic token) | 15 minutes, and deleted the moment it is used | MAGIC_TTL, edge/src/accounts.js |
| Session | 30 days from sign-in, then expires automatically | SESSION_TTL, edge/src/accounts.js |
| Account record (email, plan, sites, created) | Until the account is deleted. No automatic expiry is set. | edge/src/accounts.js |
| Email lookup index (peppered hash) | Same as the account record | edge/src/accounts.js |
| Site record (the configuration a site's widget reads) | Until the site key is deleted. Nothing deletes one automatically. | edge/src/worker.js |
| Install records (site key + origin host) | Three months. Cloudflare's limit for Workers Analytics Engine, not a setting of ours: "data written to Workers Analytics Engine is stored for three months". The row holds a site key and a hostname and nothing about a person. It is also less than we want — this stream is meant to support a multi-year survival curve, so a durable daily rollup has to exist before the early months age out. | edge/wrangler.toml |
| Usage-analytics events | Three months, the same platform limit, on rows that carry no visitor identifier. | edge/wrangler.toml |
| Signup country counts | Three months, the same platform limit. One country code per new account, with nothing in the row that identifies the account — see Section 2. | edge/wrangler.toml, edge/src/accounts.js |
| Sent transactional email | 45 days in Postmark, which is its default retention for message content and activity. It is adjustable from 7 to 365 days as a paid add-on; we have not changed it. | — |
| Server logs | We log errors, and deliberately never the recipient address. We run no Logpush job and no tail consumer, so no request log is exported anywhere — checkable against the deployed Worker rather than on this sentence's word. Cloudflare's own Workers Logs retention is 3 days on the free plan and 7 on paid, and it is theirs to set rather than ours. | edge/src/mailer.js, edge/wrangler.toml |
| Scan findings and the remediation log | Not operating yet — see Section 2. When it is, the remediation log's retention will be stated as a positive choice, because its entire value to a customer is being a durable dated record, and a durable record should not inherit its lifetime from a default. | — |
| Marketing site form submissions | Delivered to us as email; the copy in Formspree is cleared by hand when the enquiry is closed, and nothing stays there beyond twelve months. | Section 3 |
What happens after you cancel
We delete or irreversibly anonymise your account data within 90 days of cancellation, except where one of the four carve-outs below applies. We may do it sooner, and you can ask us to.
Ninety days rather than thirty, deliberately, and the reason is stated because a retention promise you cannot keep is worse than a longer one you can. Data does not sit in one place: it is in our own store, in a transactional email provider's records, in platform telemetry, and in backups, each with its own schedule. A thirty-day promise would be broken on day one by systems we do not control. Ninety is an outer bound that every one of those fits inside.
Four things outlive that window, and each is here because it has a reason:
- Backups. Backups are written as whole snapshots and cannot have one record surgically removed. Deleted data persists in them until they age out in the ordinary cycle, and is restored only in a disaster, never to resume ordinary use.
- Records we are required to keep. Invoices, payment records and tax documents have statutory retention periods that override a deletion request.
- What we need to defend a claim. If there is a live dispute, we keep what relates to it until it is resolved.
- Aggregated and de-identified data. Statistics derived from scan findings are kept indefinitely, because they no longer identify you or your site. This is the same data described under the aggregate dataset above, and deleting an account does not withdraw a site's contribution to figures already published - which is a real limit and is stated here rather than left to be discovered.
The remediation log is deliberately durable. Its whole value to a customer is being a dated record of a good-faith programme, which is worth nothing if it evaporates on cancellation. We keep it for the retention period above and will export it to you on request before deletion.
Ninety days was checked against the systems that would have to honour it, rather than chosen because it sounded safe. Postmark's default retention for message content and activity is 45 days. Workers Analytics Engine is three months and holds no personal data — a site key and a hostname. Account records sit in a key-value store we delete from ourselves. All of those fit inside ninety days, which is why the number stayed where it was instead of quietly moving up.
One correction to a common assumption, made here because it would be easy to imply otherwise: usage-analytics events are stored as individual event rows (site key, event name, setting, value, timestamp) and aggregated when they are queried, rather than being aggregated at the moment they are written. There is no visitor identifier in a row, so no row can be attributed to a person — but "we only keep aggregates" would be the wrong description of the storage, and this is the accurate one.
Your rights, and what we can actually do today
If you are a customer, you have the right to access the personal data we hold about you, to have it corrected, to have it deleted, to receive an export of it, to object to or restrict certain processing, and — in the UK and EU — to complain to your data protection authority. In California you additionally have the right to know what is collected, to delete it, to correct it, and not to be discriminated against for exercising those rights.
How to exercise them: email privacy@harboraccessibility.com from the
address on your account. We will verify that you control that address —
which, given the account model, we do by sending a sign-in link to it — and
respond within 30 days. That window is deliberately tighter than either
regime requires: GDPR allows a month and California allows 45 days, and one
number is easier to keep than three.
What is honest about our current ability to do this:
- Access and export. The account data listed in Section 2 is what exists, and we can provide it. There is no self-serve export button yet; today this is a manual answer from a person.
- Correction. Manual today, for the same reason.
- Deletion — read this part. Self-serve deletion of a live site key does not exist. This is a deliberate omission recorded in our own design notes, not an oversight: deleting a site key removes the widget from a live web page, so it needs a confirmation step and a grace period, and both are easier to design correctly against one real account than against none. Until it exists, deletion is a request you make to us and we perform by hand. We will do it; you just cannot yet do it yourself at three in the morning, and pretending otherwise in a policy would be the kind of claim this document exists to avoid making.
- Visitors to customer sites. As Section 1 explains, we hold nothing that identifies you, so there is no record of yours for us to produce or erase. This is not a refusal — there is genuinely nothing there to find.
We do not sell personal information, and we do not share it for cross-context behavioural advertising, as those terms are used in California law. The change of control clause above is the one transfer of customer data this policy contemplates, and it is disclosed rather than buried.
Legal basis for processing (UK and EU)
Whether we are a controller or a processor for the widget's site-record request is the most consequential open question in this policy, so here is our answer rather than a blank. For the account system in Section 2 we are plainly the controller: we decide what an account holds and why. For the site-record request in Section 1 the site owner decides to install the widget and is the controller for that decision, and we are their processor for serving the configuration back — but the install count we derive from the very same request is ours, taken for our own business purposes, which makes us a controller for that narrow use and nobody's processor for it. When scanning goes live, the page content we fetch belongs to the customer and we are a processor for it.
That split is the most likely place for this policy to be wrong in a way that matters, because it decides who owes visitors a disclosure and whether customers are owed a data processing agreement. We would rather write it down and be corrected than leave the question open on a policy that is in force.
Our working position, for review:
| Processing | Basis | Note |
|---|---|---|
| Running your account, signing you in, delivering the service | Performance of a contract | Art. 6(1)(b) |
| Sending the sign-in link | Performance of a contract | It is the sign-in mechanism, not a message we chose to send |
| Counting live installs by site key and origin host (Section 1) | Legitimate interests — knowing whether our own product is in use | No personal data is in the record; the basis is stated for completeness, not because we think a website's hostname is a person |
| Counting the country a new account was opened from | Legitimate interests, and a legal-obligation question — knowing whether we are obliged to appoint an Article 27 representative | One country code, in a stream with no account id in it. The alternative was a location field on your account record, which would have been more precise about you and no more useful to us |
| Usage analytics, when a site owner enables it | Legitimate interests, with the site owner deciding | No visitor identifier is stored; see Section 1 |
| The aggregate dataset | Legitimate interests, over de-identified aggregates | Section 11 of the Terms of Service carries the matching permission, so this does not rest on legitimate interests alone, and the opt-out above stands whichever basis a reviewer prefers |
| Security and abuse prevention (rate limiting) | Legitimate interests | Keyed on account and on hashed email, never on IP |
A data processing agreement is available on request — email the privacy address above. Agencies are a named target segment for this business and will ask for one early, so the answer is yes rather than a negotiation.
Children
Harbor is a service for the operators of websites and is not directed at children. No part of it is directed at anyone under 16, which is the highest of the ages the regimes covering us use, so a single number satisfies all of them. We do not knowingly collect personal data from anyone under that age, and if we learn that we have, we delete it.
Security
Some of what protects your data is described above rather than repeated here, because it is structural rather than a control we bolted on: no passwords to steal, short-lived sign-in links, credentials stored hashed, an email index stored peppered, a host-only session cookie, and an authenticated surface that refuses to answer on any hostname but the dashboard's.
One rule is worth naming because it shapes the rest: a site key identifies
and never authorises. The key sits in the HTML of every page of every
installed site, so anyone who views source has it. It is never accepted as
proof of ownership of anything — the test suite presents a valid site key as a
credential and requires the request to be rejected
(edge/test/accounts.test.mjs).
No system is perfectly secure, and we do not claim ours is.
If a breach affects your personal data we will tell you without undue delay, and within 72 hours of confirming it, by email to your account address — what we know, what we do not yet know, and what we are doing about it. Seventy-two hours is the deadline GDPR sets for notifying a regulator; inventing a slower one for the people actually affected would be hard to defend and harder to explain.
Changes to this policy
We will update this policy when what we do changes. The Last updated date at the top always reflects the current version.
For a change that materially affects customers — a new category of data, a new purpose, a new sub-processor that receives personal data, or any change to the aggregate dataset or change of control sections — we will email account holders at the address on their account before it takes effect.
We will not use a policy update to grant ourselves permission over data collected before the update. If we ever want to do something with existing data that this policy does not allow, we will ask.
Material changes get 30 days' notice by email before they take effect — the same window as a change of control, so there is one number rather than two.
We keep every previous version of this policy, and we will send you any of
them on request. Ask at privacy@harboraccessibility.com; it falls under the
same 30-day response commitment as every other request in this policy. Your
account records the dated version you agreed to, so there is always a specific
document to ask for — and a consent record naming text nobody kept would be
worth as little as no record at all.
The claims in this document about what the software does can be checked against the files named beside them. If this policy and the code ever disagree, the code is what is happening and the policy is the bug — tell us and we will fix whichever one is wrong.