Biltevo

Security overview

Last reviewed: 3 May 2026

This page explains how Biltevo protects customer data, why each control exists, and how the controls reinforce each other. Every claim below maps to specific code, infrastructure, or runbook artefacts in the Biltevo repository — it is not aspirational.

1. Summary at a glance

LayerControlMechanism
IdentityMFA, password strength, bot defenceSupabase Auth + TOTP + hCaptcha + rate limits
DataTenant isolation, audit trail, erasurePostgres RLS + immutable audit log + GDPR sweep
ApplicationBrowser-side hardeningStrict CSP, HSTS, frame-deny, security headers
InfrastructureHosting, TLS, backupsVercel + Fly.io + Supabase Pro daily snapshots
Supply chainDependency + secret hygieneDependabot + 7-tool security CI on every commit
OperationsRunbooks for the bad dayIncident response, backup drill, retention policy

The defence is layered: any single control failing should leave the others standing. The sections below describe each control, why it exists, and where the evidence lives.

2. Architecture in one paragraph

Biltevo is a Next.js web application (hosted on Vercel) backed by a Go API (hosted on Fly.io) and a Supabase Postgres database (hosted in eu-west-1, London). Authentication is Supabase Auth. Every request from the browser carries a short-lived JWT issued by Supabase; the API verifies the JWT signature and enforces tenant isolation against the database. The database additionally enforces row-level security so a compromise at any single layer does not leak data across tenants. Customer data never leaves the EU.

3. Identity & access

3.1 Password authentication

  • Minimum length 12 characters, mixed case + digit + symbol. Enforced server-side by Supabase Auth.
  • Email verification on signup. No account can authenticate until the user clicks the verification link.
  • Secure email change. Both the old and the new email address must confirm before the change takes effect — a single compromised inbox cannot redirect the account.
  • Secure password change. A logged-in attacker with access to a hijacked session cannot change the password without re-confirmation.

3.2 Multi-factor authentication (TOTP)

  • TOTP-based MFA is available to every user. Once enrolled, the application requires aal2 (the second factor) before granting access to project data.
  • SMS is deliberately not offered. SIM-swap attacks are the most common MFA bypass in the field; we don’t trust phone numbers.

3.3 Bot defence — Cloudflare Turnstile

  • Cloudflare Turnstile (privacy-respecting, GDPR-friendly, invisible in the common case) gates every authentication endpoint: signup, sign-in, password reset, invite accept.
  • Enforcement is at the Supabase auth layer, not just the form — a request without a verified Turnstile token is rejected with HTTP 400 before any password check or account creation.
  • Turnstile renders invisibly for the vast majority of requests; a checkbox only appears when the request looks genuinely suspicious. This keeps friction low without weakening protection.
  • The site key is exposed publicly (intentional); the secret key lives only in the Supabase backend.

3.4 Rate limits

  • Sign-up and sign-in: 10 requests per 5 minutes per IP.
  • Password reset: 5 attempts per hour per email address.
  • Token verification: 10 requests per 5 minutes per IP.
  • Token refresh: 150 / 5 min (legitimate apps refresh frequently).

Combined with Turnstile on account-creation surfaces, an attacker is bottlenecked to a rate at which they cannot meaningfully credential-stuff or harvest accounts.

3.5 Idle session sign-out

After a period of inactivity the browser evicts the session and the user must re-authenticate. Reason and the redirect path are tagged (?reason=idle) so the user understands why.

4. Data protection

4.1 Multi-tenant isolation

  • Every domain table has a company_id column.
  • Postgres row-level security (RLS) policies restrict reads and writes so that a query running as a normal authenticated user can only see rows belonging to that user’s company.
  • The Go API additionally enforces the same predicate — defence in depth: a bug in either layer alone cannot leak cross-tenant data.
  • An integration test seeds two tenants and asserts no cross-tenant leakage across every list endpoint; it guards against regressions.

4.2 Audit trail (Tier 1 / Tier 2)

The programme_audit_log table records who changed what, when, and why for every programme-significant action. Three properties matter:

  • Append-only — a database trigger rejects UPDATE and DELETE on this table. Even with database-admin access an attacker cannot silently rewrite history.
  • Reason-attributed — every entry captures responsible_party and reason_category so audit reviewers know not just what changed but why. The party vocabulary defaults to construction roles (subcontractor / main contractor / shared) on construction projects and is configurable per workspace for other industries.
  • 7-year retentionretention_until is set automatically by an immutable trigger. This aligns with the UK Limitation Act 1980’s six-year window plus a one-year safety margin: claims arising from contract are time-limited at six years, so seven gives the evidence chain a full lifecycle.

4.3 GDPR Article 17 — right to erasure

When a user requests deletion:

  1. The application records deletion_requested_at = now() on the user’s row but does not immediately destroy data — this gives a 30-day grace window during which the user can withdraw the request and recover the account.
  2. After 30 days a scheduled Postgres function (run_user_deletions) anonymises the record: email becomes deleted-{uuid}@biltevo.invalid, name becomes “Deleted user”, auth_id is nulled, and deleted_at is timestamped.
  3. An audit-log row is written documenting the anonymisation, with responsible_party = system, so the deletion itself is auditable.
  4. The function runs daily at 03:00 UTC via pg_cron. Each user is anonymised inside its own exception block so a single bad row cannot abort the sweep.

The function lives in the database; it cannot fail to run because the API is down. It has been verified end-to-end on production.

4.4 Soft-delete + last-admin guard

  • Users are soft-deleted by default. Hard deletion is reserved for the GDPR sweep above.
  • A database trigger refuses to delete or demote the last admin in a company so a misclick cannot lock the customer out of their own data.

4.5 Save-state visibility & silent-failure prevention

  • Every mutation (every edit you make) is wrapped by a global error handler that reports to Sentry and surfaces a visible error toast on failure. A save cannot fail silently and leave the UI showing a stale value as if it succeeded.
  • A persistent “saving / saved / save failed” indicator in the topbar confirms the system has acknowledged your edits.
  • All optimistic UI updates roll back their changes on server failure — the displayed state cannot diverge from server truth without you being told.

4.6 Encryption

  • At rest: Supabase Postgres uses AES-256 disk encryption; backups inherit the same encryption.
  • In transit: TLS 1.2+ enforced everywhere. HSTS (see §5.2) forbids downgrade to HTTP.

5. Application security

5.1 Content Security Policy

Every response from the web application carries a strict CSP:

  • script-src ‘self’ plus a small allowlist (hCaptcha, Sentry, Supabase). No third-party analytics.
  • frame-ancestors ‘none’ — Biltevo cannot be iframed by anyone, defeating clickjacking.
  • frame-src allowlist limited to hCaptcha (the only legitimate third-party iframe).
  • form-action ‘self’ — credentials cannot be POSTed off-domain even if an attacker injects markup.
  • upgrade-insecure-requests — any accidental http:// reference is upgraded to https:// by the browser before the request leaves.

5.2 HTTP security headers

  • Strict-Transport-Security: max-age=63072000; includeSubDomains; preload (2-year HSTS, included in browsers’ preload list)
  • X-Content-Type-Options: nosniff
  • X-Frame-Options: DENY
  • Referrer-Policy: strict-origin-when-cross-origin
  • Permissions-Policy locks down camera, mic, geolocation, payment.

5.3 Input handling

  • All API entry points use typed Go structs with explicit field binding — no untyped JSON traverses business logic.
  • The shared TypeScript type definitions in types/biltevo.ts are the single source of truth for entity shapes; no component or handler can invent its own.

6. Infrastructure & hosting

ComponentProviderRegionNotes
Web (Next.js)VercelAuto-multi-region edgeTLS managed by Vercel
API (Go)Fly.ioLondonHardened Alpine container
DatabaseSupabase Proeu-west-1 (London)Daily backups, 7-day retention
ErrorsSentryEUPII scrubbing on by default
AuthSupabase AuthEUSame project as DB
  • Customer data never leaves the EU. UK GDPR / EU GDPR cross-border transfer concerns do not arise.
  • The API container is built from alpine:3.20 with only ca-certificates added — minimal attack surface.
  • Backups: daily Supabase snapshots, 7-day retention. Recovery point objective is 24 hours today; the runbook describes how this is tested and the upgrade path (point-in-time recovery add-on) when customer scale demands tighter RPO.

7. Supply chain & dependency hygiene

The CI pipeline (.github/workflows/security.yml) runs on every push and weekly:

ToolWhat it scansAction on finding
DependabotAll package manifestsAuto-PR; safe minors auto-merged
npm auditJS dependenciesFail on CRITICAL
ESLint security pluginJS/TS sourceFail on any security/* rule
govulncheckGo stdlib + depsReport-only (stdlib CVEs await Go release)
gosecGo sourceFail on HIGH severity
cargo auditRust depsFail on advisories
TrivyAPI container imageFail on HIGH+CRITICAL with available fix
gitleaksFull git historyFail on detected secret

All seven gates are enforced on the main branch. A vulnerability in a transitive dependency cannot reach production without either being acknowledged by a human or being below the severity threshold.

8. Operational security

Three runbooks live in our security folder:

  • Incident response — what to do when something has gone wrong: triage, containment, customer notification (with the UK GDPR Article 33 72-hour ICO clock noted), post-mortem.
  • Backup-restore drill — quarterly drill that proves the backups actually work. A backup never restored is not a backup.
  • Cyber Essentials prep — mapping of Biltevo controls to the five Cyber Essentials domains, ready for IASME self-assessment.

Admin consoles (GitHub, Vercel, Supabase, Fly.io, Sentry) are MFA-gated using TOTP or hardware keys. SMS is not used. The list of consoles and their MFA status is reviewed every quarter.

9. Compliance hooks

Regulation / standardHow Biltevo aligns
UK GDPR Article 17 (right to erasure)30-day grace + automated anonymisation sweep; see §4.3
UK GDPR Article 33 (breach notification)72-hour clock referenced in incident runbook
UK GDPR Article 32 (security of processing)Encryption at rest + in transit, RLS, audit log
Limitation Act 19807-year audit-log retention covers the 6-year claim window
Cyber Essentials (preparing)Five-domain mapping documented internally
ISO 27001 (alignment, not certified)Layered controls + runbooks + continuous monitoring map naturally

We do not currently claim ISO 27001 certification or SOC 2; we will pursue these when customer scale justifies the audit cost. The underlying controls are in place.

10. What’s explicitly out of scope today

Honesty matters more than checkboxes. Items that are not yet in place — and the trigger for adding them:

ItemTrigger
Point-in-time recovery (sub-hour RPO)First customer where 24h data loss is material
Formal DPA templatePre-signed before first customer onboarding
Cyber liability insurancePre-signed before first customer onboarding
ISO 27001 auditCustomer demand or regulatory need
SOC 2 Type IIUS enterprise customer demand
Account-level lockout (custom auth hook)After 10+ customers; today’s rate-limits + MFA suffice

Each of these is tracked. None of them is a blocker for the controls above being effective at current scale.

11. Contact

Security concerns, vulnerability disclosures, or questions from a security reviewer should go to security@biltevo.com. We aim to acknowledge within one business day.

This page is reviewed at minimum every quarter, and after any material security change. The latest version is the one in themain branch of the Biltevo repository.