The parts that touch personal data were built to be audited.
Veil processes the most sensitive material an organisation holds, on behalf of a request it is legally obliged to answer. This page is what a security review usually has to extract over three calls.
Everything stays in the European Union.
There is no region setting to get wrong, because there is only one region. Each component below is pinned, not merely defaulted.
| Component | Region | Note |
|---|---|---|
| Database and authentication | Frankfurt | Row-level isolation per organisation |
| Object storage | Frankfurt | Private buckets, no public objects |
| Processing workers | Frankfurt and Amsterdam | No public ingress beyond a health check |
| Web functions | Frankfurt | Pinned at the deployment level |
| Model inference | European provider | Identifiers stripped before the request leaves |
A purge destroys the key, not just the rows.
The densest personal data in the system is encrypted with a per-case key, and that key is itself wrapped by a key belonging to your organisation.
Envelope encryption
Entity surface text and the normalised document model are encrypted with AES-256-GCM inside the worker. The plaintext never reaches the browser and never reaches a log.
Crypto-shredding
When a case purges, the case key is destroyed. Any copy that survives anywhere, including in a backup, is unreadable from that moment.
Encryption in transit
TLS on every hop, including the internal call from the web tier to the processing worker, which is additionally gated by a shared secret.
Cases expire on a clock you set.
Retention is a plan-capped setting, not a support ticket. A released case purges on schedule and leaves an accountability record with no personal data in it.
- Default retention after release
- 30 days
- Maximum retention
- Up to 90 days by plan
- Unreleased cases
- Purged at 180 days, with warnings first
- What the purge removes
- Objects, extractions, entity mentions, the case key
- What the purge leaves
- A tombstone: reference, dates, counts, quality summary
Nothing is listable, and nothing is long-lived.
Signed URLs only
Storage is never browsable. Each object is reached through a single-object signed URL valid for five minutes to upload and two minutes to download, issued by a server route that re-checks membership and role at the moment of issue.
Row-level isolation
Every tenant table carries the organisation id and a row-level security policy. Cross-organisation reads are refused by the database, not by application code.
Roles and multi-factor
Owner, admin, member and viewer, with multi-factor authentication available on every plan and SAML single sign-on on the enterprise plan.
Dual control
On the enterprise plan, releasing a bundle and marking an entity as the data subject each require a second approver, enforced inside the database transition function rather than in the interface.
Logs carry counts. Never content.
No personal data in logs
Loggers accept ids, counts, enumerations and durations through an allowlist serializer. A continuous integration rule blocks raw string interpolation into a logger call, so the property is enforced rather than remembered.
Model calls logged as arithmetic
An inference call is recorded as token counts and chunk identifiers. The prompt and the completion are not written anywhere.
Append-only audit trail
State transitions, entity decisions, bundle downloads, unredacted-view toggles and settings changes are appended through a single writer and cannot be edited or deleted afterwards.
The verifier is credible because it shares no code.
A banned dependency edge
The verification package is forbidden by a lint rule from importing anything in the detection package. If the two shared code, a bug in the redactor could pass its own check.
Hostile input parsing
Archive, email and office document parsers are treated as an attack surface: zip-slip and zip-bomb guards, a recursion depth cap, per-job memory and time budgets, and fuzzing in continuous integration.
Egress allowlist
The processing worker can only reach the destinations it needs. There is no general outbound network access from the component that handles document content.
Quality gated in CI
A labelled corpus of synthetic cases runs on every change to a detector, a prompt or a model version. A change that finds fewer of the planted third parties, or that starts removing the data subject, does not merge.
Who else touches the data.
The complete list. A data processing agreement and the current version of this list are available on request.
Supabase
Database, authentication and object storage
Frankfurt, EU
Fly.io
Document processing workers
Frankfurt and Amsterdam, EU
Vercel
Web application hosting
Frankfurt, EU
Mistral AI
Entity detection and optical character recognition
EU
Stripe
Billing. Never receives document content
EU
Your next access request does not have to eat a week.
Open a case, declare the subject, drop the documents in. Veil does the rest and shows you its working.