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.

POST /v1/gatewaykind: sql_httpkind: sql_write

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. 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.
  2. 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.
  3. 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.
  4. 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.

KeyRequiredWhat it is
urlRequiredThe https query endpoint. A postgres:// / mysql:// DSN is refused at save time with an explicit message — there is no socket to dial it with.
queryRequiredsql_http only: the statement to run. Sent as-is; the endpoint owns permissions.
tableOptionalsql_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_fieldOptionalJSON body field carrying the SQL. Default query.
rows_pathOptionalDotted path to the row array in the response. Default rows; . means the body itself.
paramsOptionalOptional bind parameters, sent as params.
authOptionalbearer or header. With header, auth_value is a whole Name: value pair.
auth_valuesecretOptionalThe 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": "…"
  }
}
curl
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
    }
  }'
response
{
  "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": []
  }
}
→ 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

CodeWhenWhat to do
400 config_invalidA 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_missingA 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_invalidThe 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_unsupportedA sql_write destination is given on_conflict.The generated statement is a plain INSERT. Refused rather than silently ignored.
502 destination_write_failedEvery batch was rejected.A configuration or permission problem, not a partial write — surfaced with the endpoint’s own error text.
400 ssrf_blockedThe 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_largeThe 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.

ChargeRateNotes
Every map$0.001A 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 ranat cost × 2Layer-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.

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.

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.

$10 minimum to start · pay only for what you map · PHI under BAA coverage