Trust & Compliance
Last updated: 2026-05-28 · Questions: security@vrsjo.com
Security architecture
VRS One is a single Cloudflare Workers application deployed at app.vrsjo.com. Every request is served over HTTPS; HTTP-only traffic is rejected at the edge. The application stack:
- Next.js 16 (App Router) on Cloudflare Workers (via OpenNext).
- Postgres (Neon, EU/US regions) — encrypted at rest, TLS in transit, point-in-time recovery enabled.
- Cloudflare R2 — encrypted CV/document storage with short-lived signed URLs.
- Auth0 — OIDC authentication with JWT signature verification against the published JWKS.
- Sentry — error tracking with PII scrubbing rules.
- DeepSeek — LLM provider; only candidate / engagement text needed for the call is sent; outputs are validated and grounded against source.
Identity & access
- Sign-in is via Kinde's hosted login (password, passkey, social login, MFA — never touches VRS infrastructure). New tenants in 2026 H1 launch on Kinde; the WorkOS AuthKit pilot from 2026-05-28 was retired the next day in favour of Kinde's free B2B tier.
- New Kinde identities cannot self-provision a VRS account: unknown identities are rejected by default, and email-based linking requires the IdP-verified email-verified flag. Operators are seeded; client admins are created only when a sign-up request is approved by a VRS super_admin.
- Four roles: super_admin (cross-tenant operator), super_admin_limited (operator tenant only), recruiter (own engagements only — no placement/invoice approval), client_admin (portal-side). Recruiter-level operators cannot mark invoices paid, approve placements, complete engagements, invite users, create tenants, mint API tokens, create outbound webhooks, or export DSAR bundles.
- The session cookie is issued, encrypted, and signed by Kinde and never carries a raw VRS user id — every request is re-validated server-side against the Kinde session before it touches the database. Logout terminates the session at Kinde so a stolen cookie cannot be reused.
Multi-tenant isolation
- Every tenant-owned domain entity (engagements, contracts, invoices, placements) is tagged with a tenant id; every repository query is wrapped in scoped(ctx, …) so a tenant cannot read another tenant's rows even if it knows their UUIDs. Candidates live in a global registry by design (a candidate may be placed by VRS across multiple clients) and are authorised at the service layer, with portal-side identity masking applied to every list and detail view.
- Candidates pre-hire appear to the client portal as 'Candidate A / B / …' with email/phone/employer/location all redacted. Full identity is released only after the client accepts a candidate. The mask applies to detail pages, actionable queues, engagement candidate lists, and the portal dashboard.
- The Kinde sign-in path enforces email-based linking only (no on-the-fly user provisioning) and additionally requires the IdP-verified email-verified flag plus a matching Kinde Organization code, so a verified email under the wrong organization cannot bind into another tenant.
AI safety & compliance
- Multiple LLM providers behind a unified interface — Anthropic Claude (US/EU-hosted, SOC-2'd) is the default for enterprise workloads; DeepSeek is available as a cost-optimised fallback. AI_PROVIDER env override + per-tenant choice on tenants.settings.aiProvider.
- Major LLM calls (candidate summary, engagement brief, CV scoring) are logged to ai_usage: feature, model, tokens, USD cost, latency, status. The aggregate report at /api/operator/ai-usage/report supports per-tenant cost allocation. Comprehensive bias-audit instrumentation across every LLM sink (incl. CV parsing) is being rolled out and will be required before formal NYC Local Law 144 and EU AI Act readiness statements.
- AI outputs are advisory, not gating. Rule-based scoring runs deterministically alongside; AI scoring augments but never replaces it. A super_admin reviews every visibility-tier promotion.
- Contact details extracted by the LLM are grounded against the source CV — any email or phone the model emitted that does not appear in the source is dropped automatically (no hallucinated contacts).
- Bias-relevant signals (name, age, gender markers) are never sent to the model as scoring inputs — only role + skills + experience.
Network — for customer IT teams
To allowlist VRS One in an egress firewall: our customer-facing domain is app.vrsjo.com. Production traffic is served via Cloudflare Workers — the official Cloudflare IP ranges (cloudflare.com/ips) cover every region. Outbound webhooks from VRS One (planned) will originate from the same Cloudflare ranges. Please reach out via security@vrsjo.com if you need a tenant-scoped IP allowlist on inbound requests.
Data privacy & residency
- GDPR Article 20 (right to portability): client_admin downloads a full JSON export of their tenant via /api/portal/data-export. Includes engagements, candidates (PII redacted unless hired), contracts, interviews, invoices, placements and audit log.
- Data residency: Neon Postgres branch can be pinned to EU (Frankfurt) or US (us-east) per customer; R2 is region-replicated.
- Encryption at rest: Neon uses AES-256; R2 uses server-side encryption with provider-managed keys.
- Audit trail: every state change is recorded with actor user, tenant, before/after state, correlation id. Append-only — rows are never updated or deleted.
Operational controls
- Liveness probe at /api/health (DB only); deep probe at /api/health?deep=1 covers DB, KV, R2, the LLM provider, and production config drift with per-dependency status and latency. Deep probe is internal-only in production (CRON_TOKEN required).
- Every API response carries an x-correlation-id header; the same id is in our Workers Logs and Sentry so any user-reported incident can be traced end-to-end.
- Rate limits: per-user limits on every paid-token call (CV parsing, AI summary, AI brief, bulk upload). Per-tenant aggregate limits are configured for the highest-cost paths.
- Inbound webhook handlers (Zoho Sign, Cal.com, Daily) verify HMAC signatures and check timestamp freshness against a 5-minute window. In production, missing webhook signing secrets cause the receiver to refuse requests — there is no fail-open path. Webhook events are recorded in a DB-backed idempotency table to prevent replay.
- Outbound webhook signing secrets are encrypted at rest with AES-256-GCM (separate key, never database-co-located, rotation-aware). Outbound webhook URLs are validated against a private/loopback/link-local/metadata SSRF blocklist on create and on dispatch.
- Background jobs run on Cloudflare Cron Triggers: SLA evaluation every 15 minutes; overdue invoices, guarantee tracking, drive-trash purge daily; stale multipart upload cleanup, per-tenant meeting retention enforcement, and off-Neon backup daily.
- Disaster recovery: Neon point-in-time recovery (5-minute RPO) + nightly gzip'd JSON snapshot of domain tables written to a separate R2 prefix.
Compliance frameworks
- GDPR — controller + processor obligations addressed for EU client tenants; data portability (Article 20) export and erasure (Article 17) workflow available. DSAR exports of candidate data require a super_admin role (audited).
- NYC Local Law 144 — preparation in progress. Bias-audit instrumentation is being extended to cover every LLM sink; a formal LL144 audit report will be published once instrumentation is complete.
- EU AI Act — high-risk classification acknowledged; human-in-the-loop is enforced (every visibility-tier promotion is a manual VRS Super-Admin action).
- SOC 2 — controls in progress; formal Type II audit roadmap: planned 2026 H2.
Incident & breach response
- Sentry alerting on every 5xx; Cloudflare Workers Logs retained 7 days.
- Breach notification: affected tenants notified within 72 hours of confirmed incident per GDPR Article 33.
- Status: public uptime + health at /status.
Reporting a vulnerability
We welcome responsible disclosures. Email security@vrsjo.com; we acknowledge within 2 business days. Please don't run automated scanners against production — use a sandbox engagement instead, we can provision one on request.