Important: Tradelynx introduces customers and independent traders. Any work contract is between customer and trader.

Security & data protection pack

Tradelynx Limited · Company No. 17064008 · Contact: security@tradelynx.co.uk

Content current as at 28 July 2026 · page generated 26 August 2026

Every section below is quoted from the internal document that owns it, so this page cannot say more than our own records do. Where a control is planned rather than in place, the source says so and so does this page. Live operational status is at tradelynx.co.uk/status.

What we commit to

Source: SECURITY_BRIEFING · reviewed 5 August 2026

  • Access limited to what your role needs, and removed the day it stops being needed.
  • No blame for reporting a mistake — including your own. The only unrecoverable error here is a delayed one.
  • This briefing kept current, and re-read annually.

What personal data we hold

Source: DATA_RETENTION · reviewed 29 July 2026

CategoryWhere storedExamples
Customer contactlead_private.customer_*, customer_profiles, auth.usersname, email, phone, postcode
Customer property dataproperties, property_history_entries, property_access_codes (encrypted)property address, recorded work history, access codes
Customer messagesmessages, lead_messages, message attachments (Storage)free-form text, images
Trader contacttraders, auth.usersname, business name, email, phone, trade
Trader credentialstrader_credentials, trader-docs Storage buckettrade-card photos, AI-extracted metadata
Identity verificationtraders.verification_status, user_identity_verifications, DiDit's own systemsDiDit session id, status
Payment metadatapayments_audit, credit_ledger, Stripe's own systemsStripe payment intent ids, amounts (no card data)
Telephony audittelephony_audit, Telnyx's own systemscall durations, outcomes, recording paths if any
Audit logsaudit_events, cron-monitor Sentry eventsuser_id, event_name, metadata, timestamp
DSAR requestsdata_subject_requestsrequest type, deadline, resolution notes
Outbound communicationsnotification_queue, Resend's logsemail contents (transient), status
Mobile push tokensmobile_push_tokensdevice tokens (Apple / FCM)

How long we keep it

Source: DATA_RETENTION · reviewed 29 July 2026

CategoryRetention periodTriggerMechanism
Customer contact (active)Lifetime of accountAccount existsManual deletion via DSAR or /api/admin/customers/anonymise
Customer contact (after deletion)None — anonymised in placeDSAR erasure request/api/admin/customers/anonymise scrubs lead_private.customer_* and customer_profiles; preserves minimal audit row
Customer property dataLifetime of property recordManual deletion via DSARAnonymisation on request; access-code rows are AES-256-GCM encrypted at rest
Customer messages + attachmentsLifetime of associated lead/jobLead/job deletionManual deletion via DSAR; storage objects removed via admin.storage.remove([path])
Trader contact (active)Lifetime of accountAccount activeSelf-service close via /api/trader/close-account (synchronous anonymisation)
Trader credentials (active)Until card expires or replacedstatus changeStays active while in use
Trader credentials (expired/rejected raw evidence)7 years from last status changecron/retention-cleanup (monthly)Storage object deleted from trader-docs/; ai_extracted_* fields nulled on the row; minimal audit fields (credential_type, expiry_date, status, verified_at, admin_notes) preserved
Identity verificationSame as trader accountTrader closureAnonymised with the trader row
Identity verification — at DiDit6 monthsConfigured in the DiDit Business Console, 2026-08-05Their default is INDEFINITE unless the Client sets a period (DPA §2.1). Nothing in lib/didit/ sets it, so this is a Console setting and it is the only thing standing between us and unlimited retention of identity documents and facial biometrics. Re-check at each quarterly Management Review — a setting made in a vendor console leaves no trace in this repository
Payment metadata6 yearsTax / accounting requirementManual / not subject to DSAR erasure (HMRC requires 6 years for company records)
Telephony auditLifetime of platform recordManual cleanupCurrently no auto-purge; row size manageable
Audit logs (audit_events)Lifetime of platform recordManual cleanupCurrently no auto-purge; designed for retention as a security control
Trader books (trader_expenses, trader_manual_income)While the trader account lives (they are the trader's business records — HMRC expects 5+ years); voids retained in revision history (digital-record integrity)Trader-initiated; export-then-delete on account deletion per the existing GDPR flowCHG-2026-0824, DPIA G-A2
Enterprise sensitive-access log (org_sensitive_access_log)12 months (founder decision 2026-08-01, CHG-2026-0823)cron/retention-cleanupSurface/actor/count metadata only — no PII (CHG-2026-0786); 12 months preserves a full annual audit cycle
DSAR records (data_subject_requests)Lifetime of platform recordRequired for compliance auditRetained as the audit trail of GDPR fulfilment
Outbound notification queueIndefinite — nothing purges itNone⚠ See "Storage limitation gap" below. Holds recipient_email, html_body and payload in plaintext. The previous entry claimed ~30 days; that was never implemented (CHG-2026-0780)
Mobile push tokensLifetime of device installApp-side revoke calls or DSARDeleted on app uninstall (best-effort)
Sentry events90 days (Sentry default plan)Sentry built-inSet in Sentry project settings
Vercel logs7 days (Vercel Pro default)Vercel built-inSet in Vercel project settings
Stripe dataStripe's own retentionStripe built-inWe do not separately retain Stripe customer data
Business-development prospects and contacts (sales_prospects, sales_prospect_contacts, sales_outreach_drafts)12 months uncontacted or after no reply; 24 months where a conversation happenedCreation date, or last contact / last pipeline activity. An objection deletes contact details at once, leaving a suppression record only/api/cron/prospect-retention (daily). LIA: compliance/lia/2026-08-26_LIA_business_development.md (CHG-2026-1157)

Where the data lives

Source: DATA_RETENTION · reviewed 29 July 2026

AssetServiceRegion
DatabaseSupabase Postgreseu-west-2 (London)
Object storageSupabase Storageeu-west-2 (London)
Application runtimeVerceleu-west-2 (multi-region edge)
Email deliveryResendEU (eu-west-1, hosted by Resend)
TelephonyTelnyxEU PoP
Payment processingStripeEU (Stripe's UK / Ireland / NL)
Identity verificationDiDitEU
Error monitoringSentryEU
AI inferenceAnthropicUS (model calls) — only non-PII content sent (moderation prompts redact identifiers); Anthropic does not train on API content

All personal data of UK data subjects rests in the UK or EEA.

What happens in a breach

Source: BREACH_NOTIFICATION · reviewed 5 August 2026

0. Start the clock and write it down (minute 0)

Open an incident record from governance/templates/incident-template.md immediately — before investigating. Fill in Detected to the minute, and answer the regulatory-notification block at the top. The record is the clock: both deadlines are computed from that timestamp.

Do not wait for certainty to open the record. An incident that turns out to be nothing is closed in a line; an incident opened three hours late has burned three hours of a 72-hour budget.

1. Contain, then preserve (first hour)

Stop the bleeding, then stop destroying evidence. Revoke the credential, disable the route, take the surface down. Do not delete logs, rotate away the affected keys' audit trail, or clean up the data before it has been captured — both regulators will ask what was accessed and by whom, and the audit tables (property_data_access_log, data_disclosure_log, audit_events) are the answer.

2. Establish the four facts both regulators ask for

1. What happened — plain sentences, no speculation about cause. 2. What data, and whose — categories and approximate numbers of people. Never the data itself: the ICO explicitly says not to include personal data in the report form. 3. Likely consequences for the people affected. 4. What you have done and will do — containment already applied, and the remediation planned.

If you do not have all four at 71 hours, report anyway. Both regulators accept a partial report followed by an update; neither accepts silence.

3. Notify HMRC

Log the Developer Hub support ticket. Include: application name (Tradelynx Books), the four facts above, and the breach contact name and telephone number — that is a stated requirement, not a courtesy. Record the ticket reference in the incident record.

4. Notify the ICO

Complete the online form. Allow about 30 minutes. The ICO responds within seven calendar days with next steps. Record the reference in the incident record.

5. Tell the people affected, if the risk is high

Where the breach is likely to result in a high risk to people's rights and freedoms, they must be told directly and without undue delay, in plain language. This is a separate duty from notifying the ICO, and it is the one most often missed.

6. Close it honestly

The incident record's regulatory block must carry both references and both timestamps before the record moves to Resolved. A guard enforces this: tests/unit/breachNotification.spec.ts fails the build if an incident marked as a breach reaches Resolved without them.

Backups

Source: BACKUP_RECOVERY · reviewed 28 July 2026

AssetBackup mechanismRetentionSource of truth
Supabase Postgres databaseDaily physical backups via WAL-G; point-in-time recovery (PITR) currently disabled (see "PITR — deliberate deferral" below)7 daily snapshots rollingSupabase project mmwcvinqnacmarnazvve
Supabase Storage bucketsReplicated within the storage layer; not separately snapshottedLifetime of the bucket; deleted objects are not retained beyond Supabase's internal retention (~30 days)Buckets: trader-docs, lead-media, support-media, org-property-photos, tool-assets, market-readiness, property-media, branding, telephony-prompts
Stripe customer + payment dataHeld by Stripe; we do not back it upStripe's own retentionStripe dashboard
Source code + migrationsGit, GitHubIndefinitegithub.com/tradelynx/tradelynx
DocumentationGit, GitHubIndefinitedocs/ in this repo
Environment variablesVercel project settings + local .env.localVercel retains historical values across deployments; .env.local lives only on developer machinesVercel dashboard

Recovery objectives (RTO / RPO)

Source: BACKUP_RECOVERY · reviewed 28 July 2026

ObjectiveTargetJustification
RPO (data loss tolerance)24 hoursDaily snapshots are the constraint. PITR would drop this to ~2 minutes; see "PITR — deliberate deferral" below for why it is not enabled today.
RTO (time to restore)4 hours for full DB restore from snapshotIncludes: incident detection, decision to restore, Supabase restore operation (~5–15 min wall clock), spot-check, reconciliation against Stripe / Telnyx / Resend / audit logs, traffic resumption. Validated end-to-end by the 2026-05-03 restore drill.
MTTR for partial-table recovery1 hourSingle-table or single-row restore from snapshot via SQL; faster than a full project restore

PITR — deliberate deferral

Point-in-Time Recovery is a documented and ready follow-up, not an oversight. It is not enabled today because the cost (~£150–250/mo including the required compute upgrade) is not justified by the current data-at-risk profile: pre-revenue, 4 verified traders, no paying enterprise customers, and a daily-snapshot RPO that loses at most a small number of leads and messages in a worst-case 24-hour window. The procedure to enable PITR is a single dashboard action (Settings → Add-ons → Point-in-Time Recovery) that takes effect within minutes.

Trigger conditions for enabling PITR. Any of:

1. First paying enterprise customer signed (any tier — validates the pricing infrastructure and changes the data-at-risk profile) 2. First insurer or lender in active sales conversation (regulated buyers will require shorter RPO commitments in their procurement review) 3. Database size exceeds 4 GB (Supabase's own production checklist recommends PITR above this threshold) 4. Daily Stripe completion-fee volume exceeds £500 (i.e. 24 hours of lost data starts representing material financial reconciliation work)

Compensating controls in the absence of PITR. Daily Supabase snapshots cover the database. External systems (Stripe events, Telnyx call records, Resend email log, Sentry events) provide a secondary recovery surface for the 24-hour worst-case window — the "Reconcile against external sources" step in the restore procedure below documents how. The 2026-05-03 restore drill exercised the clone-and-restore path and confirmed the procedure works.

Penetration test — scope

Source: PENETRATION_TEST_2026-08 · reviewed 5 August 2026

What was tested: the Tradelynx web application, running locally from the current main, exercised over HTTP with automated templates and manual probes. 6,384 requests.

What was NOT tested:

  • A third-party engagement has not happened. This is tool-assisted in-house testing. Tools and an independent tester are not the same thing, and the answer to HMRC should say "tooling", not "we passed a penetration test".
  • The deployed edge — the WAF, TLS termination and CDN behaviour — was not scanned, for the reason in the incident note below. Production security headers were verified by inspection, not by attack.
  • Authenticated attack surface was not driven by the scanner. Gated routes were confirmed to refuse anonymous callers, but post-authentication testing (privilege escalation between real roles) rests on the standing test suite rather than on this scan.

Penetration test — results

Source: PENETRATION_TEST_2026-08 · reviewed 5 August 2026

Automated templates: 2 informational results, both triaged to nothing. No exposures, misconfigurations, default credentials, injection points or known-CVE matches.

ResultSeverityVerdict
robots.txt fileinfoExpected. We serve one deliberately
SNMPv3 Fingerprint - DetectinfoFalse positive, verified. The template reported the host, not the application. Nothing listens on UDP/161 (lsof -nP -iUDP:161 → zero listeners); the only listener on the tested port is the Node process serving the app. A scanner artefact, not a service

Triaging both rather than reporting "0 findings" matters: the SNMP line would look alarming in a report and dismissing it silently would be indistinguishable from missing it.

Manual probes

ProbeResult
/.env, /.env.local, /.git/configNo response — not served
/api/debug, /server-statusNot present
/admin unauthenticated307 to sign-in
/api/admin/users unauthenticated401
/.well-known/security.txtPresent — a disclosure route exists
robots.txt, sitemap.xmlPresent, as intended

Production security headers (verified 2026-08-05, live)

HeaderValue
strict-transport-securitymax-age=63072000; includeSubDomains; preload
content-security-policydefault-src 'self' … object-src 'none', base-uri 'self', form-action 'self'
x-frame-optionsDENY
x-content-type-optionsnosniff
referrer-policystrict-origin-when-cross-origin
permissions-policycamera, microphone, geolocation denied; payment scoped to Stripe

Standing security suite

39 checks across 11 files pass: OTP verification policy, org authorisation, tenant access-token validation, invoice-token validation, signup upload guards, Turnstile, property-contribute consent, an IDOR heuristic, an SSRF fetch guard and the API scope registry.

Complementing them nightly against production: a sweep asserting 96 gated routes × 2 verbs refuse anonymous callers, and a check that every admin page refuses an anonymous visitor.

What this pack deliberately leaves out

Named here rather than quietly absent, so you can judge the omission for yourself and ask.