Integration · Database
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.
- Method
- POST
- Connector kinds
- 2
How it works
What actually happens on a sync run
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.
- 1
POST the statement
The SQL rides in a JSON body field —queryby default, renamed withconfig.query_fieldfor an endpoint that expects something else. - 2
Unwrap the rows
config.rows_pathis 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. - 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. - 4
Or write it back
Asql_writedestination 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.
HowWHERE 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.
Howfailed_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.
HowGET /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.
HownormalizeSecretField() 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_runvalidates 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 foron_conflictreturns400 upsert_unsupportedrather than quietly inserting duplicates. Use a PostgREST destination, or handle conflicts in your endpoint. - Scope the write credential to a role with
INSERTon 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.
{
"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": "…"
}
}curl https://api.adaptivmapr.com/v1/gateway \
-H "Authorization: Bearer $MAPR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input": { "connector_id": "con_4a91…" },
"destination": {
"connector_id": "con_e07b…",
"table": "lab_results",
"dry_run": true
}
}'{
"schema_id": "lab_results_v1",
"source": "destination",
"row_count": 1840,
"destination": {
"connector_id": "con_e07b…",
"kind": "sql_write",
"table": "lab_results",
"schema_source": "destination",
"preflight": "not_needed",
"protocol": "sql",
"dry_run": true,
"written_rows": 0,
"batches": 4,
"failed_batches": []
}
}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
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, no seats and no contract — 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
Why not just give you a postgres:// connection string?
How do I make a scheduled read incremental?
Can it upsert?
What can the credential you hold actually do?
Do I have to declare the target schema?
Verified against lib/connectorSyncRunner.ts · lib/sqlWrite.ts · lib/destinations.ts · lib/introspect.ts · app/api/v1/gateway/route.ts
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.
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 up to three sample rows, clamped to 80 characters a cell, ever leave you.