Security · Data handling
Where this runs, what it keeps, and for how long
This is the page a procurement form, a privacy officer or a careful customer wants before they put a code on 10,000 menus. It has no certification badges on it because we hold none. Every statement below describes the code that is actually deployed; where something is a limitation, it says so. The privacy policy is the legal document; this is the plain one.
Last reviewed against the deployed code on 9 September 2026.
Hosting and the services underneath
Named, so you can look each one up rather than take our word.
- The site and its API run on Cloudflare Pages and Pages Functions. There is no server of our own to patch; the code executes on Cloudflare’s network in response to each request.
- Account data, dynamic codes and scan records live in Cloudflare D1, a managed SQLite database. Images you upload for hosted pages (link-in-bio backgrounds and avatars) are stored in Cloudflare R2. We have not pinned either to a region: Cloudflare places the database at creation, and we cannot promise EU-only storage today. If that matters to you, say so before you rely on us.
- Sign-in is Firebase Authentication (Google), with an email-and-password account or a Google account. Your password is handled by Firebase and never reaches our code or our database. There is no two-factor option on email-and-password accounts; signing in with Google gives you Google’s.
- Payments are Stripe. Card numbers are entered on Stripe’s checkout page and never touch our servers. We store your Stripe customer and subscription identifiers so we can match a payment to a plan.
- Transactional email is sent through Brevo from support@qrcodeharbor.com. Marketing email carries a one-click unsubscribe; receipts and alerts about your own codes are transactional and are sent regardless.
- Everything is served over HTTPS with HSTS (one year, subdomains included, preload), a Content-Security-Policy that allows only hash-listed inline scripts,
X-Frame-Options: SAMEORIGINandX-Content-Type-Options: nosniff. API responses are sent withCache-Control: no-store. - Backups are Cloudflare’s point-in-time history for D1, which holds the previous 30 days. We keep no backup copies of our own. That cuts both ways: a row you delete can exist in that history for up to 30 days afterwards.
What is stored, item by item
Grouped by the thing you did that caused it to be stored.
-
Static codes
Nothing
A static QR code or barcode is generated in your browser. The content you type — a URL, WiFi password, contact card, payment address — is not sent to us, and a scan of a static code goes straight from the phone to the destination without passing through any server of ours. The exceptions are the tools that need a server by nature: dynamic codes, hosted pages, the developer API, and AI art, which sends your prompt and any reference image to the model provider to be rendered.
-
Your account
Identity, plan, billing identifiers
The Firebase user id, your email address and display name, the plan you are on, your Stripe customer and subscription ids, AI credits, and the API keys you create. Of an API key we store only a SHA-256 hash and a short display prefix; the full key is shown once at creation and cannot be recovered afterwards, only replaced.
-
Dynamic codes
The destination and its settings
The short id, the destination URL, when it was created, its scan count, and any pause, expiry date or redirect rules you set. A password on a code is stored as a PBKDF2-SHA256 hash with a random salt and 100,000 iterations (older ones are upgraded the next time the code is unlocked), and wrong guesses are limited per code and network address — still, do not reuse a password that matters elsewhere. For a code attached to a Discover listing we also keep a fingerprint of the destination’s domain, so a repoint to a different domain sends the listing back for review.
-
Scans of your codes
Coarse, cookie-free, pseudonymous
For each scan of a dynamic code: timestamp, device type, operating system, browser, language, referrer, network operator, and the country, region and city Cloudflare reports for the connection plus that city’s approximate coordinates. The redirect sets no cookies and the raw IP address is never stored — a daily-rotating token derived from it is, which is how repeat scans are estimated. That token and the network-level city make scan rows pseudonymous personal data; mention them in your own privacy policy. The full description of what a scan records.
-
Hosted pages
Whatever you put on them, at a public URL
Link-in-bio pages, review funnels, Discover listings and safety profiles are public by design at their URL. What you type into them is stored as entered. A safety profile can hold medical information and emergency contacts if you choose to add them — that is the point of the product, and it means anyone with the code can read it. Review-funnel private feedback and leads collected by a hosted page are stored for you alone.
-
Support and affiliates
Messages and payout links
Contact-form messages are stored so we can reply. Affiliates are paid through Stripe Connect; your bank details are entered on Stripe’s own onboarding page and we store only the connected-account id and the commissions owed.
How long it is kept
Retention here means what actually happens to the rows, not what the dashboard shows.
- Scan history: kept until the code or the account is deleted. Each plan has an analytics window — 30 days on Free, 365 on Pro, 1,095 on Business — and that window is how far back the dashboard and the API will show, not a deletion schedule. Deleting a code deletes its scan rows with it.
- Dynamic codes: kept for as long as the account exists. A code is never paused, expired or deleted because a plan ended. You delete codes; we do not.
- Account deletion erases everything at once. The Delete account button in your account cancels any Stripe subscription first, then deletes, in dependency order, your scans, codes, hosted pages and their leads and views, review funnels and their feedback, safety profiles, Discover listings, API keys, votes and referral rows, and finally the account row. Contact messages, bug reports and feature requests are kept as our operational record but stripped of your id, name and email. Affiliate commission records (amounts, Stripe invoice and transfer ids) are kept as accounting records; the affiliate row they pointed at is deleted. If any step fails, the response says which table could not be erased instead of reporting success.
- You can take a copy first. The account page exports everything the account owns as one JSON file, on every plan including a lapsed one. The export tells you what does not transfer: the printed artwork resolves through qrcodeharbor.com, so a destination map lets you rebuild your routing elsewhere, but an existing print keeps pointing here.
- Deleted rows can persist in Cloudflare’s 30-day point-in-time history after erasure, as noted above. We have no separate copies beyond that.
Cancelling, downgrading, and what happens to printed codes
The part that decides whether you can put our code on something permanent.
- Cancel at any time from the billing portal in your account. The subscription stays active until the end of the period you have paid for, then the account returns to the free plan. No cancellation fee, no email required.
- Every code keeps redirecting after a downgrade. The redirect does not check your plan. If you cancel Pro with 40 dynamic codes, all 40 keep working and can still be repointed; you cannot create a 41st until you are back under the free plan’s 2 or subscribe again. Analytics drop to the free plan’s 30-day window; the older rows are not deleted.
- Setting a pause, an expiry or a password needs Pro or Business; clearing one never does. A code paused on Pro stays paused after a downgrade until you unpause it, which you can always do.
- A dynamic code depends on this site answering, and that is the honest limit of any hosted redirect. If you need a code that must outlive every vendor — a WiFi card, a contact card, a memorial plaque — use a static code; it needs nobody. The honest version of “never expires”.
Abuse reports and blocked destinations
What stops a printed code from being turned into a phishing link.
- Anyone can report a code, page, profile or file at /report-abuse, without an account. Reports go to a queue that a person reads; nothing is switched off automatically. A blocked target answers 403 with a plain “disabled” page — it does not redirect, and it does not repeat what the reporter wrote.
- Destinations are checked when a code is created or re-pointed. A URL with a username before the host name (
https://bank.example@evil.example/) is refused. A destination on a domain the operator has blocked after a report is refused. Bare IP addresses, plain-http destinations, public URL shorteners and punycode host names are allowed but the owner is warned in the response. When Google Web Risk is configured, destinations it lists are refused as well; when it is not, or the lookup fails, no verdict is invented. - Of a reporter we store a salted hash of the connection address, used only to limit repeat reports, together with the reason, the optional description and a snapshot of where the code pointed at the time.
What we do not have
So that nobody has to ask.
- No SOC 2, ISO 27001 or similar certification, and no audit report to send. The infrastructure providers named above publish their own.
- No uptime guarantee or SLA. The site runs on Cloudflare’s network and inherits its availability; we do not add a contractual promise on top.
- No standard data-processing agreement on file. If your organisation needs one, email and ask; do not assume one exists.
- No security team. This service is created and operated by one person, Ardit Tashi, in Milan, Italy. That is who reads a security report, and who fixes it.
- No bug bounty. Reports are welcome and acknowledged; there is no payment programme.
Contact
Security reports, data requests under the GDPR or any other law, questions from a privacy officer, or anything on this page that turns out to be out of date: support@qrcodeharbor.com. Data-subject requests are handled by the operator named above; deletion you can do yourself from the account page at any time.