The BAA is the US HIPAA Business Associate Agreement and is separate from our GDPR / nFADP DPA. See the current vendor list at /legal/subprocessors.
1. Security at a glance
- A pure-compute mapping engine inside a stateful product. The mapping cascade holds no state between requests. The product around it stores accounts, uploads for their retention window, what you choose to save, the audit log and billing records — in Cloudflare KV and a Supabase Postgres database in Zurich, as described in the Privacy Policy and DPA.
- HMAC-signed API keys. Keys are self-contained, scoped credentials signed with
MAPR_KEY_SECRET. Verification needs no lookup; a revocation check against the database runs on every request and fails closed. - Schema-only by default. Only column headers and up to three sample row values per column (truncated to 80 characters) leave the customer. Full-data mode needs the full-data entitlement; PHI routing needs the BAA accepted in the dashboard.
- Per-workspace upload TTL. Parsed uploads expire automatically on the retention window your workspace sets — 24 hours by default, or 0 (destroyed on commit), 168 or 720 hours; each upload reports its own
expires_at. See §8.5 for what the database mirror keeps. - Complete erasure. Deleting a workspace erases it in one database transaction, audit log included, and leaves one anonymous record that it happened (§8.6).
2. Encryption
- In transit: TLS at the Cloudflare edge for every public hostname; HSTS with
includeSubDomainsandpreload. - At rest: Cloudflare KV and Supabase Postgres encrypt stored data (AES-256) by default.
- Secrets we hold for you: connector credentials and webhook signing secrets are sealed with AES-256-GCM under a key-encryption key held as a Worker secret, with key ids so the key can be rotated.
- Platform secrets:
MAPR_KEY_SECRET,MAPR_LLM_API_KEYand related secrets are Cloudflare Workers encrypted secrets; they are never committed to source control and never logged.
3. Access control
- Workspace roles. Owner, admin and member, assigned per workspace.
- Scoped API keys. Each key carries the workspace id, the subject, an optional expiry, its capability scopes, and whether the workspace holds the full-data entitlement.
- Sign-in. Dashboard access is Supabase Auth — Google sign-in or a one-time email link or code. Sessions are refreshed at the edge and end on sign-out or when the refresh token expires; automatic idle logoff is not currently enforced.
- Audit logging. Key creation and revocation, settings and consent changes, mapping commits and confirmations, approvals, billing events, exports and erasure intents are written to a hash-chained, append-only audit log per workspace (§8.4).
- Operator access. The prompt-injection signal log and other service-role tables are readable only by the Worker's service role and AdaptivMapr operators on an admin allowlist.
4. Network security
- Edge. Cloudflare DNS and proxy in front of every public hostname, with Cloudflare's DDoS protection.
- Application. Cloudflare Workers (via OpenNext); static assets from the Cloudflare CDN.
- Security headers. A nonce-based Content Security Policy (
default-src 'self',frame-ancestors 'none'), HSTS with preload, X-Frame-Options, X-Content-Type-Optionsnosniffand a strict Referrer-Policy. The application cannot be framed by another site. - CSRF. Every state-changing dashboard request must carry a CSRF token; without one it is refused.
5. Application security
- Input validation. API routes validate input before any downstream call; field-level validators (regex, iban, gtin, loinc, icd10, npi and others) check mapped outputs.
- Schema-only enforcement. Sample rows are clamped at the HTTP edge by
clampForSchemaOnly()(≤3 rows × ≤80 chars/cell) and the same clamp is re-applied at the model call boundary. Persisted upload records storeparsed_sampleonly and keepparsed_rowsempty for schema-only uploads. - Prompt trust boundary. Customer content in every model prompt is fenced as untrusted data. A watch-only detector records suspected prompt-injection attempts (§8.13); it never blocks or changes a run.
- Rate limiting. A shared counter, enforced before billable work begins, per workspace, key or (for keyless endpoints) client address — stored as a truncated keyed hash, never the address, and deleted by a daily cleanup once its window is more than a day old.
- Dependencies. GitHub Dependabot security alerts and weekly version updates.
- Continuous integration. Every pull request runs type-checking, lint, the unit test suite (including the log-scrub, RLS-snapshot, erasure-completeness and prompt-trust-boundary rails named below) and browser end-to-end tests.
6. Infrastructure
- Compute & edge. Cloudflare Workers, deployed globally. DNS and DDoS protection are also Cloudflare.
- Storage. Cloudflare KV for uploads in flight (kept for the workspace's retention setting), webhook secrets and delivery records, and operational markers. No Cloudflare R2 object storage is used.
- Database. Supabase Postgres in Zurich, Switzerland (AWS
eu-central-2), with Supabase's daily backups kept for 7 days. Row-level security on every tenant table. - Models. Our own mapping ranker on our hardware in Switzerland; everything else through phi-cloud. See /legal/subprocessors.
- Billing. Stripe.
- Email. Resend, with an SMTP relay as fallback.
7. Compliance
- SOC 2 Type II — in progress. No attestation report exists yet.
- GDPR-aligned. Standard Contractual Clauses incorporated by reference in our DPA; subprocessor list maintained publicly.
- nFADP-aligned. The Swiss Federal Act on Data Protection 2020 is treated as equivalent to GDPR for the purposes of customer obligations.
- HIPAA-eligible with PHI routing. Customers requiring HIPAA coverage accept the BAA and use PHI routing, which sends model calls through phi-cloud to PHI-eligible providers only. AdaptivMapr offers its own BAA covering its role as a business associate; phi-cloud holds BAAs with the providers it uses for PHI-routed calls.
- CCPA / CPRA. We do not sell or share personal data within the meaning of California law.
8. PHI / HIPAA handling
AdaptivMapr is offered as a HIPAA-eligible Business Associate; an authorised person accepts the BAA for a workspace in Settings → Security & Data, and a counter-signed copy is available from compliance@adaptivmapr.com. We do not claim SOC 2 Type II, HITRUST, ISO 27001, or any “HIPAA certified” status — no such certification exists in U.S. law. The controls below describe how Protected Health Information (PHI) is handled in the cascade.
8.1 Schema-only mode minimises what leaves
In schema-only mode (the default), only column headers and up to three sample row values per column, each truncated to 80 characters, leave the customer's tenant. The clamp is enforced at the HTTP edge by clampForSchemaOnly() in lib/parser.ts and re-applied defensively at the LLM call boundary. Beyond the clamped sample, no row-level data leaves the customer under schema-only mode — but those three sample values can themselves be PHI, and they can reach a model (§8.2), so schema-only mode reduces exposure without removing it.
8.2 Models, and what PHI routing changes
Columns the deterministic layers leave unresolved go to our mapping ranker (column names and computed profiles only) and then to the model layer through phi-cloud. There is no request option that keeps a map away from the models. A PHI-routed call carries X-PHI: true and the workspace's X-Region, so phi-cloud forwards it only to a PHI-eligible model resident in that region, under a BAA. A standard call goes to a general-purpose model (see /legal/subprocessors). One exception, stated so nobody has to find it: the sandbox that runs a generated reshape program is currently called without the PHI marker, because phi-cloud's BAA gate for that sandbox is not yet enabled; such a run is billed at the standard rate and carries a warning on its result.
The AdaptivMapr Worker only sends X-PHI: true traffic to hosts listed in MAPR_PHI_ALLOWED_HOSTS. If the list is unset in production, PHI-routed requests fail closed; a host not on it is refused before any payload leaves the Worker. The behaviour is pinned by lib/__tests__/phi-host-allowlist.test.ts.
8.3 Do not put PHI in column names
Column names travel further than cell values: they are stored in your workspace's mapping statistics and confirmed layouts, sent to the mapping ranker, and kept in the upload record. Use stable, schema-style identifiers such as patient_id, dob or last_name, never a patient identifier. Statistics are per workspace: there is a Cross-workspace learning setting (off by default, opt_in_global_stats), but no cross-workspace pool is built today, so turning it on shares nothing.
8.4 Hash-chained audit log
Commits, confirmations, key and settings changes, billing events, connector syncs, support-ticket ids and erasure intents are written to public.audit_logs, where the audit_logs_chain_assign trigger assigns each entry a per-workspace chain_seq, hash and prev_hash. Update, delete and truncate are revoked from every API role and a guard trigger refuses them; the only deletes are the retention sweep and workspace erasure. Customers read their own trail via GET /v1/audit and the full export.
Retention. While a workspace exists, each entry is kept for seven years (2,557 days) and then deleted by a daily sweep (app/api/cron/audit-cleanup → public.audit_logs_purge(), at most 10,000 rows per run). When the workspace is erased, its whole chain is deleted with it (§8.6). Entries of workspaces erased before 9 October 2026 were detached from the workspace instead of deleted; a one-off cleanup removes them, and any that remain are deleted by the same sweep six years after they were written.
8.5 Transient-data retention
Parsed uploads live in Cloudflare KV with a hard expirationTtl set at create time from the workspace's retention_hours — 0 (destroyed on commit), 24 (the default), 168 (7 days) or 720 (30 days); see lib/workspaceSettings.ts and Settings → Security & Data. Refresh writes recompute the TTL from the original expires_at, never from the current time. The database mirror is handled as follows: a committed upload is deleted at commit; a sweep every 15 minutes clears the full rows of any expired full-data upload; the record of an upload that expires uncommitted — file name, headers, the clamped sample and the mappings — stays until the workspace is erased. Quote the expires_at returned with the upload rather than a figure from this page.
8.6 Right to erasure
A workspace owner deletes a workspace from /dashboard/settings, and any person deletes their account from Settings → Account. Both run one implementation (lib/workspaceErasure.ts). Both are session-only — a bearer API key cannot trigger them — and both need a CSRF token and a typed confirmation string, so a misclick or a stray request from a connector cannot erase a workspace.
What is erased. Deleting a workspace erases everything in it, for every member: uploads and their stored copies, connectors and their secrets, API keys, chats and stored files, schemas, saved mappings and reshape programs, settings, and the workspace’s audit log. Its phi-cloud key is revoked and its Stripe customer profile is deleted, which also cancels any subscription. Deleting your account erases every workspace where you are the only member and removes you from workspaces you share with others — those stay with their members, including their audit log of what you did there, but your email is removed from their uploads and approvals. Then your sign-in is deleted. The audit log is erased with the workspace. What remains is one anonymous erasure record — a one-way hash of the workspace id, the time, and how many records were removed — so an erasure can be shown to have happened without keeping whose it was.
How. The intent is audited first. Stores outside the database are cleared next, while the database still says what belongs to the workspace: upload copies, webhook secrets, delivery records and dead-letter entries in KV, the phi-cloud sub-key (revoked at phi-cloud), and the Stripe customer (deleted, which cancels any subscription). If any of these fails, the erasure stops and the workspace is left whole for a retry. Then public.erase_tenant() erases the whole database side in ONE transaction — the audit chain, the workspace row and everything that cascades from it — checks every table with a tenant_id column for anything left (any residue rolls the whole erasure back), and writes the anonymous tombstone. Only then are caches cleared and the notification email sent. Neither route reports success unless every step finished; a failed account deletion keeps the sign-in so the person can retry.
What is kept. Kept, for accounting and as evidence of what was agreed: the billing ledger (amounts, dates and Stripe references, no longer linked to the workspace or to you — Stripe keeps the invoices and payments), and signed BAA/DPA records and Terms/Privacy acceptances, which keep the signer’s name, email, IP address and browser and are deleted automatically after six years.
What cannot be erased on demand. Cloudflare Workers Logs (7 days) and Analytics Engine events carry identifiers, never file content, and expire under Cloudflare's retention. Resend keeps its log of sent email. Self-expiring KV markers (email-sent flags, hashed auth-failure counters) expire within 7 days. Supabase daily backups contain erased data until they age out after 7 days; we do not restore an erased workspace from them.
The coverage is pinned by tests: lib/__tests__/erasure-completeness.test.ts fails the build when a migration adds a table that lib/erasureRegistry.ts has not classified, and checks each decision against the foreign key the migrations declare.
8.7 Subprocessors
The current list — with what each receives, where, and under which agreement — is maintained at /legal/subprocessors and in DPA Article 8. Which BAAs are in force for the production deployment is stated in the deployment certificate we send on request.
8.8 Encryption
TLS in transit at the Cloudflare edge (see §2). Cloudflare KV and Supabase Postgres both encrypt stored data at rest (AES-256) by default.
8.9 Access controls
HMAC-signed self-contained API keys (signed with MAPR_KEY_SECRET) for machine access; Supabase Auth sessions for the dashboard. Postgres row-level security is enforced on every tenant table; tables that may hold PHI (uploads, chats, connectors, the prompt-injection signal log) have no user-facing policy at all and are reached only by the service role with an explicit workspace id. The posture is pinned by lib/__tests__/rls-snapshot.test.ts.
8.10 Email scoping
Transactional email — sign-in links, billing receipts, dunning notices, usage alerts — is addressed to workspace members only. Patients are never recipients of AdaptivMapr email, and no template includes content from uploads. Support requests are the exception we cannot control: they carry what the sender typed, travel as ordinary email to our support mailbox, and the form asks for no patient information. A request that contains PHI anyway is deleted and the sender asked to resend without it.
8.11 PHI is not logged
Application logs carry request metadata (route, status, latency, key id, workspace id) and structured error context only. A static check in CI (lib/__tests__/log-scrub.test.ts) fails the build if a log call names a field that could carry record content. Browser error reports are scrubbed in the browser and again on the server before they are logged.
8.12 Human review for risky templates
The 13 medium- and high-risk templates (for example patient_demographics_v1, lab_results_v1, payments_v1, payroll_v1) return requires_hitl: true in the PATCH /v1/uploads/{id}/mappings response, and on the hosted service a commit against one of them needs a human approval recorded from the dashboard, bound to that exact mapping — otherwise it returns 428 hitl_approval_required. A bearer key cannot approve its own commit.
8.13 Prompt-injection signal log
Text bound for a model is checked by a pattern detector for attempts to manipulate the AI. When it fires, a row is written to public.mcp_security_events: the rules and score, the feature and channel, routing, request and workspace ids, the input's length and SHA-256, and an excerpt of the matched text only (≤2,000 characters; it can contain PHI near the match). No IP address or email. The detector is watch-only — it never refuses, rewrites or reroutes a run — and incognito Chat runs are never recorded. Rows are readable only by the service role and operators on the admin allowlist, deleted after 180 days by the daily cleanup or with the workspace, included in the workspace export, and used to train and evaluate a model that detects such attempts.
9. Vulnerability disclosure
We welcome reports from security researchers. Please email security@adaptivmapr.com with:
- A description of the issue and its potential impact;
- Reproduction steps or a minimal proof of concept;
- Your preferred attribution and contact for follow-up.
We commit to:
- Acknowledging receipt within 2 business days;
- Providing a triage status within 5 business days;
- Coordinating disclosure within a 90-day window under a responsible-disclosure policy;
- Not pursuing legal action against researchers acting in good faith and within the scope of this policy.
Out of scope: denial-of-service attacks, social engineering of AdaptivMapr personnel, physical attacks, and findings on third-party SaaS subprocessors (please disclose to the relevant vendor).
10. Incident response
- Recovery. The database can be restored from Supabase's daily backups (up to 7 days back); the application is redeployed from source control.
- Customer notification. We notify affected customers of any confirmed Personal Data Breach within 72 hours, consistent with our DPA Article 11.
- Status page. Live component health and incidents are at adaptivmapr.com/status; email status@adaptivmapr.com to subscribe to updates.
- Post-incident reviews. Customer-affecting incidents receive a written post-incident review within 10 business days, shared with affected customers.
11. Subprocessors
Our current list of subprocessors is maintained at /legal/subprocessors (also cross-referenced from /legal/dpa#subprocessors). New subprocessors are announced at least 30 days before they process customer data.
12. Contact
Security disclosures: security@adaptivmapr.com. Data protection requests: dpo@adaptivmapr.com. General contact: hello@adaptivmapr.com.