Platform
One engine. Any data in, your schema out.
A stateless, pure-compute mapping cascade that turns any CSV, Excel, JSON, or SQL into your schema — resolving most columns on free deterministic layers before any AI is touched.
- Headers + ≤3 rows, ≤80 chars a cell
- HMAC self-contained API keys
- RLS on the dashboard plane
- Uploads expire from KV — 24 h by default
- In-region PHI routing under a BAA
The cascade
Six layers, cost-ordered — the engine stops the moment a column resolves.
Each header runs down a ladder from free and deterministic to metered AI. The instant a layer is confident, the cascade accepts and moves on — so most files are fully mapped before the paid layer is ever touched. That early exit is the single biggest cost lever in the system.
- Layer 1
Statistics
Auto-accept from confirmed history: a header maps when past confirmations for it agree at a production-tested ratio.
{minN:100, minRatio:0.95} · {minN:20, minRatio:1.00}free · deterministic
- Layer 2
Heuristic
normalize() strips accents, punctuation and whitespace, then compares the header against each field’s column, label and every hint (DE/FR/IT/EN/ES).
exact = 1.00 · substring = 0.85free · pure compute
- Layer 3
Fuzzy
Token-set ratio plus Levenshtein over the normalized strings — tolerates typos and token-order drift ("Frist Name", "name first").
FUZZY_AUTO_ACCEPT = 0.80free · pure compute
- Layer 3½
Mapping ranker
A fine-tuned mDeBERTa cross-encoder we host ourselves scores every still-unresolved column against each template field and against "none", then assigns one-to-one. Only its confident lane resolves; a confident "none" does not. Header text and a locally computed value profile are all it receives — never a cell value.
lane: execute · RANKER_PROFILE_ROWS = 40self-hosted model · metered per column resolved
- Layer 4
Semantic
Embeddings cosine over the header against each field’s label + hints. Cached, and silently skipped when no embedding key is configured.
SEMANTIC_THRESHOLD = 0.78cheap · cached
- Layer 5
LLM
Every header still unresolved goes into ONE collision-aware batched call, constrained to your column set and told which fields are already claimed.
1 call per file · llmMatchHeadersBatch()metered
Contests are settled by quality, not column order: layers 1–3 all propose, the engine arbitrates once on (layer rank, confidence, column index), then runs layer 4 only for what is still unresolved. A target field, once assigned, cannot be claimed twice — you can’t emit two source columns into one field. How the cascade works →
Structural understanding
Real-world grids are messy. The engine reads them before it matches.
Before a single header is compared, a deterministic pass (lib/structure.ts) turns spreadsheets-as-humans-made-them into clean logical tables. No LLM, no configuration — just structure recovery.
Headers that aren’t on row 0
Detects the real header row under title banners, logos, and blank lead-in rows — no “skiprows” guesswork required.
Multi-row & grouped headers
Flattens stacked header bands into a single logical name, joining a group and its child — “Instrument › Variable” — so each column reads cleanly.
Unit rows captured
Recognizes a units row beneath the header and carries it as column metadata instead of poisoning the first data row.
Transposed sheets un-pivoted
Auto-detects wide → long layouts where variables run down a column, and unpivots them back into tidy rows — deterministically, no LLM.
Reshape: beyond renaming
When mapping isn’t enough, the engine reshapes — safely.
Some inputs need more than column renaming: a pivot, a split, a join across sheets. A router picks the cheapest strategy that works, and the model — when one is used at all — never sees your data.
direct
The plain cascade. Columns map one-to-one; nothing to reshape. Free and deterministic.
HowNo program is authored at all — the mapping IS the transform.
plan
A declarative JSON DSL run in-process — deterministic and byte-reproducible. Same input, same output, every time. No code execution, no LLM.
HowRe-run a plan and you get identical bytes back.
code
Generated Python / R / SQL run in an isolated sandbox with no network. The model sees only the table shape and a clamped preview — never the full dataset.
HowEvery prompt carrying customer content is fenced; the generated program runs behind network isolation.
Across every generated path the boundary holds: the model is shown a skeleton — headers plus a clamped preview — and writes a transform it never gets to run against your full records. See how reshape works →
Two modes, one boundary
Schema-only, or full-data under a BAA — the boundary is one code path.
Both modes run the same in-process cascade. The only difference is what may leave you, and both are gated at a single clamp — not scattered across routes.
| Schema-only | Full-data | |
|---|---|---|
| What leaves you | Headers + ≤3 rows, 80 chars a cell. | The same clamp, plus the surviving cells on the layer-5 call. |
| Where it runs | Entirely in-process — the deterministic cascade never phones out. | In-process, except the one layer-5 call, under X-PHI + X-Region. |
| What it takes | Nothing — a DPA is usually not needed (provided on request). | A PHI entitlement and a BAA accepted in-app. |
| The clamp | clampForSchemaOnly() at the HTTP edge of every route. | The same function, the same code path. |
The API
Every surface is a REST route you can read.
No SDK required and no bespoke verbs: the engine is a dozen routes under /v1, each doing one thing. These are the paths as they exist in the codebase.
- POST
/v1/uploadsStage a CSV, Excel, JSON or SQL export. Returns an upload id; the bytes live in KV for 24 hours. - POST
/v1/uploads/{id}/matchRun the six-layer cascade over the file’s headers against a template. - POST
/v1/uploads/{id}/match/streamThe same run as Server-Sent Events — a frame per cascade layer.streams - PATCH
/v1/uploads/{id}/mappingsOverride a proposed mapping. Medium- and high-risk templates come back with requires_hitl. - POST
/v1/uploads/{id}/commitConfirm the mapping. Feeds layer 1 and records the layout fingerprint for one-click reuse. - POST
/v1/matchHeaders plus ≤3 clamped sample rows, no key required. Layers 1–4 only — an anonymous call can never reach the metered layer.public - POST
/v1/reshapePivot, split, join across sheets. The router picks direct, plan or code, and echoes which it chose. - POST
/v1/convertAny-to-any import: images, PDF, audio, HTML and URLs into structured rows. - POST
/v1/layouts/lookupReuse the last confirmed mapping for an identical header row — a fingerprint cache hit, no AI. - POST
/v1/layouts/driftDiff a new field set against the newest confirmed layout: stable, first_seen, or drift with an added/removed diff. - GET
/v1/templates33 templates across 7 packs, with their fields, multilingual hints and validators. - GET
/v1/usageMetered usage for the workspace; ?phi=1 adds token consumption and cost.
The public host is api.adaptivmapr.com. Dashboard-session routes live under /v1/me/* and are CSRF-gated. Full API reference →
For developers
One call, and the layer is in the answer.
Every match carries the layer that produced it and the confidence it produced it at. You can prove which columns cost nothing — and exactly which ones reached the metered layer.
- REST /v1 — uploads, match, mappings, commit, reshape, convert, layouts, templates, usage
- HMAC self-contained keys — a key verifies without a database lookup; revocation is a check layered on top
- SSE progress — /match/stream and /me/reshape/stream emit a frame per phase, then the same terminal JSON
- MCP — adaptivmapr.match_headers calls POST /v1/match, clamped to the schema-only contract at the edge; free layers 1–3 with no key, plus the ranker and AI (billed to the wallet) when a key is set
curl -s https://api.adaptivmapr.com/v1/match \
-H 'content-type: application/json' \
-d '{
"template_id": "lab_results_v1",
"headers": ["Patient-ID","LOINC","Wert","Einheit","Entnahme"],
"sample_rows": [["P-1042","2345-7","5.4","mmol/L","2026-03-11"]]
}'{
"template_id": "lab_results_v1",
"matches": [
{ "source_col": "Patient-ID", "target_field": "patient_id", "confidence": 1, "source": "heuristic" },
{ "source_col": "LOINC", "target_field": "loinc_code", "confidence": 0.85, "source": "heuristic" },
{ "source_col": "Wert", "target_field": "value", "confidence": 1, "source": "heuristic" },
{ "source_col": "Einheit", "target_field": "unit", "confidence": 1, "source": "heuristic" },
{ "source_col": "Entnahme", "target_field": "taken_at", "confidence": 1, "source": "heuristic" }
],
"auto_accept_threshold": [
{ "minN": 100, "minRatio": 0.95 },
{ "minN": 20, "minRatio": 1 }
],
"cascade_layers": ["statistics","heuristic","fuzzy","ranker","semantic","ai"],
"unmapped": []
}Built for regulated data
Compliance isn’t a mode — it’s the substrate.
The cascade is the product, but the deployed engine is a full stateful SaaS with the controls regulated teams have to answer for. These are wired in, not roadmap.
Hash-chained audit trail
Every commit and confirmation lands in the audit_logs table, where a trigger assigns a per-tenant chain sequence, hash and prev_hash — a tamper-evident, replayable record of who mapped what. The external Chainlog emit is the database-less fallback.
Howmapping.commit carries review_approved and the attempt count.
Uploads expire on a TTL you set
Uploaded files live in Cloudflare KV under a TTL your workspace sets — 24 hours by default, or 0, 168 or 720 — and then vanish. 0 means the upload is destroyed on commit; the response carries the actual expires_at, which is the value to trust.
Howlib/uploads.ts — createUpload writes retention_hours * 3600 into the KV expirationTtl, mirrored to Supabase.
KEK-encrypted secrets
Connector credentials and other secrets are envelope-encrypted under a key-encryption key, scoped per tenant and never returned in plaintext.
HowGET /v1/connectors/{id}/secret-meta returns metadata, never the secret.
Strict CSP, per-request nonce
Every page ships a strict Content-Security-Policy with a fresh nonce minted per request — inline scripts that aren’t ours simply don’t run.
Howmiddleware.ts mints x-nonce; every executable inline script carries it.
Fail-closed PHI allowlist
Full-data PHI traffic only reaches BAA-covered hosts on an explicit allowlist. If it’s unset in production, PHI routing refuses rather than leaks.
HowMAPR_PHI_ALLOWED_HOSTS — /api/health reports the component down when unset.
HIPAA-ready, GDPR & nFADP posture
Regulated-data-first by design: schema-only ships headers plus three clamped rows and nothing else, full-data routes in-region under a BAA, and residency is enforced at the edge. SOC 2 Type II is in progress.
HowBAA + confidentiality terms are accepted in-app and recorded in tenant_agreements.
One key unlocks the whole engine.
Schema-only mode runs the same cascade on your prepaid wallet — every map is a few tokens; only files that need the paid layer add AI token cost.