Appearance
Querying Data
Table-backed fields — links, artists, link_destinations, link_tracks, optins, fans, pixels, domains, and the rest — share a common set of arguments for filtering, sorting, pagination, and aggregation. Learn them once and they apply everywhere.
Custom resolvers (login, create_link, link_statistics, search_artists) do not take these arguments; they have fixed signatures documented on their own pages.
Arguments
Every table field accepts:
| Argument | Type | Purpose |
|---|---|---|
where | <table>_bool_exp | Filter rows. |
order_by | [<table>_order_by!] | Sort rows. |
limit | Int | Maximum rows to return. |
offset | Int | Rows to skip. Use only with order_by. |
distinct_on | [<table>_select_column!] | Return one row per distinct value of a column. |
Each table also exposes two companions:
<table>_by_pk(id: uuid!)— fetch a single row by primary key, returning the object ornull.<table>_aggregate(...)— counts and numeric aggregates over the same filter.
Filtering
where takes a boolean expression. Each column maps to a comparison object.
graphql
query {
links(
where: {
artist_id: { _eq: "3aad8009-a307-4429-a586-8b3dbe39cdda" }
type: { _eq: "release" }
created_at: { _gte: "2026-01-01" }
}
) {
id
path
title
}
}Conditions at the same level are combined with AND.
Comparison operators
| Operator | Meaning |
|---|---|
_eq, _neq | Equal / not equal |
_gt, _gte, _lt, _lte | Ordering comparisons |
_in, _nin | Value is / is not in a list |
_is_null | true matches NULL, false matches non-NULL |
_like, _nlike | Case-sensitive pattern match, % wildcard |
_ilike, _nilike | Case-insensitive pattern match |
_regex, _iregex, _nregex, _niregex | Regular expression match |
_similar, _nsimilar | SQL SIMILAR TO |
Text operators are available on string columns only. uuid, timestamp, and numeric columns support the equality, ordering, list, and null operators.
Boolean combinators
graphql
query {
links(
where: {
artist_id: { _eq: $artist_id }
_or: [
{ type: { _eq: "release" } }
{ type: { _eq: "playlist" } }
]
_not: { path: { _ilike: "test-%" } }
}
) {
id
path
}
}_and, _or, and _not each nest a full boolean expression.
Filtering across relationships
Relationship names can be used inside where, which is how you filter a table by a property of a related row.
graphql
# Links whose artist has a specific Spotify ID
query {
links(where: { artist: { spotify_id: { _eq: "1uNFoZAHBGtllmzznpCI3s" } } }) {
id
path
}
}graphql
# Links that have at least one enabled Apple Music destination
query {
links(
where: {
destinations: { type: { _eq: "applemusic" }, enabled: { _eq: true } }
}
) {
id
path
}
}For an array relationship the condition means "at least one related row matches".
Sorting
graphql
query {
links(
where: { artist_id: { _eq: $artist_id } }
order_by: [{ views_total: desc }, { created_at: desc }]
) {
id
title
views_total
}
}Direction values: asc, desc, asc_nulls_first, asc_nulls_last, desc_nulls_first, desc_nulls_last.
Ordering by a related column works too:
graphql
order_by: { artist: { name: asc } }Pagination
graphql
query Page($artist_id: uuid!, $limit: Int!, $offset: Int!) {
links(
where: { artist_id: { _eq: $artist_id } }
order_by: { created_at: desc }
limit: $limit
offset: $offset
) {
id
path
}
links_aggregate(where: { artist_id: { _eq: $artist_id } }) {
aggregate { count }
}
}Always pair offset with a deterministic order_by; without one, row order is not stable between pages and you will see duplicates and gaps.
For large exports, keyset pagination is cheaper and safe against inserts mid-walk:
graphql
query After($artist_id: uuid!, $after: timestamptz!) {
links(
where: {
artist_id: { _eq: $artist_id }
created_at: { _gt: $after }
}
order_by: { created_at: asc }
limit: 500
) {
id
created_at
}
}Aggregates
graphql
query {
links_aggregate(where: { artist_id: { _eq: $artist_id } }) {
aggregate {
count
sum { views_total clickthroughs_total }
avg { views_total }
max { created_at }
min { created_at }
}
}
}count is available on every table; sum, avg, max, min, stddev, and variance apply to numeric columns.
Aggregates can be nested inside a parent row, which is the efficient way to get per-artist counts in one pass:
graphql
query {
artists {
id
name
links_aggregate {
aggregate { count }
}
}
}Nesting relationships
A single document can walk the whole graph. Nested array relationships accept the same where / order_by / limit arguments as top-level fields.
graphql
query FullLink($id: uuid!) {
links_by_pk(id: $id) {
id
domain
path
title
artist {
id
name
socials { type url }
}
destinations(
where: { enabled: { _eq: true } }
order_by: { priority: asc }
) {
id
type
url
cta
}
tracks(order_by: { priority: asc }) {
id
name
url
}
}
}Prefer one nested document over several flat requests — it is a single round trip and a single authorization pass.
Mutations
Table mutations follow the same naming pattern.
| Mutation | Purpose |
|---|---|
insert_<table>(objects: [...], on_conflict: ...) | Insert many rows. |
insert_<table>_one(object: {...}) | Insert one row, returning the row itself. |
update_<table>(where: ..., _set: {...}) | Update matching rows. |
update_<table>_by_pk(pk_columns: { id: ... }, _set: {...}) | Update one row by primary key. |
delete_<table>(where: ...) | Delete matching rows. |
delete_<table>_by_pk(id: ...) | Delete one row by primary key. |
Bulk forms return an affected_rows count plus an optional returning array:
graphql
mutation {
update_link_destinations(
where: { link_id: { _eq: $link_id }, type: { _eq: "spotify" } }
_set: { enabled: false }
) {
affected_rows
returning { id type enabled }
}
}_by_pk forms return the row directly, or null if no row matched — which, under row-level permissions, also covers "you do not own it".
INFO
Writes are constrained twice: by a row rule (you may only touch rows belonging to your artists) and by a column allowlist (only certain columns are settable). Writing a disallowed column fails with permission-error. The allowlist for each table is documented on the relevant page — see Edit Link and Destinations.
Variables
Use variables rather than string interpolation. They are type-checked, they avoid quoting bugs, and they let the server cache the query plan.
json
{
"query": "query GetLink($id: uuid!) { links_by_pk(id: $id) { id path } }",
"variables": { "id": "3aad8009-a307-4429-a586-8b3dbe39cdda" }
}Note the type is uuid! on table fields. Custom resolvers usually declare the same value as String! — check the signature on the page for that field.