Integration · Database
Mapped rows, inserted or upserted.
POST batched JSON rows into PostgREST — Supabase or any PostgREST deployment. Insert by default, upsert when you name the conflict columns, and the database’s own error text when something is wrong.
- Method
- POST
- Connector kind
- 1
How it works
What actually happens on a sync run
PostgREST does not accept raw SQL — it takes an array of row objects at a table endpoint — which is why this is its own protocol rather than a flag on the SQL destination. AdaptivMapr builds the endpoint from your project URL and table, sends rows in batches of 500 with Prefer: return=minimal, and reports what landed. Naming conflict columns turns the insert into an upsert; leaving them out means a re-run can never silently overwrite what is already there.
- 1
Build the endpoint
<project-url>/rest/v1/<table>, or your own mount path if the base URL already ends in one. - 2
Authenticate twice
Supabase wants both apikey and Authorization; a bare PostgREST reads only the second. Both are sent from one stored key. - 3
Send batched rows
500 rows per request, each batch independent, with return=minimal so the rows are not echoed back — pointless bandwidth, and for PHI an avoidable second copy. - 4
Report honestly
written_rows,batchesandfailed_batches. Every batch failing is a 502 with PostgREST’s own message, not a success.
What you get
Built for files that keep arriving
Upsert
Insert by default, merge when you say so
Name the conflict columns and the batch becomes an upsert. Leave them out and a re-run cannot overwrite a row you did not mean to touch.
Howon_conflict adds the PostgREST query parameter and Prefer: resolution=merge-duplicates. Opt-in by design: a silent overwrite of customer data is never the default.
Minimal
The rows are not echoed back
Every insert carries return=minimal, so the data you just sent is not written a second time into a response body.
HowPointless bandwidth for any payload, and for PHI an avoidable second copy in a second place. The response is a count, not your data.
Test
A connection test that writes nothing
Testing the connector does a read with limit=0 — it proves the URL resolves, the key authenticates and the table is visible to that key, without inserting a test row into your table.
HowPOST /v1/connectors/{id}/test names which check it actually ran, so the UI never implies more assurance than was obtained. Nothing invents rows in a customer table.
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
Prefer a key scoped to that one table — or an RLS policy that limits it — over a full service-role key. The create response says exactly that, because it is the difference between a credential that can insert and one that can do anything.
| Key | Required | What it is |
|---|---|---|
| url | Required | The PROJECT url, e.g. https://<ref>.supabase.co. A table endpoint is refused — drop the /rest/v1/<table> suffix and set table instead. |
| table | Required | Target table. Must be a bare identifier. Overridable per call with destination.table. |
| auth_valuesecret | Required | A service-role or table-scoped API key / PostgREST JWT. Encrypted at rest. |
| schema | Optional | A non-public Postgres schema, sent as the Content-Profile header. |
In code
A spreadsheet in, your table upserted.
Name the input and the destination. The target columns come from the table itself, every row is validated, and only the ones that pass are written.
- 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 - 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
- PostgREST’s own error text is passed through — “column X does not exist”, “violates row-level security policy” — because hiding it behind a generic 502 makes these genuinely hard to debug.
- This works against any PostgREST deployment, not only Supabase. If your base URL already carries its own mount path, it is respected rather than having
/rest/v1appended. dry_runreports the batch count without sending anything.
{
"kind": "supabase",
"name": "Clinical warehouse",
"config": {
"url": "https://abcdefgh.supabase.co",
"table": "lab_results",
"schema": "clinical",
"auth_value": "eyJhbGciOi…"
}
}curl https://api.adaptivmapr.com/v1/gateway \
-H "Authorization: Bearer $MAPR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input": { "url": "https://acme.example.com/exports/labs.xlsx" },
"destination": {
"connector_id": "con_e07b…",
"on_conflict": ["patient_id", "loinc_code", "taken_at"]
}
}'{
"schema_id": "lab_results_v1",
"source": "destination",
"row_count": 1840,
"errors": [
{ "row_index": 812, "field": "loinc_code", "code": "loinc_format", "message": "expected NNNNN-N" }
],
"destination": {
"connector_id": "con_e07b…",
"kind": "supabase",
"table": "lab_results",
"schema_source": "destination",
"preflight": "not_needed",
"protocol": "postgrest",
"written_rows": 1839,
"batches": 4,
"failed_batches": []
}
}Limits & failure modes
What it refuses, and what it tells you
| Code | When | What to do |
|---|---|---|
400 config_invalid | The URL is a table endpoint rather than the project root. | A very common paste error. Drop the /rest/v1/<table> suffix and set config.table — the endpoint is built for you. |
400 table_required | No table on the connector and none in the request. | Set config.table or pass destination.table. |
502 destination_write_failed | Every batch was rejected. | Returned with PostgREST’s own message — an RLS violation and a missing column read very differently, and you need to know which. |
422 schema_destination_mismatch | A supplied schema names a column the table does not have. | Caught in pre-flight, BEFORE anything is written. |
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. |
Incremental sync
Not applicable: this is a write destination, not a source. What limits repetition here is the write mode — a plain insert by default, an upsert only when you name the conflict columns — so re-running a delivery does not quietly rewrite rows you did not intend to touch.
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
Does this only work with Supabase?
What key should I give it?
How do I avoid duplicate rows on a re-run?
Will testing the connector write anything into my table?
Do the rows I write come back in the response?
Verified against lib/destinations.ts · lib/introspect.ts · app/api/v1/connectors/route.ts · app/api/v1/connectors/[id]/test/route.ts
Supabase is a trademark of Supabase, Inc.; PostgREST is an independent open-source project. Named here to describe interoperability only — no affiliation, endorsement or partnership is claimed.
Point it at Supabase. 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.