How we handle your data, what we encrypt, who hosts what, and how to reach us if you find something we missed.
Last updated · 11 June 2026We store what's needed to run device-profile scans, bill you, and keep the onboarding emails sane. Specifically:
DATABASE_URL is set; otherwise in JSON files inside the application's data directory.In transit. All traffic to canaryflux.com and the dashboard API is TLS 1.3, terminated at Cloudflare. All canaryflux.com hostnames are HTTPS-only at the Cloudflare edge.
At rest. Postgres and the application volume are Railway-managed and encrypted at rest under Railway's storage policy. Screenshots are written to the application volume; Postgres holds only the relative paths plus the rest of the application data.
Column-level for authenticated-scan secrets. Pro and Studio projects can store a session cookie or Authorization header so the scanner can reach pages behind a login. We never persist those values in plaintext — each one is individually Fernet-encrypted (AES-128-CBC + HMAC-SHA256) under a key derived via HKDF-SHA256 from the server-side master secret. The master secret lives only in the runtime environment; it is never written to disk, never logged, and never round-tripped through the API.
Signed asset URLs. Screenshot and console-capture URLs are HMAC-SHA256 signed with a 7-day TTL. The payload is domain-separated from JWT signing so a screenshot signature can't be replayed as a session token. The signing secret never leaves the server.
Scan-permission attestation audit trail. Every project records the consent text the owner agreed to (full text snapshot, not just a version string), the attested origin, the timestamp, and the IP + User-Agent at grant time. Older attestations are preserved on an append-only history list bounded at 50 entries per project so abuse investigations can be reconstructed months later.
Passwords. bcrypt with a cost factor of 12. We never see, store, or log plaintext passwords.
Sessions. JWTs signed with HS256, 14-day TTL, reissued on every login. Tokens live in browser localStorage for the dashboard and are sent as bearer tokens to the API. JWTs are stateless (no server-side session table) — a token is valid until its exp claim or until the master secret is rotated.
Login lockout. Five consecutive failed attempts against the same email lock the account for 15 minutes.
Password resets. Single-use tokens with a 1-hour TTL. Requesting a new reset invalidates the previous one immediately.
Email verification. Required to run any scan from a signed-in account (free or paid). Tokens are single-use with a 24-hour TTL; requesting a new verification email invalidates the previous token.
Before we point Playwright at any URL, the request runs through an SSRF guard (_validate_scan_url). We reject:
http(s) schemes127.0.0.0/8) and link-local (169.254.0.0/16) addresses10/8, 172.16/12, 192.168/16)169.254.169.254 and friends)user:pass@host)Every scan runs in a fresh, sandboxed Chromium browser context that is destroyed when the scan ends. No cookies, localStorage, or extensions carry over between runs. (On Pro/Studio authenticated scans, a customer-supplied session cookie or Authorization header is injected into that fresh context for the duration of the run only — see Authenticated scanning above.)
Honest disclosure, because the language elsewhere in this industry is loose: when Canaryflux scans your URL on iPhone 15 Pro, Pixel 8, iPad Pro, or any of the device names in our matrix, it is not running on physical hardware. We do not own or operate a device lab.
What we actually do is render your URL inside a Chromium browser context that Playwright has configured with the device’s real user-agent string, its viewport dimensions, its device-scale factor (DPR), its mobile touch-event flag, and its color-gamut metadata. That context behaves to the page the same way the device’s real browser would — serves the same media queries, triggers the same JS device-detection branches, fires the same touch events, downloads the same image variants.
What this catches: CSS layout bugs that only show at specific viewports, broken touch-event handlers, responsive images that load the wrong source, JS that mis-detects the user agent, modal traps on mobile, post-tap rendering issues, console errors that only fire on mobile UAs. The vast majority of cross-device bugs that ship in marketing-site code live in this category.
What this does NOT catch: hardware-specific failures — camera/microphone permission flows, real GPU rendering quirks, vendor-modified WebKit/Blink builds shipped by Samsung Internet or MIUI Browser, physical-device thermal throttling under load, accelerometer/gyroscope code paths, or the specific way OLED panels handle near-black gradients. If your bug only shows on a real customer’s Galaxy A14 running Samsung Internet 23, no emulation cloud will catch it — you need either a physical device or a real-device cloud service like BrowserStack or Sauce Labs.
We chose emulation over a device lab because it lets us deliver scans in minutes (vs. device labs that take ten times longer) at a price point indie hackers can sign up to without a procurement process. For the marketing-site, auth-flow, and visual-regression class of bugs Canaryflux is built to catch, the trade-off is honest.
On Pro and Studio, you can paste a session cookie or Authorization header into
a project so the scanner can reach pages behind a login. We want to be explicit about
what that means in practice, because it’s the single biggest exposure decision a
customer makes on the platform.
What we encrypt. The cookie or header value itself is individually Fernet-encrypted (AES-128-CBC + HMAC-SHA256) using a key derived via HKDF-SHA256 from the server’s master secret before it ever hits the database. You looking at the Postgres row see only ciphertext. We can’t recover the value from a backup, a logs dump, or a stolen read-replica without the master secret.
What encryption does NOT do. The encryption protects the cookie at rest. It does not redact the data the cookie unlocks. When the scanner loads a page using your session, everything that page renders — customer names, billing details, internal dashboards, PII in form fields — ends up in:
The screenshot is sent to Google (Gemini API) for vision-based finding extraction on the primary path, and to Anthropic on the fallback path when Gemini is unavailable, under each provider's commercial API terms. Canaryflux operates exclusively on the paid Gemini API tier (whose commercial terms prohibit training on customer inputs and outputs) and on Anthropic's commercial API (whose terms likewise prohibit training on customer inputs and outputs). It’s also written to disk on our Railway server and served back to your dashboard via HMAC-signed URLs with a 7-day TTL. The full screenshot file is deleted from disk on our retention sweep — which runs on app startup and at least every 24 hours thereafter — once it ages past your tier’s history window (7 / 30 / 90 / 365 days). DOM-text PII masking (Privacy at the LLM boundary below) covers emails, phones, API keys / tokens (JWTs, Bearer headers, Stripe / AWS / GitHub / Slack token patterns), credit cards, and SSNs in the masked text channel; structured customer data that isn’t in those patterns (names, addresses, balances) is not redacted and is visible to the vision models (Gemini primary, Claude fallback) in screenshots.
Internal access. A small number of Canaryflux engineers can open stored screenshots and DOM excerpts when strictly needed for customer-support tickets you open or abuse-of-service investigations. Access is logged. We do not browse stored scan content for any other purpose; we do not use it for marketing, training, or analytics.
Controller vs processor. When you paste a session that exposes your own end-customers’ personal data, GDPR and LFPDPPP treat you as the controller of that data. Canaryflux becomes a processor of it the moment scanning begins, and our AI vision sub-processors (Google for Gemini on the primary path, Anthropic for Claude on the fallback path) handle it under our agreements with each of them. You need a lawful basis for the processing and, for EU/UK personal data, a Data Processing Agreement with us before the scan runs. Email dpa@canaryflux.com — we ship a standard DPA with SCCs on request.
Our recommendation. Use a low-privilege test-account session when you can. Avoid pasting admin, root, or owner-level sessions for production tenants. If your product has a per-customer dashboard, scan with a seeded test customer’s session, not a real one. The fewer privileges your scanning session has, the less customer data ends up in screenshots and the lower the blast radius if you ever get a takedown request, a DSAR, or a security review.
Special-category data. Do not paste sessions that reveal health, financial-account, biometric, government-ID, or other special-category data (GDPR Art 9 / LFPDPPP “datos sensibles”) unless you have express consent from those data subjects and a written processor agreement with us covering that category. Mexican LFPDPPP penalties for sensitive-data mishandling are materially higher than ordinary-data penalties; the conservative default is “don’t paste it.”
Revoking. The "Authenticated scan…" item in each project’s kebab menu has
a Clear saved auth button that wipes the encrypted cookie + header for that
project. The next scan goes back to the logged-out view. The DELETE
/api/projects/{id}/consent endpoint additionally clears the project’s
scan-permission attestation (immutable history of what was attested and when
is preserved on the audit row).
Every piece of DOM-derived TEXT we send to our vision LLMs (Google Gemini on the primary path, Anthropic Claude on the fallback path) passes through mask_pii first. The actual replacement tokens that ship to the model:
[EMAIL_REDACTED][PHONE_REDACTED][JWT_REDACTED]Authorization header values → [TOKEN_REDACTED]sk_, pk_, rk_) → [STRIPE_KEY_REDACTED]AKIA, ASIA) → [AWS_KEY_REDACTED][GH_TOKEN_REDACTED]xoxb-, xoxp-, xapp-, etc.) → [SLACK_TOKEN_REDACTED]token= / api_key= / password= / secret= values → key=[TOKEN_REDACTED][CC_REDACTED][SSN_REDACTED]The mask runs on the dom_excerpt, runtime signals, and any candidate title/element/evidence values before they hit the prompt or the cache key. Screenshots are a separate channel — they're the vision model's primary signal, so we send them unmodified. If your page visibly renders PII (a logged-in customer's email in the header, for example), assume those pixels reach our AI vision sub-processors as image bytes — Google Gemini on the primary path and Anthropic Claude on the fallback path — under each provider's commercial API privacy terms. Scan pages where that's acceptable, or sign out first.
Dashboard. Vercel (US-East). Static HTML and Vite-built React shell, no server runtime.
API + scanner workers. Railway, US region. Each scan runs in a fresh, sandboxed Chromium browser context, isolated from other in-flight scans. (Application-level Python state — rate-limit buckets, in-memory caches — does live in the worker process for the lifetime of the process; the per-scan isolation is at the browser-context level, not the Python level.)
Database. Railway-managed Postgres, encrypted at rest under Railway's storage policy.
DNS & edge. Cloudflare proxied with TLS termination.
These are the only third parties that ever touch your data:
| Vendor | Purpose | Region |
|---|---|---|
| Google (Gemini API) | Primary vision LLM (Gemini 2.5 Flash-Lite) for finding detection & verification, used for all tiers including Free. Canaryflux operates exclusively on the paid Gemini API tier, whose commercial terms prohibit training on customer inputs / outputs. Processing under Standard Contractual Clauses where EU/UK personal data is involved. | US |
| Anthropic | Fallback vision LLM (Claude Sonnet 4.6) for finding detection & verification when the primary provider is unavailable. Commercial API terms prohibit training on customer inputs / outputs. | US |
| Railway | API server, scanner workers, Postgres | US |
| Vercel | Marketing site + dashboard static hosting | US (global edge) |
| Vercel Analytics | Anonymous page-view telemetry across both marketing and dashboard pages (no cross-site tracking) | US |
| Cloudflare | DNS + TLS termination | Global |
| Stripe | Subscription billing & payment processing | US |
| Resend | Transactional email (verification, alerts) | US |
| Sentry | Server- and client-side error monitoring (stack traces, URL path, browser type — emails / cookies / Authorization headers stripped before send) | US |
Findings and screenshots are retained for the window defined by your plan:
Account deletion. From the dashboard's Account settings, you can delete your account. We immediately wipe your user record, scan history, findings, screenshots, and any pending email-verification or password-reset tokens. Stored screenshots are removed in a background task right after the user record is gone. Because JWTs are stateless, any existing browser session becomes unusable as soon as the user record is gone — every API call rejects with 401. Stripe's billing-history retention is governed by their own policies.
Shareable finding links use 24-byte cryptographically random URL-safe tokens with a 90-day TTL. The server stores only the SHA-256 hash of each token. You can revoke any link from the finding's detail view; revocation is immediate.
If you find a vulnerability, please email us before making it public. We commit to: