Security Whitepaper
This document describes the technical security controls implemented in the Cadrify platform. It is intended for IT security reviewers, procurement teams, and data protection officers conducting due-diligence assessments. A non-technical summary is available on the Security overview page.
Executive summary
What Cadrify protects
Cadrify is a multi-tenant platform for UK 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 live operational data used to coordinate election-day activity. Security is treated as a core product requirement, not an afterthought.
Cadrify processes this data as a data processor on behalf of subscribing political organisations. Each organisation’s data is logically isolated from every other organisation on the platform. Platform operator access to customer data is restricted, audited, and limited to support and incident response scenarios.
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 audit 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 · no public data APIs |
| Session hijacking | Low–Medium, 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 & supporter data | Activist contact records, opt-in status, event sign-ups | UK GDPR Art. 6, Consent / Legitimate interests (Customer’s) | Medium–High |
| Electoral register data | Full names, addresses, elector numbers, dates of birth, ward data | RPA 1983 (as amended); Customer must hold lawful basis under 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–High |
| Political opinion data | Voting intentions, G-scores, party preferences, door-knock responses | UK GDPR Art. 9, Special category. Customer must document Art. 9(2) condition (typically (d)). | Very High |
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 | §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 audit log · operator audit log · 12-month retention | §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, provisioned as a managed Render Postgres instance in Frankfurt. The application uses parameterised queries exclusively; no user-supplied input is ever interpolated directly into query strings. No raw string interpolation into query text is permitted.
Application stack
The application is a Python web application using the Flask framework with a domain-driven blueprint architecture. Rendering is server-side Jinja2, progressively enhanced with vanilla JavaScript. There is no single-page-application framework. Pages that display maps load the MapLibre GL library from a public CDN, constrained by the Content Security Policy (see §10).
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; all HTTP requests are 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-group 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 | Prevents JavaScript access; mitigates XSS-based session theft |
Secure |
true (production) | Prevents transmission over plain HTTP |
SameSite |
Lax | Prevents cookie submission on cross-site requests; mitigates CSRF |
Max-Age |
86 400 s (24 hours) | Limits session lifetime, requiring daily re-authentication. 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 group context. Switching between groups 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 group:
| Role | Scope | Access granted |
|---|---|---|
| Activist | Assignment-only | Electors and doors within active door-knock / GOTV assignment only; no register browsing |
| Organiser | Exact group | Data management and user management within the assigned group; no inheritance to parent or sibling groups |
| Admin | Group and all descendants | Full management of the group and all child groups; cannot access peer 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-group administrator (organisation-wide settings) from an administrator of a sub-group, 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 Forbidden
- Is an organisation/group 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 audit 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 group hierarchy using a recursive CTE query. A user who is admin of a parent group is implicitly admin of all descendant groups; 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 group 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 audit 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. Customer data is required to carry an organisation identifier that is checked on read and write operations. Scoping is enforced in the repository layer. Route handlers do not construct their own query predicates, and the value is always sourced from the authenticated session context, not from user-supplied input.
The risk of cross-tenant leakage is limited to application-layer bugs. Cadrify’s automated test suite includes explicit cross-organisation access assertions to guard against this class of error.
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
.strip().lower()ed; name fields are .strip()ed; numeric IDs
are cast with int() and caught on exception. 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 additionally 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 deploys 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 |
| 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 |
The application runs as a single worker process with multiple threads, so these in-process counters are consistent across concurrent requests. They are held in memory, not shared across multiple processes, and reset on deploy or restart. The durable, restart-surviving 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
Every login attempt, successful or failed, is written to the
auth_audit_log table with timestamp, actor IP address, and outcome. Login
failures are counted per account in a rolling 30-minute window; patterns that trigger
account lockout are also recorded. This log provides the primary signal for detecting
credential-stuffing campaigns or targeted account attacks.
Audit trail
All 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_lockedmagic_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 audit 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.
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)- God-user 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 |
|---|---|---|
| auth_audit_log | Minimum 12 months | Available to org admins on request |
| Operator audit 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 and access review tooling are on the roadmap (see §20).
Audit logging is designed to be highly available and failure-tolerant. Logging failures are independently captured and investigated, without affecting user availability or rolling back the triggering operation.
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 plus a small allowlist of trusted CDNs
(jsDelivr, unpkg, cdnjs) used for front-end libraries such as MapLibre GL. Map tiles load
from tiles.openfreemap.org (permitted in connect-src, with
worker-src 'self' blob: for the map renderer). On public marketing pages the
policy additionally permits the cookieless analytics endpoint and the demo-booking widget.
The policy currently allows 'unsafe-inline' for scripts and styles. This is a
known limitation, retained 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 (see §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 navigate away 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 and database | 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 EEA (eu-west-1, Ireland) |
| postcodes.io | Postcode geocoding | UK postcodes only (not linked to named individuals in the request) | UK-based 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. Self-service billing is not yet enabled in the platform. No payment-card data is processed by Cadrify and no payment processor currently receives any data. When billing launches, the payment processor will be listed here and card data will be handled by that processor directly, never stored by Cadrify.
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 group, 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 (UK) for postcode-level geocoding, receiving postcodes only.
- Democracy Club and the Electoral Commission (UK) 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 (UK) 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, and map and chart libraries from public CDNs. These requests carry only map coordinates and asset paths, no personal data.
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_id. - 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, party preferences, 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
Campaign data, particularly electoral register records, door-knock history, and GOTV operation records, is operationally critical during election periods. In the event of a data loss incident, Cadrify’s recovery objective prioritises restoration of these data categories within the shortest feasible window.
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 on a periodic basis and immediately on any suspected compromise. Session signing keys and API credentials are rotated independently of application deployments, with the application configured to tolerate a brief transition window where both old and new credentials are valid.
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 · 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, G-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; 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 described in this document represent Cadrify’s current security posture. The following improvements are planned or in active development. This roadmap is published to demonstrate commitment to continuous improvement, not to imply that current controls are insufficient.
| 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 export of authentication audit log entries within a Customer’s organisation, in CSV and JSON formats | In development |
| 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 | SAML 2.0 / OIDC integration allowing organisations to authenticate users via their own identity provider | Planned |
| 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.