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.

POST /v1/gatewaykind: supabase

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. Step 1

    Build the endpoint

    <project-url>/rest/v1/<table>, or your own mount path if the base URL already ends in one.
  2. Step 2

    Authenticate twice

    Supabase wants both apikey and Authorization; a bare PostgREST reads only the second. Both are sent from one stored key.
  3. Step 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. Step 4

    Report honestly

    written_rows, batches and failed_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.

How · on_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.

How · Pointless 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.

How · POST /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.

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

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.

KeyRequiredWhat it is
urlRequiredThe PROJECT url, e.g. https://<ref>.supabase.co. A table endpoint is refused — drop the /rest/v1/<table> suffix and set table instead.
tableRequiredTarget table. Must be a bare identifier. Overridable per call with destination.table.
auth_valuesecretRequiredA service-role or table-scoped API key / PostgREST JWT. Encrypted at rest.
schemaOptionalA 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/v1 appended.
  • dry_run reports the batch count without sending anything.
POST /v1/connectors
{
  "kind": "supabase",
  "name": "Clinical warehouse",
  "config": {
    "url": "https://abcdefgh.supabase.co",
    "table": "lab_results",
    "schema": "clinical",
    "auth_value": "eyJhbGciOi…"
  }
}
curl
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"]
    }
  }'
response
{
  "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": []
  }
}
→ 1 839 of 1 840 rows upserted · 1 flagged and not written · nothing echoed back

Limits & failure modes

What it refuses, and what it tells you

CodeWhenWhat to do
400 config_invalidThe 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_requiredNo table on the connector and none in the request.Set config.table or pass destination.table.
502 destination_write_failedEvery 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_mismatchA supplied schema names a column the table does not have.Caught in pre-flight, BEFORE anything is written.
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.

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

A PostgREST delivery 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

No. The protocol is PostgREST, so any PostgREST deployment works — Supabase is the most common one. AdaptivMapr builds the table endpoint from your base URL and, if that URL already ends in its own mount path, respects it instead of appending /rest/v1.
The narrowest one that can insert into the target table. A key scoped to that table, or an RLS policy that limits a broader key, is strongly preferred over a full service-role key. Whatever you store is envelope-encrypted before it reaches Postgres, held service-role-only, masked on read, and rotatable in place via /rotate-secret.
Pass destination.on_conflict with the columns that identify a row. That turns the batch into an upsert — PostgREST resolution=merge-duplicates. Without it the write is a plain insert, which is deliberate: a silent overwrite of customer data should never be the default behaviour.
No. The test does a read with limit=0. That proves the project URL resolves, the key authenticates and the table is visible to that key — the three things that actually go wrong — without inserting a test row. The response names which check ran, so the result never implies more assurance than was obtained.
No. Every insert carries Prefer: return=minimal, so the response is a count rather than a second copy of your data. The delivery report gives written_rows, batches and failed_batches; row-level validation errors are reported by field and row index, without the value.

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.

Ready when you are

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 a few sample rows ever leave you.

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