Your query is the source. Your table is the target.
Point a connector at any SQL-over-HTTP endpoint — Neon’s /sql, a PostgREST RPC, Cloudflare D1’s REST API, or your own thin query proxy — and read rows out of it or write mapped rows into it.
A Cloudflare Worker has no raw TCP socket, so a postgres:// DSN cannot be dialled from here — and rather than pretend otherwise, the connector speaks the protocol every managed Postgres now fronts anyway. You give it an HTTPS query endpoint and a statement; it POSTs the statement, unwraps the row array, and maps it. Pointed the other way, it generates parameter-free INSERT statements from the mapped rows and sends them in bounded batches. Two connector kinds, sql_http to read and sql_write to write, because a read credential and a write credential should not be the same record.
Step 1
POST the statement
The SQL rides in a JSON body field — query by default, renamed with config.query_field for an endpoint that expects something else.
Step 2
Unwrap the rows
config.rows_path is a dotted path to the row array — rows by default, . when the body IS the array. A non-row-shaped payload is named, not swallowed.
Step 3
Map the grid
Column names become headers and the cascade runs against your template, exactly as if the rows had arrived as a CSV.
Step 4
Or write it back
A sql_write destination generates INSERT statements in batches of 500, each batch independent so a partial failure is reported rather than hidden.
What you get
Built for files that keep arriving
Incremental
A watermark you put in your own SQL
Put {{since}} in the WHERE clause and each run substitutes the last successful sync timestamp. Leave it out and the run is honestly reported as a full re-read.
How · WHERE updated_at > '{{since}}' becomes an ISO-8601 literal at run time and the result carries incremental: "incremental". Without the token it reports "unsupported" — a bare POST has no conditional-request semantic to lean on.
Write-back
Independent batches, honest partial failures
Rows go in batches of 500. A batch that fails is named in the response; a run where every batch failed is a loud error, not a success that wrote nothing.
How · failed_batches lists the indices. When all batches fail the call is 502 destination_write_failed carrying the endpoint’s own message. Column order is fixed by the mapping, and identifiers are validated before a statement is built.
Schema
The table already declares its columns
A destination is introspectable, so you do not restate a schema that would drift the moment someone adds a column.
How · GET /v1/connectors/{id}/schema reads column names, types and nullability — metadata only, never row data — and returns a ready-to-use template. POST /v1/gateway does it for you.
Secrets
The credential never travels in a request body
You reference a connector by id. Keys, tokens and service-account JSON are encrypted at rest and read only by the code that makes the call.
How · normalizeSecretField() folds every provider spelling — private_key, token, secret_access_key, account_key, service_account_json — into one auth_value field, which is KEK-envelope-encrypted before the row is written. A GET masks it to a 4-character hint. If encryption fails the field is dropped rather than stored in plaintext.
Configuration
The connector record, field by field
The most common misconfiguration — a wire-protocol DSN pasted into a field that wants an HTTP endpoint — is caught by a regex at create time and answered with the reason, not a generic 400.
Key
Required
What it is
url
Required
The https query endpoint. A postgres:// / mysql:// DSN is refused at save time with an explicit message — there is no socket to dial it with.
query
Required
sql_http only: the statement to run. Sent as-is; the endpoint owns permissions.
table
Optional
sql_write only: the target table. Must be a bare identifier — letters, digits, underscore — or the save is refused. Overridable per call with destination.table.
query_field
Optional
JSON body field carrying the SQL. Default query.
rows_path
Optional
Dotted path to the row array in the response. Default rows; . means the body itself.
params
Optional
Optional bind parameters, sent as params.
auth
Optional
bearer or header. With header, auth_value is a whole Name: value pair.
auth_valuesecret
Optional
The token or header value. Encrypted at rest.
In code
One call: query in, table populated.
A saved query as the input and a saved table as the destination. AdaptivMapr reads the target’s own columns, maps onto those, validates every row, writes the ones that pass, and returns the report.
POST/v1/connectorsSave the connector. The secret is encrypted before it reaches Postgres.session
POST/v1/connectors/{id}/testMake a real call and report what was actually proven.session
POST/v1/connectors/{id}/syncPull now. Accepts ?since=<iso> to override the watermark.bearer
GET/v1/connectors/{id}/schemaRead the target’s own columns — metadata only, never row data.bearer
POST/v1/gatewayAny input in, this destination populated, a delivery report out.bearer
POST/v1/connectors/{id}/rotate-secretReplace the credential in place; the old one becomes unrecoverable.session
dry_run validates and reports what WOULD be written without sending anything — the safe first call against a production table.
Upsert is not available on sql_write: the generated statement is a plain INSERT, and asking for on_conflict returns 400 upsert_unsupported rather than quietly inserting duplicates. Use a PostgREST destination, or handle conflicts in your endpoint.
Scope the write credential to a role with INSERT on the target table and nothing else. AdaptivMapr holds it to write mapped rows into your database; it should not be able to do more than that.
POST /v1/connectors · the read side
{
"kind": "sql_http",
"name": "Neon — new results",
"config": {
"url": "https://ep-cool-cell-123.eu-central-1.aws.neon.tech/sql",
"query": "select * from raw_results where updated_at > '{{since}}'",
"rows_path": "rows",
"auth": "bearer",
"auth_value": "…"
}
}
→ dry run · 4 batches would be sent · nothing written · credentials never left the connector record
Limits & failure modes
What it refuses, and what it tells you
Code
When
What to do
400 config_invalid
A postgres:// or mysql:// DSN was supplied as the URL.
The Worker runtime has no raw TCP socket. The message names the alternatives: Neon’s /sql, a PostgREST RPC, D1’s REST API, or your own proxy.
400 sql_query_missing
A sql_http connector has no statement.
Checked before any DNS or network work — a connector that can never succeed does not get to resolve a host first.
502 sql_response_invalid
The endpoint answered, but not with an array of row objects at rows_path.
The message says where it looked and tells you to set config.rows_path. Scalars and nested arrays are refused too.
400 upsert_unsupported
A sql_write destination is given on_conflict.
The generated statement is a plain INSERT. Refused rather than silently ignored.
502 destination_write_failed
Every batch was rejected.
A configuration or permission problem, not a partial write — surfaced with the endpoint’s own error text.
400 ssrf_blocked
The configured host resolves to a private, link-local or loopback address.
Every outbound request is DNS-resolved and checked before it is made, on the scheduled path and the on-demand path alike. The reason is returned with the code.
413 fetch_too_large
The result set exceeds 25 MiB.
Narrow the query or page it with the {{since}} watermark.
Incremental sync
Opt-in and explicit. A {{since}} token anywhere in the statement is replaced with the last successful sync timestamp as an ISO-8601 literal, and the run reports incremental: "incremental". A statement without the token is a full re-read every time and says so — "unsupported" — rather than implying a delta nobody asked for.
PHI & residency
A SQL-over-HTTP run runs under the workspace’s routing policy, not around it. PHI routing is a separate axis from the data mode: it sends X-PHI and X-Region to phi-cloud so a regulated run lands on an in-region, BAA-eligible model, it costs +20% on the whole charge, and it is locked until the workspace accepts the BAA in Settings → Security & Data. An explicit PHI ask without an acceptance is 403 agreement_required, never a silent downgrade. A standard run keeps the workspace’s region pin — the region decides where compute may run, and the sandbox refuses a region-less run.
What it costs
Billed on the same prepaid wallet
Moving bytes is not a line item. A sync that pulls a file and a destination write that lands the rows are both part of one map, and the map is what the wallet sees. There is no free tier and no subscription — top up from $10, a balance shared across the phi-cloud suite.
Charge
Rate
Notes
Every map
$0.001
A flat per-map fee — a few tokens — charged even when the run was fully deterministic or hit the layout cache and used no AI at all.
AI, only when it ran
at cost × 2
Layer-5 cleanup, any-to-any convert and structural reshape bill the phi-cloud tokens actually consumed. Bring your own model key and it is × 0.5.
PHI / enterprise routing
+20%
Multiplies the whole charge, flat fee included — and only when the run genuinely got that routing. Locked until the workspace accepts the BAA in-app.
Questions
Before you wire it up
Because the Worker runtime that serves the API has no raw TCP socket, so a wire-protocol DSN cannot be dialled from it. Rather than accept a string that would fail at 3am, the connector refuses it at save time and names what does work: Neon’s /sql endpoint, a PostgREST or Supabase RPC, Cloudflare D1’s REST API, or a thin query proxy of your own. The capability is the same; the transport is HTTP.
Put {{since}} in the statement — for example, where updated_at > '{{since}}'. Each run substitutes the last successful sync timestamp as an ISO-8601 literal. Leave it out and every run is a full re-read, and the response reports incremental: "unsupported" so you are never told there was a delta when there was not.
Not on sql_write — the generated statement is a plain INSERT, and passing on_conflict returns 400 upsert_unsupported rather than inserting duplicates. A PostgREST/Supabase destination does support it: on_conflict adds resolution=merge-duplicates. Plain insert is the default everywhere so a re-run cannot silently overwrite your data.
Whatever role you scope it to — so scope it narrowly. For a write destination that means a role with INSERT on the target table and nothing else. The credential is envelope-encrypted at rest, service-role-only, never accepted in a request body, and rotatable in place. Every write is SSRF-guarded and read from the connector record, never from a caller-supplied field.
No. The destination table already declares its columns, so POST /v1/gateway reads them and maps onto those by default. Supply schema only when you mean something different — a deliberate subset — and the request is still pre-flighted against the real table: naming a column that does not exist fails 422 schema_destination_mismatch before anything is written.
Neon, Cloudflare D1 and PostgREST are trademarks or projects of their respective owners. Named here to describe interoperability only — no affiliation, endorsement or partnership is claimed.
Ready when you are
Point it at SQL over HTTP. Get your schema back.
Start with a $10 prepaid wallet. Every map draws a few tokens; in schema-only mode only headers and a few sample rows ever leave you.