Skip to main content

GDPR Posture

This document is the technical reference for how the Broadford Living platform implements its GDPR obligations. It is the source of truth for replies to supplier and controller questionnaires, and should be updated whenever the underlying code changes.

For incident response, see data_breach.md. For outstanding work, see gdpr_remaining_work.md at the repository root.

Roles

  • Controller: Broadford (the building owner / fund / managing entity for the property).
  • Processor: Practicable Systems Ltd, operating the platform on Broadford's behalf.
  • Sub-processors (each requires its own DPA, signed or pending):
    • Cloudflare — primary cloud infrastructure (Workers, Durable Objects, D1, KV, R2, queues).
    • Clerk — identity provider (passkeys, magic codes, sessions).
    • SmartThings — device platform for sensors, locks, alarms.
    • Yale — smart locks and video doorbell.
    • Daikin — heat-pump (Onecta cloud).
    • Hive — selected legacy thermostats.
    • PostHog (EU) — observability / access logs.
    • Supabase (EU) — anonymised analytics warehouse.
    • Airzone (post-integration) — heat-pump zoning.

Categories of personal data

The platform processes a deliberately small set of personal data:

CategorySourceWhere it livesNotes
Tenant email addressProperty manager via inviteD1 invites, Clerk, per-tenant User Durable ObjectActs as the tenant's primary identifier.
Smart-home usage data (lock events, motion, temperature, energy)Devices (SmartThings / Yale / Daikin)Per-property User Durable Object SQLiteTenancy-bounded.
Video doorbell streamYalePass-through only — not persisted on the platformRecording (if any) lives at Yale.
Guest email addresses (granted by tenant)Tenant via guest invitePer-property User Durable ObjectSubject to the tenant's own access controls.

Tenant phone numbers, addresses (other than the property address), payment info, and biometric data are not processed.

Lawful basis

  • Contract (GDPR Art. 6(1)(b)) for tenant data processed in fulfilment of the tenancy agreement.
  • Legitimate interests (Art. 6(1)(f)) for security audit logs and breach-detection telemetry — necessary to fulfil our duty of care under the tenancy.
  • No processing relies on consent at this time.

Access control architecture

Access to data is gated at three layers, all enforced server-side. Casual readers can stop at the table; deeper notes follow.

LayerCodeEffect
Identitycrates/clerk-auth/src/middleware.rs and crates/bf_resident/src/middleware/auth.rsVerifies the Clerk JWT, populates ClerkAuth extension, and emits the actor's user_id to the PostHog request log.
Role / status gatecrates/bf_user/src/permissions.rs (check_permission and the per-role check_*_permission helpers)Enforces the UserRole × PropertyStatus matrix — e.g. tenants only have control during an active tenancy, installers lose write access once the property leaves Setup/Commissioning, PMs cannot use smart-home control endpoints.
Storage isolationPer-property User Durable Object (one DO per property, keyed by property_id)All tenant-scoped reads/writes go through the DO's RPC surface. Cross-property reads at the API layer are gated by Permission, not by SQL.

Supplier credential boundary

Sub-processor credentials (SmartThings API key, Daikin OAuth client_id/client_secret, Yale OAuth tokens, Hive credentials) are the platform's most sensitive non-tenant secrets. They are protected by three rules:

  1. Encryption at rest. AES-256-GCM with a 12-byte random nonce per encryption. Key material lives in Cloudflare Workers Secrets (BF_ENCRYPTION_KEY). See crates/bf_resident_db/src/crypto.rs and the rotation plan in key-rotation-plan.md.

  2. Decryption never reaches the wire. The DB-layer struct bf_resident_db::ItSetup carries decrypted credentials in-process for legitimate server-side calls (e.g. JWT exchange against the SmartThings API). Every IT-setup HTTP handler instead returns crate::handlers::it_setup::types::ItSetupClient, which exposes only *_configured: bool flags. The IT-admin web app uses these flags to render presence; the plaintext value never leaves the worker.

  3. Post-IT-setup password protocol. When IT setup generates a Yale or Hive supplier account, bf_auth produces a deterministic email/password from a worker-secret seed (BF_CREDENTIAL_SEED_*, see crates/bf_auth/src/credentials.rs). The installer manually changes the supplier-side admin password to the generated value. The password itself is never persisted in bf_resident or any D1 table — it is regenerated on demand from the seed when needed at runtime. This means a database compromise does not leak supplier credentials.

Tenancy-period RPC invariant

A core data-isolation guarantee of the platform: while a property is in Tenancy status, the only authenticated role that may issue an RPC against the property's User Durable Object is the tenant. Admins and installers are denied at the permission gate; property managers don't have direct DO access at all (they go through the bf_resident HTTP API). Guests get a narrow, permission-gated subset, but every guest action is bounded by what the tenant has explicitly granted them.

The single allowed deviation is a small, audited list of service-only RPCs that bypass user auth entirely — is_service_only_request in crates/bf_user/src/permissions.rs. These are entered exclusively by trusted callers under service-to-service auth, never by a user session:

  • Device-token management (RegisterDeviceToken, UnregisterDeviceToken{,ByToken}, TouchDeviceToken) — issued by bf_resident after it has validated the user's Clerk JWT on the HTTP edge.
  • Lifecycle / sync (Seed, MoveIn, MoveOut, Backup, RestoreBackup, SyncProperty/SyncDevices/SyncTenants/SyncDocuments/SyncCredentials, RefreshMetadata) — admin pipelines.
  • Invite / guest sync (InviteGuest, GuestRevoked) — fan-out from invite acceptance in bf_resident.
  • Webhook callbacks (UpdateDeviceStateFromSmart, ActivatePinCode, ConfirmPinCodeDeletion) — SmartThings / Yale webhooks, signature-verified.
  • Cron-driven (CollectEnergyMeasurement, RecalculateEnergyAggregates).
  • Admin destructive operations (DeleteAllStorage).
  • PM read endpoints called via bf_resident (GetTenantsAndGuests, GetSecurityStatus).

The invariant is enforced by code (the per-role check_*_permission helpers reject all non-service RPCs from non-tenant roles during occupied statuses) and pinned by an exhaustive variant sweep in permissions.rs::tests::test_tenancy_*. The sweep iterates every RpcRequest discriminant via strum_macros::EnumDiscriminants (RpcRequestKind::iter()) and exercises the gate for each one, so a future change cannot quietly broaden access by adding a new variant — the compiler refuses to build until the new variant is added to the test's exhaustive match, and the runtime sweep then proves the new variant is denied for admin / installer during Tenancy.

Token storage

OAuth tokens (Yale, Daikin, etc.) are kept inside per-user Durable Objects in bf_auth and never returned to the browser. Refresh runs on the DO's Alarm timer with exponential backoff; failures are recorded to the DO-local access_logs table (90-day retention). See crates/bf_auth/src/storage.rs::log_access.

Authentication

  • No passwords. Tenants and staff sign in with passkeys (WebAuthn) or magic codes delivered to their registered email.
  • Passkey CRUD is exposed in:
    • Resident mobile app — Profile → Passkeys (uses the native Clerk SDK via Tauri).
    • Property-manager web app — Settings → Security.
  • Lost-device recovery: tenants authenticate via email magic code and revoke the lost passkey from the same screen.

Audit logging

Three log streams together answer "who accessed/changed what, when":

  1. PostHog request stream — emitted by bf_observability::ObservabilityLayer for every HTTP request. Carries user_id for every authenticated route, including tenant-facing /user/* and /mobile/*. Retention 90 days. This is the primary access log.
  2. D1 activity_log table — staff-personnel actions on property and infrastructure data. Written via crate::services::audit::log_action. Includes IT-setup creation/update/deletion (with non-reversible fingerprints for any secret-bearing field), invite CRUD, cloud-transfer completion. Surfaced in the admin UI under Activity.
  3. bf_auth access_logs table — every OAuth token fetch / refresh / failure for sub-processor accounts. DO-local, 90-day retention.
  4. D1 admin_audit_logs table — write-only, FK-free forensic sink for mutating staff (admin / PM / IT-admin / installer) actions, written best-effort via crate::services::admin_audit. A blanket middleware records one row per mutating admin request (method, path, status, and a sanitized request-body snapshot); destructive handlers add explicit enrichment rows (device / property / portfolio / credential deletes, cloud transfer, bulk SmartThings cleanup). It is an internal staff-personnel log: it may store the staff actor email and sanitized staff/admin request JSON for accountability, but it must not be used for tenant/customer traffic, and raw secrets — PINs, tokens, API keys, OAuth secrets, passwords, QR payloads, credential values — are redacted/fingerprinted before storage and never persisted.

Tenant-facing User Durable Object request handling is also captured by stream (1); the DO itself does not maintain a separate audit table because guest-list mutations are personal data of the tenant who initiated them and the per-endpoint PostHog log is sufficient evidence of who acted.

Data retention & deletion

  • D1 backups: Cloudflare-managed point-in-time recovery, 30-day window. Backups are not viewable by tenants.
  • PostHog request logs: 90 days.
  • bf_auth access_logs: 90 days, automatic cleanup.
  • Tenant data: bounded by the active tenancy. On move-out, the property reverts to a vacant state; tenant identifiers are removed from the property's invite list.
  • Sub-processor data after tenancy: SmartThings / Yale / Daikin retain a small amount of per-tenancy data for under a week post-handover, governed by their own DPAs.
  • Anonymised analytics: data exported into Supabase / PostHog product analytics goes through the User-DO export RPC command, which strips tenant identifiers and emits property-level aggregates only. Raw tenant-scoped data is never exported.

Tenant rights (GDPR Articles 15-22)

  • Identity self-service (Art. 15/16/17 against Clerk-held data): tenants manage their email, passkeys, and active sessions directly in the Clerk-backed account UI.
  • Access data on request (Art. 15 against platform-held data): currently fulfilled manually by the data team via D1 query; export tooling is planned (see gdpr_remaining_work.md § Workstream D).
  • Erasure (Art. 17): post-tenancy, identifiers are removed from invite lists and the User Durable Object's tenant role expires. Backup expiry (30 days) finalises deletion.
  • Restriction / objection (Art. 18/21): handled case-by-case via the Controller (Broadford), who may instruct us to suspend non-essential processing for a tenant.
  • Portability (Art. 20): same path as Access; manual export today.

Hosting & residency

EU / UK regions exclusively. This applies to:

  • Cloudflare Workers, D1, Durable Objects, R2, KV, Queues — restricted to EU jurisdictional cluster.
  • Clerk — EU instance.
  • PostHog — eu.i.posthog.com.
  • Supabase — EU project.
  • SmartThings / Yale / Daikin / Hive — UK / EU regions per their respective DPAs.

No personal data is intentionally transferred outside the EEA / UK. Where remote support access is technically possible (e.g. Cloudflare engineers), it is governed by the upstream provider's DPA and SCCs.

Outstanding work

DPAs, external penetration test, formal tenant data-export tool, and Airzone integration are tracked in gdpr_remaining_work.md at the repo root.