GraphQL

Query your Plexivo Postgres tables with GraphQL, including live queries that update as data changes.

Every Plexivo Postgres with the Data API on is also a GraphQL API. The schema is built from the tables and functions your Data API exposes, so it follows your database as it changes, and row-level security applies to every field.

The tables on this page#

sql
create table customers (
  id bigint generated always as identity primary key,
  owner uuid not null default auth.uid(),
  name text not null
);
create table orders (
  id bigint generated always as identity primary key,
  owner uuid not null default auth.uid(),
  customer_id bigint references customers (id),
  total numeric(10, 2) not null,
  status text not null default 'open',
  created_at timestamptz not null default now()
);
alter table customers enable row level security;
alter table orders enable row level security;
create policy own on customers for all to authenticated using (owner = auth.uid()) with check (owner = auth.uid());
create policy own on orders for all to authenticated using (owner = auth.uid()) with check (owner = auth.uid());
create trigger plexivo_realtime after insert or update or delete on orders
  for each row execute function auth.realtime_notify('id');
js
import { createClient } from '@plexivo/client';

const db = createClient(PLEXIVO_DATA_URL, PLEXIVO_PUBLISHABLE_KEY);
await db.auth.signUp({ email, password });
const asha = await db.from('customers').create({ name: 'Asha' });
await db.from('orders').create([{ customer_id: asha.id, total: 1299 }, { customer_id: asha.id, total: 480 }]);

The GraphQL endpoint#

Send queries to your Project URL followed by /graphql/v1, with the same keys and user tokens as the Data API. POST takes one query or a batch of up to 20; GET runs queries only.

bash
curl "$PLEXIVO_DATA_URL/graphql/v1" \
  -H "apikey: $PLEXIVO_PUBLISHABLE_KEY" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query":"{ orders(limit: 5) { id total } }"}'

Try queries first in the GraphQL tab’s Playground, As a visitor or As the service role, and download the schema as a .graphql file from Schema.

The schema#

For a table orders, you get:

FieldWhat it does
orders(where, orderBy, limit, offset)Reads rows
ordersByPk(id)Reads one row by its primary key
ordersAggregate(where)Counts and aggregates
insertOrders, insertOrdersOneInserts, with onConflict for an upsert
updateOrders, updateOrdersByPkUpdates, with set and inc
deleteOrders, deleteOrdersByPkDeletes
  • Each column is a field of the same name. Each foreign key is a field both ways: customer on an order for customer_id, and orders on a customer, with its own where, orderBy and limit.
  • Filters: eq, neq, gt, gte, lt, lte, in, nin, isNull, like, ilike, regex, iregex; contains and containedIn for arrays and JSON, and overlaps for arrays; search on full-text (tsvector) columns; and and, or and not.
  • Your exposed SQL functions appear as queries when they are stable or immutable, and as mutations otherwise. PostgreSQL enums become GraphQL enums.

Queries and mutations#

graphql
query OpenOrders {
  orders(where: { status: { eq: "open" } }, orderBy: [{ created_at: DESC }], limit: 20) {
    id
    total
    customer { name }
  }
  ordersAggregate(where: { status: { eq: "open" } }) { count }
}

mutation Pay($id: BigInt!) {
  updateOrdersByPk(id: $id, set: { status: "paid" }) { id status }
}

All the mutations in one request commit together or not at all. A bigint column is a BigInt, sent as a string so no digit is lost; numeric is a BigFloat, and timestamps are Datetime.

js
const { data, errors } = await db.graphql(
  'query($s: String!) { orders(where: { status: { eq: $s } }) { id total } }',
  { s: 'open' }
);
dart
import 'package:plexivo_client/plexivo_client.dart';

final db = PlexivoClient(dataUrl, publishableKey);
await db.signUp(email: email, password: password);

final result = await db.graphql(
  r'query($s: String!) { orders(where: { status: { eq: $s } }) { id total } }',
  variables: {'s': 'open'},
);

Send { persisted: true } in JavaScript, or persisted: true in Dart, to use automatic persisted queries: after the first time, only the query’s hash travels.

Live queries#

A live query sends its answer now, and again whenever a row it reads changes. Every table it reads must have Realtime switched on.

js
const live = db.graphql.subscribe(
  'subscription { orders(where: { status: { eq: "open" } }) { id total } }',
  {},
  ({ data }) => console.log(`${data.orders.length} open orders`)
);

// A new order sends the answer again
setTimeout(() => db.from('orders').create({ customer_id: asha.id, total: 99 }), 1000);
setTimeout(() => live.unsubscribe(), 8000);
dart
final stop = await db.graphqlSubscribe(
  'subscription { orders(where: { status: { eq: "open" } }) { id total } }',
  (result) => print(result['data']),
);

await stop();

Changes that arrive together are sent as one update. One live-query channel holds up to 20 live queries; the SDKs open a channel for each live query.

Security#

GraphQL runs every field as the caller, exactly like the Data API: tables and functions not exposed there are not in the schema, and row-level security decides which rows each user sees and writes.

Limits#

  • Queries nest up to 12 levels and select up to 500 fields.
  • A query document is up to 100,000 bytes.
  • A list returns up to 1,000 rows.

Next steps#

Updated 28 October 2026