Security and data protection
How PenaltyProof is built, what it stores, and what it does not do.
Claims on this page have been verified against the application source code. Where a claim cannot be verified from code, that is stated explicitly. Last updated: 11 June 2026.
Authentication
Magic-link only — no passwords stored
PenaltyProof has no password-based login. You sign in by entering your email address; a signed link is emailed to you, and clicking it starts a session. The application stores no password, password hash, or credential of any kind. There is nothing to leak if the database is read.
Magic-link design
- Links are signed with HMAC (itsdangerous URLSafeTimedSerializer) and expire after 72 hours.
- Each link carries a single-use JTI (JWT ID). On first use, the JTI is inserted into a
consumed_tokentable. A duplicate insert (replay attempt) is rejected immediately. - After a successful click, a 30-day rolling session cookie is issued.
Session cookie
HttpOnly: yes — not accessible from JavaScript.Secure: yes in production (requires HTTPS); off in local development.SameSite: Lax. Strict would drop the cookie when clicking the magic link from an email client (a cross-site navigation); Lax was chosen so the first post-click request carries the session.- Session version: each accountant row carries a
session_versioninteger. Incrementing it server-side immediately invalidates all outstanding cookies. - Cookie max age: 30 days, rolling (re-issued on each authenticated request).
Request hardening
CSRF defence
All state-changing requests (POST, PUT, PATCH, DELETE) that are not signed webhooks or bearer-auth cron endpoints are required to carry an Origin or Referer header matching the application's allowed-host list. Requests without a same-origin header are rejected with HTTP 403. The session cookie's SameSite=Lax provides a complementary browser-level defence.
Security headers
Content-Security-Policy: per-request nonces on scripts;frame-ancestors 'none';object-src 'none';default-src 'self'.Strict-Transport-Security:max-age=63072000; includeSubDomains; preloadX-Frame-Options: DENYX-Content-Type-Options: nosniffReferrer-Policy: strict-origin-when-cross-originPermissions-Policy: camera, microphone, and geolocation disabled
Rate limiting
Default limits: 200 requests/hour and 30 requests/minute per IP address, enforced by SlowAPI middleware. In production the counter store is Redis so limits are enforced across all application instances. The magic-link send endpoint carries its own tighter limit. Cron and webhook endpoints are exempt.
Trusted-host enforcement
Requests with a Host header not in the configured allow-list are rejected at the middleware layer before reaching any route handler.
What data we hold
PenaltyProof stores:
- Your email address (the account identifier)
- Company numbers you add to your monitor list
- Per-company obligation settings (VAT stagger group, CT period end, pension staging date — entered by you)
- Filing deadline data fetched from the public Companies House API
- Alert send history (which emails were sent, when)
- Subscription and billing status (plan name, Stripe customer ID, subscription ID)
- Email address and company number submitted via the /check result capture form (pre-signup; stored as a lead capture record in the
check_capturestable) - Name and email submitted via the Practice tier waitlist form (stored in the
practice_waitlisttable) - Team workspace metadata for Practice workspace features: workspace memberships, roles, invited email addresses, invited roles, inviter ids, invitation token hashes, expiry timestamps, accepted timestamps, and revoked timestamps. An invitation token is stored as a hash while pending, then cleared when accepted, revoked, or superseded.
PenaltyProof does not store passwords, client financial data, accounting records, HMRC agent portal credentials, or any data from your clients directly. It reads only the public Companies House API.
Audit log
Significant account events (sign-in, account deletion, company add/remove) are written to an audit log. IP addresses and user agent strings in the log are one-way hashed using SHA-256 with a server-side pepper (AUDIT_LOG_PEPPER). The hash output is truncated to 32 hex characters. The pepper can be rotated to sever historical correlation. Audit log rows are deleted after 365 days via the nightly purge job.
Team lifecycle events (team_invited, team_invite_revoked, team_member_removed, and team_invite_accepted) are included in the same generic audit log with acting account id and optional request-context hashes. The request context uses hashed IP and hashed user-agent values when available. The current audit log does not store target member/invite ids, before/after values, or a product-visible object history, so full activity history is not yet available.
Soft-delete and hard purge
When you delete your account, the record is soft-deleted (a deleted_at timestamp is set; the row is immediately excluded from all application queries). After 90 days, a nightly scheduled job hard-purges the row and all its associated data (companies, alerts, snapshots) via database cascade. This 90-day window is described in the Privacy Policy.
Alert and email send log retention
Alert sentinel rows are deleted after two years. Email send log rows (which record that an email was dispatched) are deleted after 90 days. Both deletions happen in the nightly purge job.
Snapshot data
Companies House snapshot data (daily CH API responses cached per company) is deleted after 12 calendar months.
Payments
All card payments are handled entirely by Stripe's hosted checkout and payment links. Card numbers, CVV codes, and bank details never pass through PenaltyProof's servers. The application receives a Stripe customer ID, subscription ID, and plan status from Stripe webhooks — no payment card data is stored or transmitted through our infrastructure.
Stripe's compliance status (PCI DSS, SOC 2) applies to Stripe's payment infrastructure, not to PenaltyProof. PenaltyProof itself does not hold or process cardholder data.
Account deletion and data subject requests
You can request deletion of your account from your account settings. A signed confirmation link is emailed to you; clicking it soft-deletes your account immediately. Hard purge of all associated data occurs within 90 days of soft-delete, as above.
For data subject access requests (DSAR) under UK GDPR, contact privacy@penaltyproof.co.uk. We aim to respond within 30 days. We are registered with the Information Commissioner's Office: Reg. No. ZC142025.
Subprocessors
The following third-party services process data on behalf of PenaltyProof:
- Railway — application hosting and managed PostgreSQL database. Railway operates infrastructure in the United States and other regions. The specific server region in use for this application is not independently verifiable from our configuration files; we do not make a data-residency claim for Railway.
- Stripe — subscription billing, payment processing, and customer portal. Stripe processes payment card data under its own PCI DSS compliance.
- Resend — transactional email delivery. Alert emails and magic links are sent via Resend's API. Email send status webhooks are received from Resend.
- Cloudflare Turnstile — bot detection on the signup form. Turnstile challenges are rendered client-side; the token is verified server-side via Cloudflare's API. No personal data is submitted to Cloudflare beyond the Turnstile challenge token.
- Plausible Analytics — privacy-focused web analytics. Plausible markets itself as cookieless and GDPR-compliant by design; we load their script and also post conversion events server-side from webhooks. We rely on Plausible's published privacy posture for analytics data handling — this is a third-party claim, not one we can independently verify.
- Sentry — error monitoring. Sentry receives exception reports including request paths and error messages. PII scrubbing is applied before sending: request bodies, cookies, and user email addresses are stripped from Sentry events.
- Companies House Public Data API — source of filing deadline data. This is a public API operated by Companies House; no personal data is submitted to it beyond company numbers.
What we do not have
The following are not in place and we do not claim otherwise:
- No SOC 2 or ISO 27001 certification. PenaltyProof is a small product operated by a solo developer. We have not obtained third-party security certification.
- No guaranteed response-time SLA. We monitor application health via Sentry and a public statuspage at status.penaltyproof.co.uk. We do not publish a commercial SLA for support response times.
- No UK/EU data residency guarantee. Railway may host data in non-UK regions. We cannot verify the physical location of our database from our configuration.
- No penetration testing certificate. We have not commissioned a third-party penetration test.
- No HMRC API integration. PenaltyProof calculates HMRC deadlines from rules and client-supplied configuration (VAT stagger group, CT period end, etc.). It does not connect to HMRC systems or hold HMRC credentials.
Security contact
To report a security issue: security@penaltyproof.co.uk. We aim to acknowledge reports within two working days. A security.txt file is available at the standard location.
Procurement FAQ
Common questions from practices evaluating PenaltyProof before subscribing.