Security & data protection pack

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

Content current as at 5 August 2026 · page generated 28 October 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 3 October 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 3 October 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 identity evidence (trader_verifications.didit_payload)3 months from account closurecron/retention-cleanup (daily)The raw DiDit identity payload is nulled. The decision and its trail (status, verified_at, failure_reason, check columns) are preserved. Founder decision 2026-09-16, CHG-2026-1508: identity evidence is the most sensitive material held and nothing we may later be asked about depends on keeping it once the decision is recorded
Trader credentials (expired/rejected raw evidence)7 years from last status changecron/retention-cleanup (daily)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 and CIS (trader_expenses, trader_manual_income, trader_cis_statements, trader_cis_profile, receipts in expense-receipts)While the account is open: kept, they are the trader's own records. After closure: 6 years after the end of the UK tax year each record relates to (founder decision 2026-10-03, HMRC record-keeping), then deleted; the CIS profile when the closure's own tax year passes that pointcron/retention-cleanup (daily, lib/retention/sweeps.ts); the closure dialog offers the books CSV firstCHG-2026-1720 (replaces the earlier "export-then-delete on account deletion", which was never built)
Homeowner invites (trader_homeowner_invites: a non-user's name, email, address)30 days after the invite ends (accepted, withdrawn or expired); closure withdraws open invites, which starts the clockcron/retention-cleanup (daily)Founder decision 2026-10-03, CHG-2026-1720
Accountant access and work-record share links (accountant_grants, trader_work_record_shares)Revoked at trader closure; both readers also refuse a closed trader. A revoked grant is deleted 12 months after revocation (the access history stays in data_disclosure_log); a share link 90 days after it expires or is revokedclose-account (revokes before anything else; fails closed); cron/retention-cleanup (daily deletes)Founder decisions 2026-10-03, CHG-2026-1720
Unsubscribe tokens (email_unsubscribe_tokens: SHA-256 of a link's token, the recipient address, the campaign id)12 months after the row is created (one row per marketing email sent). A link older than that is refused as an unusable link even before its row is deleted, and the page tells the person to email privacy@. The suppression it created, if used, is separate and stayscron/retention-cleanup (daily, sweepExpiredUnsubscribeTokens, batched: 500 rows per delete, at most 40 batches a run)Founder decision 2026-10-04 14:47 UTC (review page, R2), CHG-2026-1768
Insurer claim packs (insurer_claim_packs)6 years from issue (billing and disclosure evidence). On the owner's erasure the pack is reduced to its totals (insurer and invoice id stay on the row) plus a SHA-256 of the pack as issued, and logged as *retained (reduced)*cron/retention-cleanup (6-year delete); anonymiseCustomer (reduction)Founder decision 2026-10-03, CHG-2026-1720
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 queue30 days after sending; 90 days for a failed sendcron/retention-cleanup (daily)Holds recipient_email, html_body and payload in plaintext, so the period matters. Enforced since CHG-2026-0782 in code, reached on schedule only since CHG-2026-1670 (2026-10-02), and daily since CHG-2026-1682: before that the job was monthly and a row could sit for nearly 60 days. Live work (queued, sending) is never purged. History in "Storage limitation gap" below
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 3 October 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). See "AI vendors" below for what is sent
AI inferenceOpenAIUS (model calls). See "AI vendors" below for what is sent

All personal data of UK data subjects is stored in the UK or EEA. It is processed in the US when it is sent to an AI vendor, as below. *(Corrected 2026-10-03, CHG-2026-1720: this section previously said only non-personal content went to Anthropic, which was not true.)*

AI vendors: what each receives, and the transfer basis

Anthropic (Claude). Receives personal data in these cases:

  • Images: trader credential card photos (extraction); receipt and invoice images (trader expenses, home inventory, Passport service entries); images sent in messages (screening for contact details); job-lead photos.
  • Transcripts: call transcripts, for AI-10 call summaries.
  • Property facts: these feed market-readiness reports, maintenance predictions (AI-05), and the Passport, guide and golden-thread assistants, together with the question the user asks.

Inputs are minimised to what each task needs. Anthropic does not train on API content by default.

  • Contract: Art. 28 DPA, identified 2026-08-05.
  • Transfer mechanism: EU Standard Contractual Clauses with the UK Addendum (compliance/registers/processor-register.md; DPIA G8).
  • Open items: confirming that the commercial terms are in force, a zero-retention decision, and the transfer risk assessment, all tracked in governance/OUTSTANDING.md (G7/G8).

OpenAI. Receives trader verification documents for the AI-01 pre-screen (vision); a human makes every decision.

It also runs search-visibility checks, whose prompts are about Tradelynx only and carry no personal data.

  • Contract: Art. 28 DPA, identified 2026-08-05.
  • Transfer mechanism: EU Standard Contractual Clauses under that DPA. The UK transfer mechanism (IDTA or UK Addendum) and the training/retention posture are not yet confirmed (processor register).

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 3 October 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)Daily snapshots, rolling (9 completed of 9 held on 2026-10-03)Supabase 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 (14, verified by a read of the live bucket list on 2026-10-03): private trader-docs, lead-media, support-media, org-property-photos, tool-assets, market-readiness, property-media, labour-profile (worker CVs and avatars), blog-assets, expense-receipts (trader receipts), user-contributions (contribution evidence, created 2026-10-02); public branding, telephony-prompts, marketing-media. Storage objects are not in the database snapshot: a database restore brings back the storage.objects metadata rows, not the files, so a restored row can point at a file deleted after the snapshot
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 3 October 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. Not yet validated end to end. The 2026-08-31 drill exercised the snapshot, restore and verify steps on synthetic data in a scratch project; maintenance mode, external reconciliation and a restore of the production snapshot have not been timed.
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.

Reaffirmed 2026-08-31 by the founder, on the back of the first restore drill (CHG-2026-1269): *"Not yet. I'll wait until the project is making money."* That is trigger 1 and trigger 4 below, stated as a revenue test rather than a date, and it is the right shape: the cost is certain today and the exposure is not. The drill was run under this posture and does not change it. What the drill DID establish is that the procedure works, so when a trigger fires the only thing changing is the RPO, not our familiarity with the recovery path.

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. That step has not yet been exercised in a drill: the 2026-08-31 drill had no scratch equivalent of it (see "Last tested").

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.