# Sercxi — Security Posture

**Owner:** Sercxi B.V.
**Contact:** hello@sercxi.io (subject line "Security")
**Last reviewed:** 2026-07-07
**Scope:** sercxi.io, www.sercxi.io, the sercxi.lovable.app preview, and the backend edge functions and database that support them.

This document is the source-of-truth for enterprise procurement questions about the sercxi.io website. It describes controls that are actually enabled today. Where a control is not implemented, it is called out explicitly in §11 ("What we do not claim").

---

## 1. Architecture at a glance

- **Frontend** — React 18 + Vite 5 SPA, statically compiled and served over HTTPS. No server-side rendering.
- **Backend** — Managed Postgres (Supabase / Lovable Cloud) with Row-Level Security enforced on every user-facing table. All writes from the browser route through audited Deno edge functions; the browser never holds a service-role key.
- **Auth surface** — There is no public sign-up. `disable_signup = true`. The only accounts that exist are operator accounts provisioned server-side and gated by the `user_roles` table + `has_role()` SECURITY DEFINER function.
- **Storage** — One private bucket (`frameworks`) for gated PDFs; signed URLs only, no public listing.
- **Analytics** — Umami (cookieless, self-hosted at cloud.umami.is). No third-party ad or tracking pixels.
- **Custom domain** — sercxi.io / www.sercxi.io behind Cloudflare-managed TLS. HSTS preload eligible.

## 2. Data classes and where they live

| Class | Examples | Storage | Retention |
|---|---|---|---|
| Prospect leads | name, corporate email, company, sector, message | `public.leads` | Purpose-bound; deletion on request within statutory windows |
| Assessment submissions | APEX Signal answers, score, verdict | `public.assessment_submissions` | Purpose-bound; deletion on request |
| Operator roles | user_id → role | `public.user_roles` | Lifetime of employment |
| Rate-limit counters | scope+identifier, count, window | `public.security_rate_limits` | Auto-pruned after 24h |
| Gated PDFs | Corridor Books, IMPACT one-pagers | Private storage bucket | Regenerated on demand |

No candidate CVs, contracts, or executed mandate data are stored on the website backend. Those live in a separate, contractually governed system (see §7 Subprocessors).

## 3. Authentication & authorization

- **Zero public sign-up.** Supabase auth is configured with `disable_signup = true`, `external_anonymous_users_enabled = false`, `auto_confirm_email = false`, and `password_hibp_enabled = true` (Have I Been Pwned check on every password event).
- **Roles are in a separate table**, never on a profile row. Every privileged check flows through `public.has_role(uid, role)`, a `SECURITY DEFINER STABLE` function with `search_path = public`, revoked from `PUBLIC` and executable only via authenticated + service_role.
- **RLS is enabled** on `leads`, `assessment_submissions`, `user_roles`, and `security_rate_limits`. All four have restrictive deny-all policies for anon and authenticated; writes only succeed through edge functions using the service role.
- **JWT verification** is on by default for all Lovable-managed edge functions except the four intentionally public ones (`submit-lead`, `download-framework`, `infer-disc-profile`, `send-assessment-email`, `generate-corridor-book`), which perform their own input validation, honeypot checks, and rate-limiting.

## 4. Perimeter — HTTP response headers

Applied to every HTML response via `public/_headers`:

| Header | Value |
|---|---|
| Strict-Transport-Security | `max-age=63072000; includeSubDomains; preload` (2 years) |
| Content-Security-Policy | `default-src 'self'`; `script-src 'self' https://cloud.umami.is` (no `'unsafe-inline'` — inline scripts extracted); connect restricted to self + Umami + Supabase project; **object-src 'none'**, **base-uri 'self'**, **form-action 'self'**, **frame-ancestors 'none'**, `upgrade-insecure-requests`, `block-all-mixed-content` |
| X-Frame-Options | `DENY` |
| X-Content-Type-Options | `nosniff` |
| Referrer-Policy | `strict-origin-when-cross-origin` |
| Permissions-Policy | camera, microphone, geolocation, payment, USB, magnetometer, gyroscope, accelerometer, interest-cohort all disabled |
| Cross-Origin-Opener-Policy | `same-origin` |
| Cross-Origin-Resource-Policy | `same-origin` |
| X-Permitted-Cross-Domain-Policies | `none` |

Known trade-off: `style-src` still allows `'unsafe-inline'` because Tailwind's inline critical CSS and React's `style={...}` attributes (~850 usages across the app) rely on inline styles. Removing `'unsafe-inline'` from `style-src` would require refactoring every inline style to a class-based rule; the surface for style-based exfiltration is narrow and mitigated by strict `default-src 'self'`, `object-src 'none'`, `connect-src` allowlist, and `frame-ancestors 'none'`. `script-src` no longer allows `'unsafe-inline'` — all inline scripts were extracted to external files (`/theme-init.js`, `/speculation-rules.js`, `/critical-shell.js`, `/umami-loader.js`) served same-origin, so XSS via inline script injection is blocked outright.

## 5. Input validation

- Every public edge function validates every field: type, non-empty, length caps, regex where applicable (email, UUID, UID). See `supabase/functions/submit-lead/index.ts` for the canonical pattern.
- Zod schemas mirror the same rules on the client (e.g. `MandateInitiationProtocol`, `CorridorGate`).
- Corporate-email gate (`src/lib/corporateEmail.ts`) rejects free-mail domains on the corridor-book download.
- Honeypot fields are present on every lead form; server treats a filled honeypot as silent success (returns a fake lead ID).
- HTML escaping helper (`escapeHtml`) applied to any user string echoed into email templates.
- `dangerouslySetInnerHTML` audit: zero occurrences on user-controlled input.

## 6. Rate limiting

Persistent, cross-instance rate limits are backed by the `public.security_rate_limits` table and the `public.check_rate_limit(bucket, max, window_seconds)` `SECURITY DEFINER` function (execute revoked from anon/authenticated; only service_role can call it).

| Endpoint | Bucket | Limit |
|---|---|---|
| `submit-lead` | per-IP | 8 / hour |
| `submit-lead` | per-email | 3 / day |
| `generate-corridor-book` | per-IP | 6 / hour |
| `infer-disc-profile` | per-IP | 5 / hour |
| `send-assessment-email` | per-IP | 5 / hour |
| `corporate-signal` | per-IP | 30 / hour |

Over-limit responses return `429 Too Many Requests` with `Retry-After`. The helper fails **open** on transient DB errors (logged) so the site stays available; abuse spikes are then absorbed by upstream Cloudflare rate-limiting rules that the customer can configure at the DNS layer.

## 7. Subprocessors

Published at `/legal/subprocessors`. Current list:

| Subprocessor | Purpose | Region |
|---|---|---|
| Supabase (Lovable Cloud) | Managed Postgres, Auth, Storage, Edge Functions | EU (Frankfurt) |
| Resend | Transactional email (briefing acknowledgements, internal notifications) | EU |
| OnePageCRM | CRM sync for accepted leads | EU |
| Cloudflare | DNS, edge TLS termination for sercxi.io | Global |
| Umami | Cookieless product analytics | EU |
| IPInfo | IP → corporate-signal enrichment (edge function only, no persistence) | US |
| Firecrawl | On-demand public-web scraping for APEX Signal assessments | US |
| Bing Webmaster / Google Search Console | SEO indexing | US |

DPA available on request under NDA.

## 8. Secrets management

All third-party credentials (`RESEND_API_KEY`, `ONEPAGECRM_API_KEY`, `IPINFO_TOKEN`, `BING_WEBMASTER_API_KEY`, `SUPABASE_SERVICE_ROLE_KEY`) live in Supabase Vault and are exposed only to edge functions via `Deno.env.get()`. No secret is written into the frontend bundle. The only keys that ship to the browser are the Supabase URL and the publishable (anon) key, both of which are safe by design because RLS enforces access.

## 9. Data-subject rights

GDPR / UK-GDPR / PDPA-SG / PDPL / APPI / PIPL requests are actioned via `hello@sercxi.io` with subject "Privacy Request". A partner reviews every request personally. Access, correction, deletion, restriction, portability and objection are all supported. Statutory response windows are honoured (30 days GDPR, shorter where local law requires).

## 10. Incident response

- **Contact** — `hello@sercxi.io` (subject "Security"), see `/.well-known/security.txt`.
- **Ack SLA** — good-faith reports acknowledged within 5 working days.
- **Triage** — reproduce, classify severity (CVSS-lite), rotate any exposed credential immediately, deploy a hotfix, communicate to affected data subjects if a personal-data breach is confirmed (GDPR 72-hour notification honoured).
- **Post-mortem** — written internally; a redacted summary shared on request with materially-affected clients.

## 11. What we do not claim

- We do **not** hold SOC 2 Type II, ISO 27001, PCI-DSS, or HIPAA certifications.
- We do **not** run continuous third-party penetration testing on the website (the underlying Supabase/Cloudflare platforms are independently certified).
- We do **not** operate a bug-bounty program with monetary rewards.
- We do **not** guarantee 100% uptime; the website inherits the SLA of the underlying managed platform.

Where any of the above is a hard requirement for a procurement process, we will (a) confirm what the underlying platform is certified for and share their attestation letters under NDA, and (b) discuss a bespoke assurance addendum.

## 12. Change log

| Date | Change |
|---|---|
| 2026-07-08 | Removed `'unsafe-inline'` from `script-src`. All inline scripts extracted to same-origin external files (`theme-init.js`, `speculation-rules.js`, `critical-shell.js`, `umami-loader.js`). `style-src` retains `'unsafe-inline'` as an accepted trade-off (React inline styles). |
| 2026-07-07 | Persistent DB-backed rate limiting; hardened CSP (object-src, base-uri, form-action, upgrade-insecure-requests); HSTS extended to 2 years; COOP/CORP added; HIBP password check enabled; `/trust` page updated. |
| 2026-06-28 | Initial `/trust` hub published, subprocessor list, security.txt. |

---

_This document is maintained by Sercxi B.V. as app-owner editable content. It is a factual description of controls actually enabled, not an independent certification or audit report._
