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
| Layer | Control | Mechanism |
|---|---|---|
| Identity | MFA, password strength, bot defence | Supabase Auth + TOTP + hCaptcha + rate limits |
| Data | Tenant isolation, audit trail, erasure | Postgres RLS + immutable audit log + GDPR sweep |
| Application | Browser-side hardening | Strict CSP, HSTS, frame-deny, security headers |
| Infrastructure | Hosting, TLS, backups | Vercel + Fly.io + Supabase Pro daily snapshots |
| Supply chain | Dependency + secret hygiene | Dependabot + 7-tool security CI on every commit |
| Operations | Runbooks for the bad day | Incident 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_idcolumn. - 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
UPDATEandDELETEon this table. Even with database-admin access an attacker cannot silently rewrite history. - Reason-attributed — every entry captures
responsible_partyandreason_categoryso 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 retention —
retention_untilis 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:
- 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. - After 30 days a scheduled Postgres function (
run_user_deletions) anonymises the record: email becomesdeleted-{uuid}@biltevo.invalid, name becomes “Deleted user”,auth_idis nulled, anddeleted_atis timestamped. - An audit-log row is written documenting the anonymisation, with
responsible_party = system, so the deletion itself is auditable. - 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-srcallowlist 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 accidentalhttp://reference is upgraded tohttps://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: nosniffX-Frame-Options: DENYReferrer-Policy: strict-origin-when-cross-originPermissions-Policylocks 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.tsare the single source of truth for entity shapes; no component or handler can invent its own.
6. Infrastructure & hosting
| Component | Provider | Region | Notes |
|---|---|---|---|
| Web (Next.js) | Vercel | Auto-multi-region edge | TLS managed by Vercel |
| API (Go) | Fly.io | London | Hardened Alpine container |
| Database | Supabase Pro | eu-west-1 (London) | Daily backups, 7-day retention |
| Errors | Sentry | EU | PII scrubbing on by default |
| Auth | Supabase Auth | EU | Same 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.20with onlyca-certificatesadded — 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:
| Tool | What it scans | Action on finding |
|---|---|---|
| Dependabot | All package manifests | Auto-PR; safe minors auto-merged |
| npm audit | JS dependencies | Fail on CRITICAL |
| ESLint security plugin | JS/TS source | Fail on any security/* rule |
| govulncheck | Go stdlib + deps | Report-only (stdlib CVEs await Go release) |
| gosec | Go source | Fail on HIGH severity |
| cargo audit | Rust deps | Fail on advisories |
| Trivy | API container image | Fail on HIGH+CRITICAL with available fix |
| gitleaks | Full git history | Fail 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 / standard | How 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 1980 | 7-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:
| Item | Trigger |
|---|---|
| Point-in-time recovery (sub-hour RPO) | First customer where 24h data loss is material |
| Formal DPA template | Pre-signed before first customer onboarding |
| Cyber liability insurance | Pre-signed before first customer onboarding |
| ISO 27001 audit | Customer demand or regulatory need |
| SOC 2 Type II | US 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.