Hasura – Relationships and Nested Queries

July 6, 20264 min readUpdated 8/21/2026

Relationships are what make a Hasura API worth having. Without them you have a slightly convenient way to select rows; with them, one round trip returns a listing, its host, its photos and its amenities, correctly joined, with permissions applied at every level.

Two kinds

Object relationships point at one row — a property has one host. Array relationships point at many — a property has many bookings. The distinction is which side of the foreign key you are standing on.

From the demo app’s live metadata:

{
  "table": { "name": "properties" },
  "object_relationships": [ { "name": "host" } ],
  "array_relationships": [ { "name": "bookings" }, { "name": "images" },
                           { "name": "propertyAmenities" } ]
}

Which produces exactly the query you would hope for:

query {
  properties(orderBy: {ratingAverage: DESC}, limit: 2) {
    title
    city
    ratingAverage
    host { firstName isHost }
    images(limit: 1) { url }
  }
}
{"data":{"properties":[
  {"title":"Oceanfront Villa, Private Steps to Sand","city":"Maui","ratingAverage":4.98,
   "host":{"firstName":"Marcus","isHost":true},
   "images":[{"url":"https://images.unsplash.com/photo-1613490493576-..."}]},
  {"title":"Lakefront A-Frame","city":"Lake Tahoe","ratingAverage":4.96,
   "host":{"firstName":"Priya","isHost":true},
   "images":[{"url":"https://images.unsplash.com/photo-1449844908441-..."}]}]}}

Foreign keys suggest, they do not decide

When you track a table with foreign keys, Hasura offers the relationships it can infer and names them after the tables. You can rename them, and you should — host reads better than user even though the column is host_id pointing at users.

You can also create a relationship where no foreign key exists, by telling Hasura which columns to join on. Useful across schemas or against legacy tables that never had constraints. The cost is that nothing enforces referential integrity — you have described a join, not a guarantee.

Relationships work inside where, in both directions. Listings whose host is a verified host:

query {
  properties(where: {host: {isHost: {_eq: true}}}) { title }
}

And through an array relationship, where the semantics matter. _some, _every and _none ask different questions:

properties(where: {bookings: {status: {_eq: "CONFIRMED"}}})          # at least one
properties(where: {bookings: {_none: {status: {_eq: "CANCELLED"}}}}) # not a single one

The plain form means “at least one”. If you meant “every booking is confirmed” you must say _every, and the difference is easy to ship by accident.

Is this n+1?

No, and you can prove it. Hasura has an explain endpoint that shows the SQL it generates:

curl -s -X POST http://localhost:8081/v1/graphql/explain \
  -H 'x-hasura-admin-secret: stayhub-admin-secret' \
  -d '{"query":{"query":"query { properties(limit:2){ title host { firstName } } }"}}'

The nested host comes back as a lateral join inside one statement:

SELECT coalesce(json_agg("root"), '[]') AS "root"
FROM (
  SELECT row_to_json(...) AS "root"
  FROM (SELECT * FROM "public"."properties" WHERE ('true') LIMIT 2) AS "_root.base"
  LEFT OUTER JOIN LATERAL (
    SELECT row_to_json(...) AS "host"
    FROM (SELECT * FROM "public"."users"
          WHERE (("_root.base"."host_id") = ("id")) LIMIT 1) AS "_root.or.host.base"
  ) AS "_root.or.host" ON ('true')
) AS "_root"

And the plan Postgres chose for it:

Aggregate  (cost=5.19..5.20 rows=1 width=32)
  ->  Nested Loop Left Join  (cost=0.01..5.16 rows=2 width=63)
        ->  Limit  (cost=0.00..0.45 rows=2 width=1766)
              ->  Seq Scan on properties  (cost=0.00..3.14 rows=14 width=1766)
        ->  Memoize  (cost=0.01..3.11 rows=1 width=32)
              Cache Key: properties.host_id

One query, one round trip, and the JSON assembled in the database. This is the single strongest argument for Hasura over a hand-written resolver layer, where n+1 is the default failure and avoiding it is work.

It is not a licence to nest without thought. Depth multiplies rows, and a three-level nest across large tables is still a large query — see lesson 14.

Permissions do not cascade

This one has teeth. Being allowed to read properties does not grant you the related host row. Each table’s permissions are evaluated on their own, so a role that can see listings but has no select permission on users gets no host field at all.

That is the safe default — access is never granted implicitly by a join — and it is also why a nested field mysteriously vanishes for one role. The fix is a permission on the child table, not on the relationship.

Relationships in v3 (DDN)

On the v3 sections. Everything marked v3 (DDN) is taken from the official Hasura DDN documentation as read on 2026-08-21 and was not run locally — it shows configuration, never claimed output. The v2 material was executed against a running engine.

v3 makes a relationship an explicit metadata object rather than an entry on a table:

kind: Relationship
version: v1
definition:
  name: host
  sourceType: Properties
  target:
    model:
      name: Users
      relationshipType: Object
  mapping:
    - source:
        fieldPath:
          - fieldName: hostId
      target:
        modelField:
          - fieldName: id

The capability gained is the interesting part. A v2 relationship joins two tables in the same database. A v3 Relationship can span different connectors and different subgraphs — a Postgres listing related to a row in another team’s service, or to a result from a REST API, resolved as one field. That is the supergraph idea made concrete, and it is the thing v2 relationships genuinely could not do.

Next

Lesson 6 turns to writes, and the question of which ones belong in Hasura at all.