Security whitepaper
This document describes the technical security controls in the Cadrify platform. It is written for IT security reviewers, procurement teams and data protection officers carrying out due diligence. A non-technical summary is on the security overview page, and the contractual and data protection position is in the security and data protection policy.
Executive summary
What Cadrify protects
Cadrify is a multi-tenant platform for British political campaign management. It processes some of the most sensitive categories of personal data in domestic politics: the full electoral register, door-knock records expressing individual voters’ political opinions, volunteer and supporter contact lists and the live operational data used to coordinate election-day activity.
Cadrify processes this data as a data processor on behalf of subscribing political organisations. Each organisation’s data is partitioned from every other organisation on the platform, in the application and again in the database. Platform operator access to customer data is restricted, audited and limited to support and incident response.
Threat model
| Threat | Risk to political data | Primary controls |
|---|---|---|
| Credential stuffing / account takeover | High, political organisations are high-value targets for state and partisan actors | scrypt password hashing · account lockout · rate limiting · session rotation on credential change |
| Insider threat / privilege abuse | Medium, staff or former members with access could exfiltrate data | Least-privilege RBAC · per-request operator access log · account deactivation on role removal |
| Web application attack (SQLi, XSS, CSRF) | Medium, successful exploit could expose register or door-knock data | Parameterised queries · Jinja2 auto-escaping · Flask-WTF CSRF · nh3 sanitisation |
| Electoral data exfiltration | High, electoral register and political opinions are restricted special-category data | Authentication required on all data endpoints · org_id scoping on every query · PostgreSQL row-level security · no public data APIs |
| Session hijacking | Low to medium, a stolen session token grants access equivalent to the user | HttpOnly · SameSite=Lax · Secure flag · 24-hour expiry · server-side token store (SHA-256 hashed) · full session invalidation on password change |
| Supply chain / third-party compromise | Low, no third party receives electoral register or door-knock (special-category) data | Electoral and door-knock data never leave the platform · contact sync only to customer-configured CRMs under the Customer’s own credentials · geocoding receives postcodes only · DPAs with platform processors |
Security architecture
Dashed arrow = HTTPS only. Electoral register and door-knock (special-category) data never leave the platform. Activist contact data syncs only to customer-configured CRMs.
Data classification
| Classification | Examples | Regulatory basis | Risk |
|---|---|---|---|
| Public | Campaign pages, petition forms, event listings | N/A | Low |
| Platform account data | User names, email addresses, profile pictures, role memberships | UK GDPR Art. 6, Contract / Legitimate interests | Medium |
| Volunteer and supporter data | Activist contact records, opt-in status, event sign-ups | UK GDPR Art. 6, Consent / Legitimate interests (Customer’s) | Medium to high |
| Electoral register data | Full names, addresses, elector numbers, dates of birth, ward data | RPA 1983 (as amended). The Customer must hold a lawful basis under the RPA and UK GDPR | High |
| Operational campaign data | Door-knock rounds, GOTV assignments, calling-card drops, route plans | UK GDPR Art. 6, Contract (as processor) | Medium to high |
| Political opinion data | Voting intentions, support scores, core party affiliation, door-knock responses | UK GDPR Art. 9, Special category. Customer must document Art. 9(2) condition (typically (d)). | Severe |
Key controls summary
| Control area | Implementation | Section |
|---|---|---|
| Data encryption | TLS 1.2/1.3 in transit · AES-256 at rest (AWS EBS) · Fernet application-layer encryption of stored integration secrets | §2, §18 |
| Authentication | scrypt hashing · 256-bit tokens · lockout · forced rotation | §3, §4 |
| Access control | Role-based access control · ancestor-chain inheritance · org_id isolation · PostgreSQL row-level security | §5, §6 |
| Application security | Parameterised SQL · XSS escaping · CSRF tokens · input validation | §7 |
| Brute-force protection | IP rate limiting (Flask-Limiter) · per-account lockout | §8 |
| Audit logging | Authentication and account log · organisation audit log · operator access log | §9 |
| Compliance | UK GDPR · DPA 2018 · PECR · RPA 1983 · Article 9 | §19 |
1. Infrastructure and hosting
Hosting provider
Cadrify runs on Render (render.com), a managed cloud platform. Application servers and the PostgreSQL database are provisioned in Render’s Frankfurt, Germany region (within the EEA). No customer data is persisted outside the EEA at rest. Render operates on Amazon Web Services infrastructure with physical access controls and ISO 27001-aligned operational practices.
Database
The primary data store is PostgreSQL 18, provisioned as a managed Render Postgres instance in Frankfurt with daily automated backups. The application uses parameterised queries only. No user-supplied input is interpolated into query text.
Application stack
The application is a Python web application using the Flask framework with a domain-driven blueprint architecture. Pages are rendered server-side with Jinja2. Where the interface needs richer interaction, React components built with Vite are mounted into the rendered page. Every front-end library, including MapLibre GL for maps, is served from the application’s own origin at a pinned version. No script loads from a third-party CDN, and the Content Security Policy (§10) does not allow one.
Background work
Long-running work such as register imports, geocoding batches, integration syncs and scheduled email runs on a separate worker service, also in Frankfurt, fed by a Redis-compatible queue (Render Key Value). The queue sits on Render’s private network with no external access. The state of every job, including progress and retries, lives in PostgreSQL rather than in the queue, and the worker declares an explicit cross-organisation context for its database work because it has no user session (§6).
Fail-closed configuration
On startup the application validates its own configuration and refuses to start if a security-critical setting is missing or unsafe, for example an unset signing key or the development-default signing key in a production deployment. The process exits rather than run in a known-insecure state. In production the application runs behind Render’s TLS-terminating load balancer and trusts exactly one proxy hop when resolving the client IP address used for audit logging and rate limiting.
2. Data-at-rest and data-in-transit encryption
In transit
All connections between clients and the Cadrify application are encrypted using
TLS 1.2 or 1.3, terminated at Render’s edge. Plain HTTP is not
accepted. Every HTTP request is redirected to HTTPS at the infrastructure level.
Secure flags are set on all application cookies in production, preventing
transmission over unencrypted connections. Traffic between the application server and
the PostgreSQL instance is encrypted within the Render private network.
At rest
Database volumes are encrypted at rest by Render’s underlying AWS EBS storage (AES-256). Profile images and cached geocoding data stored on application file storage are similarly encrypted at the storage layer. No customer data is stored on unencrypted media.
Application-layer encryption of secrets
Sensitive integration credentials are additionally encrypted at the application layer
before they are written to the database, so they are never stored as readable text even
within the encrypted database volume. This applies to the per-branch credentials for every
external CRM integration: the Action Network API key, the NationBuilder access token, the
Movement API key and the Salesforce private key. Encryption uses Fernet
(AES-128 in CBC mode with an HMAC-SHA256 authentication tag) from the Python
cryptography library. The encryption key is held in a dedicated environment
variable, separate from the application code and from the database. Ciphertext is decrypted
only at the moment an outbound API client is constructed. The plaintext credential is never
logged or returned in any application response. Key management for this control is described
in §18.
3. Authentication
Password hashing
Passwords are stored using the scrypt memory-hard key derivation function, applied via Werkzeug’s password hashing utilities. Passwords are never stored in plain text, logged or transmitted beyond the login POST request. The minimum password length is enforced at 12 characters at both the server and client layers. Beyond the length floor, the server rejects passwords that lack sufficient character variety, appear on a weak-password denylist or are derived from the user’s own email address.
Password reset
Password reset tokens are generated using Python’s cryptographically secure OS
random source (the secrets module), producing a URL-safe random string.
Tokens are single-use: they are marked as consumed on redemption and cannot be replayed.
Tokens expire after 24 hours. Requesting a new reset token invalidates any
previously issued token for that account. Reset tokens are delivered only to the email
address already associated with the account.
Invite tokens
New users are onboarded via invite tokens, also generated using Python’s
cryptographically secure OS random source. Tokens expire after 30 days
and are single-use. Superseded invites are marked used_at='superseded'
in the audit trail. Invite issuance and redemption are both recorded in the auth audit
log with IP address.
Passwordless sign-in (magic link)
Existing users may request a one-time sign-in link sent to their registered email
address. The link carries a token generated using the secrets module, valid
for 15 minutes and single-use. Requesting a new link invalidates any
previously issued unused link. Magic-link sign-in authenticates existing accounts only, it
does not create accounts. Requests are rate limited per IP address and per email address
(see §8), and both issuance and redemption are recorded in the
authentication audit log. To prevent account enumeration, the same confirmation is shown
whether or not the email matches an account.
Account enumeration prevention
The login endpoint returns a generic “Invalid email or password” message regardless of whether the email address exists. Account lockout is applied silently, without informing the requester whether the account exists, preventing the lockout mechanism from becoming an account-enumeration oracle.
Forced password change
When an administrator resets a user’s password, the
must_change_password flag is set. The user is redirected to a forced
password-change screen on next login and cannot access any other part of the
application until the change is completed. A new session token is issued on completion
and all prior sessions are invalidated.
4. Session management
Token generation and storage
On successful authentication, the application generates a session token using
Python’s cryptographically secure OS random source (the secrets
module). Only a SHA-256 hash of the token is stored server-side in the
database, alongside the associated user identity, creation timestamp, expiry, client IP
address and active organisation context. The raw token exists only in the user’s
cookie, so a read-only exposure of the session table yields no usable credentials. The
token is issued as an HTTP-only cookie named auth_token. It is an opaque
bearer token validated by server-side lookup, not a signed client-side payload.
Cookie security attributes
| Attribute | Value | Purpose |
|---|---|---|
HttpOnly |
true | Blocks JavaScript access, which limits session theft through XSS |
Secure |
true (production) | Prevents transmission over plain HTTP |
SameSite |
Lax | Withholds the cookie on cross-site requests, a second guard against CSRF |
Max-Age |
86 400 s (24 hours) | Limits session lifetime to a day, chosen because campaign devices are often shared |
Session rotation and invalidation
A new session token is issued on every login. On logout, the token is deleted from the database and the cookie is cleared. On password change (self-initiated or admin-forced), all active sessions for that user are deleted before the new session is created, ensuring no stale sessions survive a credential change. Sessions are also purged opportunistically during request processing, removing expired rows.
Session context
Each session record stores the user’s currently active organisation and branch context. Switching between branches updates this record in place rather than issuing a new token. The client IP address captured when the session was created is retained on the session record.
5. Authorisation and role-based access control
Role hierarchy
Cadrify implements a layered role model within each organisation branch:
| Role | Scope | Access granted |
|---|---|---|
| Activist | Assignment-only | Electors and doors within an active door-knock or GOTV assignment only, with no register browsing |
| Organiser | Exact branch | Data management and user management within the assigned branch, with no inheritance to parent or sibling branches |
| Admin | Branch and all descendants | Full management of the branch and all child branches. No access to other organisations |
| Data protection officer | Organisation-wide, read-only | Read-only access to the compliance area for the whole organisation: audit log, subject-access and erasure tooling. No campaign-data write access |
Two refinements apply in practice. First, the Admin role distinguishes the organisation’s root-branch administrator (organisation-wide settings) from an administrator of a sub-branch, who manages that branch and its descendants only. Second, activist-level access is granted through specific assignment roles (for example door-knocker, teller or GOTV knocker), each scoped to an active assignment and, where relevant, to a particular round.
Permission check order
Every protected route passes through server-side authorisation middleware that enforces the following check sequence, aborting with the appropriate HTTP status at the first failure:
- Is the user authenticated? → redirect 302 to login
- Is the user’s account active? → 403
- Is an organisation/branch context set in the session? → redirect to the context picker
- Does the user hold the platform operator flag? → allow (operator access is recorded in the operator access log earlier in the request pipeline)
- Does the user hold the minimum required role, considering ancestor-chain inheritance? → allow or 403
Ancestor-chain inheritance
Admin role checks traverse the branch hierarchy using a recursive CTE query. A user who is admin of a parent branch is implicitly admin of all descendant branches. The database query returns the full ancestor chain and checks for admin membership at any level. Organiser access does not inherit. It applies only to the exact branch assigned.
Platform operator access
A small number of platform operators hold an elevated privilege flag that permits cross-organisation access for support and incident response. Every request made by a platform operator is recorded in a dedicated operator access log, capturing the operator account, the action, the active organisation and the client IP address. The record is written in the request pipeline before the route handler runs. These records are held in a separate table from the application’s ordinary data and are not editable through the application interface.
6. Multi-tenant data isolation
Cadrify is a multi-tenant application, and isolation between organisations is enforced twice.
In the application. Every customer record carries an organisation identifier that is checked on read and write. Scoping is applied in the repository layer, so route handlers do not build their own query predicates, and the identifier is always taken from the authenticated session, never from user-supplied input.
In the database. Every table whose rows belong to exactly one organisation has PostgreSQL row-level security enabled and forced, with a single policy that admits a row only when the connection has declared a matching organisation or an explicit, logged bypass. At the start of each request the application declares the active organisation on its database connection as a transaction-local setting, which is reset at commit or rollback, so a pooled connection can never carry one organisation’s context into the next request. A connection that declares nothing sees no tenant rows at all. The policy fails closed. A query that forgot its organisation clause would therefore return nothing rather than another organisation’s data.
The bypass exists for two cases only: platform operators, whose every request is written to the operator access log (§5) and work with no request context, such as background jobs and scheduled tasks, which legitimately span organisations. A signed-in user with no resolved organisation is treated as an anomaly and fails closed rather than being granted the bypass.
Cadrify’s automated test suite includes explicit cross-organisation access assertions for both layers.
Reference data (Democracy Club election/party tables, geographic reference areas) is not org-scoped and is read-only for all users. It contains no personal data.
7. Application security controls
SQL injection prevention
All database interactions use parameterised queries. No user-supplied input is interpolated directly into query strings. This approach is enforced consistently across the codebase through a shared database access layer rather than relying on individual developer discipline per module.
Cross-site scripting (XSS)
Flask’s Jinja2 templating engine performs automatic HTML escaping on all variables rendered into HTML responses. User-supplied content is never rendered as raw HTML except in administrator-controlled branding fields, which are additionally sanitised using nh3, an HTML sanitiser that enforces a strict allowlist of tags and attributes.
Cross-site request forgery (CSRF)
CSRF protection is provided by Flask-WTF (CSRFProtect),
initialised globally on application startup. All state-changing requests from the browser
require a valid CSRF token, validated server-side on every form submission. CSRF
protection is disabled in the development and test configurations
(WTF_CSRF_ENABLED = False) and is always enforced in production.
Two server-to-server endpoints are exempt from CSRF tokens because they carry no session cookie and therefore present no CSRF surface. Each authenticates by a different mechanism:
- The data export API authenticates with a per-organisation bearer
API key supplied in an
AuthorizationorX-API-Keyheader. Each key carries a scope, is rate limited per key, and every call is logged. A browser cannot attach the secret key header on a cross-site request, so CSRF does not apply. - The integration webhook receivers (for inbound CRM events)
authenticate with a per-integration shared secret presented in a request header,
compared in constant time. A request with a missing or incorrect secret returns
404so the endpoint’s existence is not confirmed. Integrity rests on TLS and the secret. The webhook body itself is not separately signed.
Input validation
User-supplied strings are normalised before use. Email addresses are trimmed and
lower-cased, name fields are trimmed and numeric identifiers are cast to integers with the
failure caught. File uploads are validated by MIME type before processing. The platform does
not evaluate user-supplied data as code at any point. Data exported to CSV is guarded against
spreadsheet formula injection: any cell beginning with a formula trigger character
(=, +, -, @, tab or carriage return)
is prefixed so spreadsheet software treats it as text.
8. Rate limiting and brute-force protection
Cadrify runs two independent layers of brute-force protection.
Endpoint rate limiting
Rate limiting is applied to authentication and other sensitive endpoints. Most limits are keyed on client IP address. Login and magic-link requests are additionally limited per submitted email address, and the data export API is limited per API key. Current limits:
| Operation | Limit |
|---|---|
| Login | Per IP: 10 per minute, 50 per hour. Per account: 10 per minute, 30 per hour |
| Self-service password reset request | 10 per hour |
| Password reset token redemption | 20 per hour |
| Magic-link request | 5 per hour per IP, 5 per hour per email |
| Magic-link verification | 20 per hour |
| Invite token redemption | 20 per hour |
| User creation | 60 per hour |
| Admin-initiated password reset | 30 per hour |
| Role assignment (add or remove) | 20 per hour |
| Public contact form | 8 per hour |
| Self-service organisation sign-up | 5 per 15 minutes, with the verification email resend at 3 per hour |
| Single sign-on start and callback | 60 per minute |
| Data export API | 120 per hour per key, plus a 200 per hour per-key limit enforced from the request log |
| Integration webhook receivers | 200 per minute (Stripe billing webhook: 300 per minute) |
In production these counters are held in the Redis-compatible queue store rather than in application memory, so they are shared across every web process and survive a restart of the web service. The durable control against password guessing is the per-account lockout described below, which is stored in the database.
Per-account lockout
Independently of IP-based rate limiting, the application tracks failed login attempts per user account. After 10 failures within a 30-minute rolling window, the account is locked for 30 minutes. The lockout is cleared automatically on expiry or on a successful login. Account lockout events are recorded in the auth audit log with the actor IP address.
9. Logging and monitoring
Authentication logs
Sign-in attempts, successful or failed, by password, passkey, single sign-on or sign-in
link, are written to the auth_audit_log table with timestamp, actor IP address
and outcome. A failed attempt against an email address that has no account is recorded too,
with no account attached. Login failures are counted per account in a rolling 30-minute
window, and lockouts are also recorded. This log provides the primary signal for detecting
credential-stuffing campaigns or targeted account attacks.
Organisation administrators and data protection officers see sign-in and credential events on the Sign-ins page of the compliance area: password, passkey, single sign-on and sign-in link sign-ins, failed attempts, lockouts, password resets and changes, with the time, the account, the action and the IP address. The page covers accounts that hold a role in the organisation, and attempts through the organisation’s own single sign-on connection. A failed attempt against an email address with no account is not attributed to any organisation and is not shown to customers.
Audit trail
Security-relevant platform events are written to auth_audit_log
with the following fields: actor_user_id, action,
target_user_id, target_group_id, target_role,
details (JSONB, event-specific context), ip_address,
created_at.
Recorded action types:
login_success,login_failed,login_lockedpasskey_login_success,passkey_login_failed,sso_login_success,sso_login_failedmagic_link_requested,magic_link_usedpassword_changed_self,password_changed_forced,password_reset_requested,password_reset_token_used,password_reset_adminuser_created,user_activated,user_deactivated,user_deleted,user_purgedrole_added,role_updated,role_reactivated,role_removedinvite_issued,invite_redeemed,invite_superseded,invite_regenerated,invite_cancelled,group_invite_accepted
Every request made by a platform operator is additionally written to a separate operator access log, capturing the operator account, the action, the active organisation, the client IP address and the timestamp. This logging is performed in the request pipeline before any route handler executes.
Alongside the authentication audit log, a broader data-change audit trail records security-relevant and data-mutating actions within an organisation, for example record edits, exports and erasures. It is available to organisation administrators and data protection officers through the compliance area. Downloads of an organisation’s personal data are also logged separately, with who downloaded what, when and how many rows. They appear on the compliance overview and on the exports page of the data hub.
None of these logs can be edited through the application. Two things change entries. When an erasure request is approved, the organisation audit trail entries about that elector or activist keep who did what and when, and lose the summary and details that describe the person. When an organisation is deleted, its audit trail and export records are deleted with it. Authentication log entries are not deleted. When an account is deleted, its entries stay without the link to the account.
Anomaly indicators
The following patterns in the audit log indicate potential security anomalies and can be used for manual or automated alerting:
- High volume of
login_failedevents from a single IP within a short window (credential stuffing) login_lockedevents against multiple accounts (distributed brute force)- Platform operator access to an organisation outside normal support windows
- Bulk
role_removedoruser_deactivatedevents (potential insider sabotage) - Password reset tokens issued but not redeemed within a short interval of a successful login (potential account takeover via reset)
Automated alerting and SIEM integration are on the security roadmap (see §20).
Retention periods
| Log type | Retention | Notes |
|---|---|---|
| Authentication and account log | No automatic deletion | Sign-in and credential events on the Sign-ins page. The rest available to org admins on request |
| Organisation audit log | Kept while the account is open. Deleted 30 days after it closes, except as set out above | Edits, exports and erasures, available to org admins |
| Operator access log | Retained for the life of the platform | Held in a separate table, not editable through the application interface |
| Application server logs | 30 days (Render default) | HTTP access and error logs |
Access review
Organisation administrators are responsible for periodically reviewing the list of active users and their role assignments within their organisation. Cadrify provides the user management interface for this purpose. When a user is deactivated, all subsequent requests are rejected by the authorisation middleware regardless of any existing session token. Customer-facing audit log export is already available to an organisation's data protection officer (see §20).
A failure to write an audit row is itself logged and investigated separately. It does not block or roll back the action that triggered it.
10. Security headers
The following HTTP security headers are applied by the application’s
after_request hook (Strict-Transport-Security is sent on HTTPS
responses):
| Header | Value | Purpose |
|---|---|---|
X-Frame-Options |
SAMEORIGIN |
Prevents clickjacking via iframe embedding on third-party origins |
X-Content-Type-Options |
nosniff |
Prevents MIME-type sniffing attacks |
Referrer-Policy |
strict-origin-when-cross-origin |
Limits referrer information sent to third-party origins |
Content-Security-Policy |
see below | Restricts the origins from which scripts, styles, images, fonts, frames and connections may load. Complements output escaping against XSS |
Strict-Transport-Security |
max-age=31536000; includeSubDomains |
Instructs browsers to use HTTPS only for one year, including subdomains. Sent on HTTPS responses |
Content Security Policy
An enforcing Content Security Policy is applied to every response. The base policy is
default-src 'self' with object-src 'none',
base-uri 'self' and frame-ancestors 'self'. Scripts and styles
are restricted to the application’s own origin. Front-end libraries such as MapLibre GL
and Bootstrap are served from that origin at pinned versions, with no third-party
script CDNs in the allowlist. Map tiles load
from tiles.openfreemap.org (permitted in connect-src, with
worker-src 'self' blob: for the map renderer). Fonts may load from Google Fonts.
Images may load from any HTTPS origin, because branding and leaflet images are supplied by
customers. The policy also permits the cookieless analytics endpoint and the demo-booking
widget used on the public pages, and Stripe’s hosted payment scripts and frames on
donation and ticket pages. The policy currently allows 'unsafe-inline' for
scripts and styles. This is a known limitation, kept because the server-rendered templates
still use some inline handlers and style attributes. It does not allow
'unsafe-eval'. Moving inline allowances to a nonce-based model is on the roadmap
(§20).
11. File handling and upload security
Profile avatars
Avatar uploads are validated against an allowlist of MIME types
(image/jpeg, image/png, image/gif,
image/webp). The uploaded file is then opened, decoded and re-encoded by
Pillow (PIL), cropped to a 256×256 square and re-encoded as
JPEG, before storage. This re-encoding strips any embedded metadata (EXIF, ICC
profiles) and prevents polyglot file attacks. The global upload size limit is 32 MB.
Stored profile images live in a private application directory restricted to the
application user.
Data file imports
Electoral register imports are parsed with pandas from the raw bytes of the uploaded file in application memory. No uploaded file is written to a publicly accessible location. Between the upload and the user confirming the column mapping, the parsed data is stored server-side in the database as a compressed draft (not in a cookie), scoped to the importing organisation, so a large import survives a worker restart. Draft imports that are abandoned are purged automatically after 30 days. On confirmation the data is committed to the register tables and the draft is cleared.
Other uploads
The platform accepts a small number of other uploads, each validated before use:
- Leaflet-drop and campaign images are size-capped, decoded by Pillow, and re-encoded, which strips metadata and rejects non-image payloads. Stored images use generated filenames and the file-serving paths reject directory traversal.
- Activist and phone-bank list imports (CSV) are read and parsed in memory, validated against the expected columns, with no file written to disk.
- Bulk data imports in the data-operations area spool to a temporary file in the system temp directory, are validated against the dataset schema, loaded, and then deleted.
Runtime directories used for uploads and generated files (profile images, map boundaries, geocoding cache, generated rounds and sheets) are created with restrictive permissions so they are readable only by the application user.
12. Browser storage and offline data
To support canvassing in areas with poor connectivity, the field tools keep a limited amount of operational data on the user’s device. This is a deliberate design choice for offline working, and it means some personal data is held on the device until it is synchronised back to the server.
- Door-knock field view and GOTV. Responses and knock results are queued on-device (in IndexedDB, with a localStorage fallback) and flushed to the server over an authenticated request as soon as connectivity returns, then cleared. The queue holds elector identifiers and the recorded responses while offline. The application warns the user if they leave the page while the queue is non-empty.
- Telling and leaflet rounds. Running counts and the set of delivered addresses for the current round are held on-device for the same offline-working reason.
- Service worker cache. A service worker caches the application shell and, for the active GOTV and leaflet rounds, the round’s data, so the field view works offline. The elector and address data for an active round may therefore sit in the browser’s cache storage on the device until the cache is refreshed or cleared.
- Interface preferences. Table column widths, pinned rows, sidebar state and the light/dark theme choice are stored in localStorage. These hold no personal data.
All browser-stored data is scoped to the application’s own origin and is not accessible to third-party code. None of it is used for authentication tokens or session state, which remain in the HTTP-only session cookie. Signing out and the device’s own controls for clearing site data, remove the on-device copies.
13. Third-party integrations and subprocessors
This section distinguishes two kinds of third party. Platform subprocessors are services Cadrify uses to operate the platform, engaged under data processing agreements. Customer-configured integrations are external systems a Customer chooses to connect, using the Customer’s own credentials, so that data flows between Cadrify and a system the Customer already controls.
Platform subprocessors
| Provider | Purpose | Data handled | Security posture |
|---|---|---|---|
| Render | Application hosting, database, background worker and job queue | All platform data | AWS-backed. Frankfurt region. AES-256 at rest. SOC 2 Type II (via AWS) |
| Resend | Transactional email delivery (Dublin, Ireland, EEA) | Recipient email address and email content (password reset links, invite links, notifications) | SOC 2 Type II. TLS in transit. Data within the EEA (eu-west-1, Ireland) |
| postcodes.io | Postcode geocoding | British postcodes only, not linked to a named individual in the request | British open-data service. Results cached locally |
Email delivery. Transactional email is delivered through Resend (EEA, Ireland). Cadrify may move to a different EEA email provider in future, and this section will be updated if it does.
Billing. Subscription billing, and donation or ticket payments where a Customer uses them, are handled by Stripe, which processes in Ireland and the United States under standard contractual clauses. Card details are entered on a Stripe-hosted page and never reach Cadrify’s servers. No campaign data goes to Stripe.
AI features. A small number of AI-backed features are granted per organisation and are off unless Cadrify enables them for a Customer. Where none is enabled, nothing the organisation holds reaches an AI provider. Where one is, the request goes to Anthropic or to OpenAI where a deployment is configured to use it, both of which process outside the EEA. Voting intentions, support scores and door-knock notes are never sent. What each feature does send is itemised in the security and data protection policy.
Customer-configured integrations
A Customer may connect one or more external CRM or organising systems so that supporter and volunteer records stay in step between Cadrify and that system. These integrations are optional, configured per branch and use credentials issued by the Customer for their own account in the external system. For these systems the Customer is the controller and the external provider is the Customer’s processor, not Cadrify’s.
| System | Direction | Data exchanged | Region |
|---|---|---|---|
| Action Network | Both ways | Supporter contact records: name, email, postal address and postcode, tags | United States |
| NationBuilder | Inbound, outbound available | Supporter contact records: name, email, phone, postcode | United States |
| Salesforce | Inbound, outbound available | Contact records: name, email, mobile, postcode, contact preferences | The Customer’s own Salesforce instance |
| Movement | Inbound, outbound available | Supporter contact records and sign-up activity: name, email, phone, subscription status | Per the Customer’s Movement account |
Credentials for these integrations are stored encrypted at the application layer (see §2). Inbound data is written only to the volunteer-and-supporter contact records. No electoral register record and no door-knock or other special-category data is ever included in any integration sync. Because these systems are chosen and controlled by the Customer, the contact data they exchange may be processed outside the UK or EEA. The Customer is responsible for the lawful basis and transfer safeguards for their own CRM.
Reference-data and geocoding services
Cadrify calls a number of public services for reference data and geocoding. These receive only postcodes or public area codes, never names, full addresses linked to a named person, or any register record:
- postcodes.io (Britain) for postcode-level geocoding, receiving postcodes only.
- Democracy Club and the Electoral Commission (Britain) for elections, candidates, party emblems and per-postcode polling-station lookups. Polling-station lookups send postcodes only.
- mySociety MapIt, Ordnance Survey and the Office for National Statistics (Britain) for boundary geometry and census reference data, addressed by GSS area code. No personal data is sent.
Per-address geocoding through Google Maps or Nominatim is available as an optional backend. When enabled it sends address text, without a person’s name, to that provider. It is not used by default. Postcode-level geocoding via postcodes.io is the standard path.
Browser-side services on public pages
On the public marketing and legal pages only, the browser loads a cookieless analytics script (Plausible) and a demo-booking widget (TidyCal). Neither is loaded on authenticated application pages, so URLs that contain record identifiers are never sent to them. Map views load vector tiles directly from OpenFreeMap. Those requests carry map coordinates and nothing else. Map and chart libraries are served from Cadrify’s own origin, not a CDN.
In summary: electoral register data and door-knock (special-category) data never leave the platform to any third party. Supporter and volunteer contact data leaves only to the Customer’s own configured CRM integrations. Email content goes only to the email provider and geocoding and reference lookups receive only postcodes or public area codes.
14. Electoral register data handling
Electoral register data is subject to the Representation of the People Act 1983 and the Electoral Registration and Administration Act 2013. Access to the full register is restricted to the purposes defined in law. Cadrify enforces the following additional controls specific to this data category:
- Register data is stored in the same PostgreSQL instance as all other data, scoped
to the importing organisation by
org_idand covered by the row-level security policy in §6. - Access to elector records requires at minimum organiser-level role or an active door-knock/GOTV assignment for activist-level users (who see only their assigned electors).
- All elector data access is behind authentication. There are no public endpoints that return elector records.
- Electoral register records are never included in any external CRM integration sync. Only volunteer-and-supporter contact records sync to a Customer’s configured systems (see §13). Postcode-level geocoding and polling-station lookups send postcodes only, never elector records.
- Door-knock data (voting intentions, core party affiliation, support scores) that constitutes special-category data under Article 9 UK GDPR is handled under the same access controls and logged in the same audit trail.
- Organisation admins are responsible for ensuring that their lawful basis for holding and processing the electoral register is documented and maintained.
15. Incident response and vulnerability disclosure
Incident response
In the event of a confirmed security incident affecting customer data, Cadrify will:
- Contain the incident and preserve evidence.
- Assess the scope and nature of data affected.
- Notify affected Customer organisations without undue delay and in any event within 72 hours of becoming aware where applicable, including the nature of the incident, categories and approximate number of data subjects affected, and measures taken or proposed, to support Customers in meeting their own breach-notification obligations.
- Cooperate with Customer organisations in any required notifications to the ICO or affected data subjects.
- Provide a post-incident report to affected Customers on request.
Responsible disclosure
If you discover a potential vulnerability in the Cadrify platform, please report it to security@cadrify.org before public disclosure. Please include:
- A clear description of the vulnerability and its potential impact
- Steps to reproduce (or proof of concept)
- Any affected URLs, endpoints or components
We will acknowledge receipt within 2 business days and aim to provide an initial assessment within 5 business days. We ask that you allow us reasonable time to remediate before any public disclosure. We will not take legal action against researchers acting in good faith under this policy.
Security contact
Email: security@cadrify.org
For data protection and privacy questions: privacy@cadrify.org
16. Backup and recovery
Database backups
The PostgreSQL database is backed up automatically by Render’s managed database service. Backups are taken daily and retained for a minimum of seven days. Point-in-time recovery (PITR) is available within the retention window, allowing the database to be restored to any point in time rather than only to a daily snapshot.
File storage
Uploaded register files and other persistent data stored on the application’s persistent disk are covered by Render’s disk snapshot capability. Snapshots are taken periodically and retained in accordance with Render’s infrastructure practices.
Restore testing
Restore procedures are tested periodically to verify that backups are valid and that recovery objectives can be met. Formal recovery time and recovery point objectives (RTO / RPO) are documented internally and shared with enterprise customers on request.
Relevance for political campaigns
Electoral register records, door-knock history and GOTV records are what a campaign cannot work without in the final weeks. A restore puts those back first.
17. Dependency management and patching
Dependency tracking
Application dependencies are pinned in a requirements file and reviewed regularly. Known vulnerability databases (including GitHub Dependabot alerts and the Python Packaging Advisory Database) are monitored for security advisories affecting packages in use. Continuous integration runs an automated dependency vulnerability scan (pip-audit) and a static application security analysis (bandit) on every change, alongside linting and type checks.
Patch cadence
Security-relevant dependency updates are applied with priority. Critical vulnerabilities in direct dependencies are targeted for remediation within seven days of a confirmed advisory. Lower-severity updates are applied in the normal release cycle. The operating system and runtime environment are kept current by Render’s managed infrastructure.
Infrastructure updates
Render manages patching of the underlying compute infrastructure, container runtime, and database engine. Cadrify reviews Render’s security communications and applies application-level changes required by infrastructure updates promptly.
18. Secrets management
Secret storage
Application secrets (database credentials, API keys, cryptographic signing keys, email service credentials) are stored exclusively as environment variables in Render’s secret environment configuration. No secrets are committed to the application code repository. Environment files are excluded from version control and code review guards against accidental secret inclusion.
Encryption key for stored secrets
The application-layer encryption described in §2 uses a dedicated encryption key supplied as its own environment variable, held separately from the database. Where a dedicated key is not configured the key is derived from the application signing key, which is acceptable for development but a dedicated key is used in production. A re-encryption procedure is available so that stored integration credentials remain readable across a key change.
Secret rotation
Secrets are rotated when a person with access to them leaves and immediately on any suspected compromise. Because sessions are server-side tokens rather than signed cookies, rotating the application signing key does not sign users out. Rotating the integration encryption key uses the re-encryption procedure above so no stored credential is orphaned.
Access to secrets
Access to production environment secrets is restricted to a minimal set of platform operators. Secret values are not logged, echoed in error messages or included in application responses under any circumstances.
19. Privacy and compliance alignment
Cadrify’s security controls are designed to meet its obligations as a data processor and, where applicable, as a data controller under UK data protection law. The table below maps key regulatory obligations to the platform controls that address them.
| Regulation | Obligation | Cadrify controls |
|---|---|---|
| UK GDPR / DPA 2018 | Art. 5(1)(f), integrity and confidentiality (security principle) | TLS in transit · AES-256 at rest · RBAC · session management · account lockout |
| Art. 25, data protection by design and by default | Least-privilege roles · org_id data isolation · PostgreSQL row-level security · minimal data sent to geocoding APIs · no analytics cookies | |
| Art. 28, processor obligations | Written DPA with each Customer · subprocessor DPAs · audit trail for operator access · prompt breach notification to support Customer obligations | |
| Art. 33, breach notification | Incident response procedure (see §15) · notification to Customers without undue delay to support their regulatory obligations · cooperation with ICO notification where required | |
| UK GDPR Art. 9, special category data | Political opinions (voting intentions, scores) require an Art. 9(2) condition in addition to an Art. 6 lawful basis | Customer (as controller) must document their Art. 9(2) condition (typically (d) for political parties). Cadrify processes only under Customer instructions and applies heightened access controls and audit logging to all door-knock data. |
| PECR 2003 | Cookies, consent requirements for non-essential cookies | No advertising cookies used. The marketing-site analytics is cookieless. Session and auth cookies are strictly necessary. Functional cookie (signer_info) is disclosed in the Cookies policy. No consent banner required for current cookie set. |
| RPA 1983 (as amended) Electoral Registration and Administration Act 2013 |
Full electoral register, restricted to lawful purposes only, with supply and use tightly controlled | Authentication required on all elector endpoints · org_id scoping · activist access limited to assigned electors only · no public API exposing register data · Customer is responsible for documenting their lawful basis under RPA for holding the register |
| UK GDPR Chapter V, international transfers | Transfers outside UK/EEA require adequate safeguards | Database in Frankfurt (EEA, UK adequacy). Email delivery via Resend in Dublin, Ireland (EEA, UK adequacy). Electoral register and door-knock (special-category) data stay within the EEA. Customer-configured CRM integrations may transfer supporter contact data outside the UK or EEA under the Customer’s own controller relationship and transfer safeguards (see §13). |
20. Security roadmap
The controls above are what is built today. The items below are planned or in progress. We publish them so a reviewer can see what is coming and what is not, rather than find out in month two.
| Control | Description | Status |
|---|---|---|
| Content Security Policy hardening | Replace the 'unsafe-inline' script and style allowances in the current enforcing policy (see §10) with per-response nonces |
Planned |
| Multi-factor authentication (MFA / 2FA) | TOTP-based second factor for all platform users, with admin-enforced MFA policy per organisation | Planned |
| Customer-facing audit log export | Self-service CSV export of the audit log within a Customer’s organisation, available to the organisation’s data protection officer role | Available |
| Automated anomaly alerting | Real-time alerting on login failure spikes, account lockout events and operator access outside normal hours | Planned |
| External penetration testing | Annual CREST-accredited or equivalent penetration test of the application and infrastructure, with results shared on request | Planned |
| Organisation SSO | OIDC integration allowing organisations to authenticate users via their own identity provider. SAML 2.0 support is not yet built | Available (OIDC) |
| Customer-managed retention | Organisation admins set their own data retention period for electoral and door-knock data, with automated deletion | Planned |
| SOC 2-aligned controls | Formal documentation of security policies, access review procedures, change management and vendor management aligned to SOC 2 Trust Service Criteria | Planned |
| Encryption key management | Extend application-layer field encryption beyond integration secrets to the highest-sensitivity campaign data (political opinions, elector data), with a customer-managed key hierarchy | Under evaluation |
Questions about the security roadmap or specific controls can be directed to security@cadrify.org.