Repository: authzed/spicedb

Issue:
`x-request-id` isn't being propagated from the request to the response
Unless I'm misunderstanding this feature, If i do:

```
 go run ./cmd/spicedb/main.go serve --http-enabled true --grpc-preshared-key "somerandomkeyhere"
```

and then 

```
curl --location 'http://localhost:8443/v1/schema/write' \
    --header 'Content-Type: application/json' \
    --header 'Accept: application/json' \
    --header 'x-request-id: mysuperduperrequestid' \
    --header 'Authorization: Bearer somerandomkeyhere' \
    --data '{
        "schema": "definition user {} \n definition folder { \n relation parent: folder\n relation viewer: user \n permission view = viewer + parent->view \n } \n definition document {\n relation folder: folder \n relation viewer: user \n permission view = viewer + folder->view \n }"
    }' -D -
```

shows

```
HTTP/1.1 200 OK
Content-Type: application/json
Grpc-Metadata-Content-Type: application/grpc
Date: Fri, 17 Oct 2025 23:52:14 GMT
Content-Length: 70

{"writtenAt":{"token":"Gh8KEzE3NjA3NDUxMzQ1MTg1MzYwMDASCDU0NjQ2MDM5"}}%
```

those headers are lacking the 'x-request-id: mysuperduperrequestid'  that I sent.

To debug further, I added a log line after this one https://github.com/authzed/spicedb/blob/cefe49ab1ee65af262cc84a94506e6ce365b6669/pkg/middleware/requestid/requestid.go#L99 and it shows:

```
5:03PM WRN requestid: extracted metadata: map[:authority:[localhost:50051] authorization:[Bearer somerandomkeyhere] content-type:[application/grpc] grpc-accept-encoding:[s2,gzip] grpcgateway-accept:[application/json] grpcgateway-authorization:[Bearer somerandomkeyhere] grpcgateway-content-type:[application/json] grpcgateway-user-agent:[curl/8.7.1] user-agent:[grpc-go/1.75.1] x-forwarded-for:[::1] x-forwarded-host:[localhost:8443]]
```

Hints from issue comments:
@Verolop: see: https://github.com/authzed/authzed-go/issues/361 & https://github.com/authzed/authzed-go/pull/362@miparnisari: We need to update the http->grpc proxy so that the header gets included in the response.

Solution patch:
diff --git a/Dockerfile b/Dockerfile
index c2d11ead..57f331d3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -11,7 +11,7 @@ WORKDIR /go/src/app
 RUN apk update && apk add --no-cache git
 RUN git clone https://github.com/authzed/grpc-health-probe.git
 WORKDIR /go/src/app/grpc-health-probe
-RUN git checkout master
+RUN git checkout main
 RUN CGO_ENABLED=0 go install -a -tags netgo -ldflags=-w
 
 # use `crane digest <image>` to get the multi-platform sha256
diff --git a/Dockerfile.release b/Dockerfile.release
index 2006b6f4..21bb0dc1 100644
--- a/Dockerfile.release
+++ b/Dockerfile.release
@@ -6,7 +6,7 @@ WORKDIR /go/src/app
 RUN apk update && apk add --no-cache git
 RUN git clone https://github.com/authzed/grpc-health-probe.git
 WORKDIR /go/src/app/grpc-health-probe
-RUN git checkout master
+RUN git checkout main
 RUN CGO_ENABLED=0 go install -a -tags netgo -ldflags=-w
 
 FROM $BASE
diff --git a/e2e/go.mod b/e2e/go.mod
index bf415a4b..14a8ff9a 100644
--- a/e2e/go.mod
+++ b/e2e/go.mod
@@ -39,7 +39,7 @@ require (
 	github.com/aws/aws-sdk-go-v2/service/sts v1.39.0 // indirect
 	github.com/aws/smithy-go v1.23.1 // indirect
 	github.com/benbjohnson/clock v1.3.5 // indirect
-	github.com/ccoveille/go-safecast v1.8.1 // indirect
+	github.com/ccoveille/go-safecast/v2 v2.0.0 // indirect
 	github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d // indirect
 	github.com/creasty/defaults v1.8.0 // indirect
 	github.com/dave/jennifer v1.7.1 // indirect
@@ -70,13 +70,13 @@ require (
 	go.opentelemetry.io/otel v1.38.0 // indirect
 	go.opentelemetry.io/otel/metric v1.38.0 // indirect
 	go.opentelemetry.io/otel/trace v1.38.0 // indirect
-	golang.org/x/crypto v0.43.0 // indirect
+	golang.org/x/crypto v0.45.0 // indirect
 	golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect
 	golang.org/x/mod v0.29.0 // indirect
-	golang.org/x/net v0.46.0 // indirect
-	golang.org/x/sync v0.17.0 // indirect
-	golang.org/x/sys v0.37.0 // indirect
-	golang.org/x/text v0.30.0 // indirect
+	golang.org/x/net v0.47.0 // indirect
+	golang.org/x/sync v0.18.0 // indirect
+	golang.org/x/sys v0.38.0 // indirect
+	golang.org/x/text v0.31.0 // indirect
 	google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
 	google.golang.org/protobuf v1.36.10 // indirect
diff --git a/e2e/go.sum b/e2e/go.sum
index d824a96c..ef751ad6 100644
--- a/e2e/go.sum
+++ b/e2e/go.sum
@@ -1425,8 +1425,8 @@ github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl
 github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
 github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4=
 github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=
-github.com/ccoveille/go-safecast v1.8.1 h1:RoucjfYKKcx2lFmIjRjuo8AeX9k/GaZn5SUMHlA3kMw=
-github.com/ccoveille/go-safecast v1.8.1/go.mod h1:QqwNjxQ7DAqY0C721OIO9InMk9zCwcsO7tnRuHytad8=
+github.com/ccoveille/go-safecast/v2 v2.0.0 h1:+5eyITXAUj3wMjad6cRVJKGnC7vDS55zk0INzJagub0=
+github.com/ccoveille/go-safecast/v2 v2.0.0/go.mod h1:JIYA4CAR33blIDuE6fSwCp2sz1oOBahXnvmdBhOAABs=
 github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -1821,6 +1821,8 @@ github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzM
 github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
 github.com/prometheus/statsd_exporter v0.22.7 h1:7Pji/i2GuhK6Lu7DHrtTkFmNBCudCPT1pX2CziuyQR0=
 github.com/prometheus/statsd_exporter v0.22.7/go.mod h1:N/TevpjkIh9ccs6nuzY3jQn9dFqnUakOjnEuMPJJJnI=
+github.com/puzpuzpuz/xsync/v4 v4.2.0 h1:dlxm77dZj2c3rxq0/XNvvUKISAmovoXF4a4qM6Wvkr0=
+github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
 github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
@@ -1979,8 +1981,8 @@ golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOM
 golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
 golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
 golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
-golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
-golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
+golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
+golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
 golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -2122,8 +2124,8 @@ golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
 golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
 golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
 golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
-golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
-golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
+golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
+golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -2188,8 +2190,8 @@ golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
-golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
+golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -2287,8 +2289,8 @@ golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
-golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
+golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
 golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -2312,8 +2314,8 @@ golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
 golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
 golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
 golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
-golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
-golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
+golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
+golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -2338,8 +2340,8 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
 golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
-golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
-golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
+golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
+golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
diff --git a/go.mod b/go.mod
index a0fde7d8..811d9ba6 100644
--- a/go.mod
+++ b/go.mod
@@ -42,7 +42,7 @@ require (
 	github.com/benbjohnson/clock v1.3.5
 	github.com/bits-and-blooms/bloom/v3 v3.7.1
 	github.com/caio/go-tdigest/v4 v4.1.0
-	github.com/ccoveille/go-safecast v1.8.1
+	github.com/ccoveille/go-safecast/v2 v2.0.0
 	github.com/cenkalti/backoff/v4 v4.3.0
 	github.com/cespare/xxhash/v2 v2.3.0
 	github.com/cloudspannerecosystem/spanner-change-streams-tail v0.3.1
@@ -112,9 +112,10 @@ require (
 	go.opentelemetry.io/otel/trace v1.38.0
 	go.uber.org/atomic v1.11.0
 	go.uber.org/goleak v1.3.0
+	go.uber.org/mock v0.6.0
 	golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b
 	golang.org/x/mod v0.29.0
-	golang.org/x/sync v0.17.0
+	golang.org/x/sync v0.18.0
 	golang.org/x/time v0.14.0
 	google.golang.org/api v0.254.0
 	google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4
@@ -431,19 +432,18 @@ require (
 	go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
 	go.opentelemetry.io/proto/otlp v1.5.0 // indirect
 	go.uber.org/automaxprocs v1.6.0 // indirect
-	go.uber.org/mock v0.6.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	go.uber.org/zap v1.27.0 // indirect
 	go.yaml.in/yaml/v2 v2.4.2 // indirect
 	go.yaml.in/yaml/v3 v3.0.4 // indirect
-	golang.org/x/crypto v0.43.0 // indirect
+	golang.org/x/crypto v0.45.0 // indirect
 	golang.org/x/exp/typeparams v0.0.0-20250911091902-df9299821621 // indirect
-	golang.org/x/net v0.46.0 // indirect
+	golang.org/x/net v0.47.0 // indirect
 	golang.org/x/oauth2 v0.32.0 // indirect
-	golang.org/x/sys v0.37.0 // indirect
+	golang.org/x/sys v0.38.0 // indirect
 	golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 // indirect
-	golang.org/x/term v0.36.0 // indirect
-	golang.org/x/text v0.30.0 // indirect
+	golang.org/x/term v0.37.0 // indirect
+	golang.org/x/text v0.31.0 // indirect
 	golang.org/x/tools v0.38.0 // indirect
 	golang.org/x/vuln v1.1.4 // indirect
 	google.golang.org/genproto v0.0.0-20250908214217-97024824d090 // indirect
diff --git a/go.sum b/go.sum
index 6b89255d..60849b40 100644
--- a/go.sum
+++ b/go.sum
@@ -1530,8 +1530,8 @@ github.com/catenacyber/perfsprint v0.9.1 h1:5LlTp4RwTooQjJCvGEFV6XksZvWE7wCOUvjD
 github.com/catenacyber/perfsprint v0.9.1/go.mod h1:q//VWC2fWbcdSLEY1R3l8n0zQCDPdE4IjZwyY1HMunM=
 github.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNrUcc=
 github.com/ccojocar/zxcvbn-go v1.0.4/go.mod h1:3GxGX+rHmueTUMvm5ium7irpyjmm7ikxYFOSJB21Das=
-github.com/ccoveille/go-safecast v1.8.1 h1:RoucjfYKKcx2lFmIjRjuo8AeX9k/GaZn5SUMHlA3kMw=
-github.com/ccoveille/go-safecast v1.8.1/go.mod h1:QqwNjxQ7DAqY0C721OIO9InMk9zCwcsO7tnRuHytad8=
+github.com/ccoveille/go-safecast/v2 v2.0.0 h1:+5eyITXAUj3wMjad6cRVJKGnC7vDS55zk0INzJagub0=
+github.com/ccoveille/go-safecast/v2 v2.0.0/go.mod h1:JIYA4CAR33blIDuE6fSwCp2sz1oOBahXnvmdBhOAABs=
 github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -2637,8 +2637,8 @@ golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOM
 golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
 golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
 golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
-golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
-golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
+golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
+golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
 golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -2788,8 +2788,8 @@ golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
 golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
 golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
 golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
-golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
-golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
+golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
+golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -2854,8 +2854,8 @@ golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
-golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
+golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -2966,8 +2966,8 @@ golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
-golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
+golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
 golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
 golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU=
 golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE=
@@ -2993,8 +2993,8 @@ golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
 golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
 golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
 golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
-golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
-golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
+golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
+golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -3019,8 +3019,8 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
 golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
-golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
-golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
+golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
+golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
diff --git a/internal/datastore/common/changes.go b/internal/datastore/common/changes.go
index bcb47157..38b7206d 100644
--- a/internal/datastore/common/changes.go
+++ b/internal/datastore/common/changes.go
@@ -7,7 +7,7 @@ import (
 	"slices"
 	"sort"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"google.golang.org/protobuf/types/known/structpb"
 
 	log "github.com/authzed/spicedb/internal/logging"
diff --git a/internal/datastore/crdb/crdb.go b/internal/datastore/crdb/crdb.go
index 4b69051b..7ad0dae6 100644
--- a/internal/datastore/crdb/crdb.go
+++ b/internal/datastore/crdb/crdb.go
@@ -12,7 +12,7 @@ import (
 
 	"github.com/IBM/pgxpoolprometheus"
 	sq "github.com/Masterminds/squirrel"
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/jackc/pgx/v5"
 	"github.com/jackc/pgx/v5/pgconn"
 	"github.com/jackc/pgx/v5/pgxpool"
diff --git a/internal/datastore/crdb/pool/balancer.go b/internal/datastore/crdb/pool/balancer.go
index f1e1c1e9..38f428ba 100644
--- a/internal/datastore/crdb/pool/balancer.go
+++ b/internal/datastore/crdb/pool/balancer.go
@@ -10,7 +10,7 @@ import (
 	"strconv"
 	"time"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/jackc/pgx/v5"
 	"github.com/jackc/pgx/v5/pgxpool"
 	"github.com/prometheus/client_golang/prometheus"
@@ -87,13 +87,17 @@ type nodeConnectionBalancer[P balancePoolConn[C], C balanceConn] struct {
 // testing purposes. Callers should use the exported NewNodeConnectionBalancer.
 func newNodeConnectionBalancer[P balancePoolConn[C], C balanceConn](pool balanceablePool[P, C], healthTracker *NodeHealthTracker, interval time.Duration) *nodeConnectionBalancer[P, C] {
 	seed := int64(0)
+	var err error
 	for seed == 0 {
-		// Sum64 returns a uint64, and safecast will return 0 if it's not castable,
+		// Sum64 returns a uint64, and safecast will return an error if it's not castable,
 		// which will happen about half the time (?). We just keep running it until
 		// we get a seed that fits in the box.
 		// Subtracting math.MaxInt64 should mean that we retain the entire range of
 		// possible values.
-		seed, _ = safecast.Convert[int64](new(maphash.Hash).Sum64() - math.MaxInt64)
+		seed, err = safecast.Convert[int64](new(maphash.Hash).Sum64() - math.MaxInt64)
+		if err != nil {
+			seed = 0
+		}
 	}
 	return &nodeConnectionBalancer[P, C]{
 		ticker:        time.NewTicker(interval),
@@ -159,8 +163,8 @@ func (p *nodeConnectionBalancer[P, C]) mustPruneConnections(ctx context.Context)
 
 	// It's highly unlikely that we'll ever have an overflow in
 	// this context, so we cast directly.
-	nodeCount, _ := safecast.Convert[uint32](p.healthTracker.HealthyNodeCount())
-	if nodeCount == 0 {
+	nodeCount, err := safecast.Convert[uint32](p.healthTracker.HealthyNodeCount())
+	if err != nil || nodeCount == 0 {
 		nodeCount = 1
 	}
 
diff --git a/internal/datastore/crdb/pool/pool.go b/internal/datastore/crdb/pool/pool.go
index 1e52add9..fd5f7944 100644
--- a/internal/datastore/crdb/pool/pool.go
+++ b/internal/datastore/crdb/pool/pool.go
@@ -8,7 +8,7 @@ import (
 	"sync"
 	"time"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/jackc/pgx/v5"
 	"github.com/jackc/pgx/v5/pgconn"
 	"github.com/jackc/pgx/v5/pgxpool"
@@ -156,14 +156,20 @@ func (p *RetryPool) ID() string {
 // MaxConns returns the MaxConns configured on the underlying pool
 func (p *RetryPool) MaxConns() uint32 {
 	// This should be non-negative
-	maxConns, _ := safecast.Convert[uint32](p.pool.Config().MaxConns)
+	maxConns, err := safecast.Convert[uint32](p.pool.Config().MaxConns)
+	if err != nil {
+		maxConns = 0
+	}
 	return maxConns
 }
 
 // MinConns returns the MinConns configured on the underlying pool
 func (p *RetryPool) MinConns() uint32 {
 	// This should be non-negative
-	minConns, _ := safecast.Convert[uint32](p.pool.Config().MinConns)
+	minConns, err := safecast.Convert[uint32](p.pool.Config().MinConns)
+	if err != nil {
+		minConns = 0
+	}
 	return minConns
 }
 
diff --git a/internal/datastore/crdb/readwrite.go b/internal/datastore/crdb/readwrite.go
index 916ca34d..e2595ed4 100644
--- a/internal/datastore/crdb/readwrite.go
+++ b/internal/datastore/crdb/readwrite.go
@@ -7,7 +7,7 @@ import (
 	"fmt"
 
 	sq "github.com/Masterminds/squirrel"
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/jackc/pgx/v5"
 
 	v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
diff --git a/internal/datastore/crdb/stats.go b/internal/datastore/crdb/stats.go
index 9d58e7db..6ec65c00 100644
--- a/internal/datastore/crdb/stats.go
+++ b/internal/datastore/crdb/stats.go
@@ -6,7 +6,7 @@ import (
 	"slices"
 
 	"github.com/Masterminds/squirrel"
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/jackc/pgx/v5"
 	"github.com/rs/zerolog/log"
 
diff --git a/internal/datastore/mysql/gc.go b/internal/datastore/mysql/gc.go
index 7b5000b0..1b709bb6 100644
--- a/internal/datastore/mysql/gc.go
+++ b/internal/datastore/mysql/gc.go
@@ -6,7 +6,7 @@ import (
 	"time"
 
 	sq "github.com/Masterminds/squirrel"
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 
 	"github.com/authzed/spicedb/internal/datastore/common"
 	"github.com/authzed/spicedb/internal/datastore/revisions"
diff --git a/internal/datastore/mysql/readwrite.go b/internal/datastore/mysql/readwrite.go
index 93d410f5..f36f600b 100644
--- a/internal/datastore/mysql/readwrite.go
+++ b/internal/datastore/mysql/readwrite.go
@@ -14,7 +14,7 @@ import (
 	"time"
 
 	sq "github.com/Masterminds/squirrel"
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/go-sql-driver/mysql"
 
 	v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
diff --git a/internal/datastore/mysql/revisions.go b/internal/datastore/mysql/revisions.go
index 16f4b2e9..7d3395b0 100644
--- a/internal/datastore/mysql/revisions.go
+++ b/internal/datastore/mysql/revisions.go
@@ -7,7 +7,7 @@ import (
 	"fmt"
 	"time"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"go.opentelemetry.io/otel/attribute"
 	"go.opentelemetry.io/otel/trace"
 
diff --git a/internal/datastore/mysql/stats.go b/internal/datastore/mysql/stats.go
index 48c32b89..7beac266 100644
--- a/internal/datastore/mysql/stats.go
+++ b/internal/datastore/mysql/stats.go
@@ -6,7 +6,7 @@ import (
 	"fmt"
 
 	"github.com/Masterminds/squirrel"
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 
 	"github.com/authzed/spicedb/internal/datastore/common"
 	"github.com/authzed/spicedb/pkg/datastore"
diff --git a/internal/datastore/postgres/common/bulk.go b/internal/datastore/postgres/common/bulk.go
index 5cec311d..0fbbf7bb 100644
--- a/internal/datastore/postgres/common/bulk.go
+++ b/internal/datastore/postgres/common/bulk.go
@@ -3,7 +3,7 @@ package common
 import (
 	"context"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/jackc/pgx/v5"
 
 	"github.com/authzed/spicedb/pkg/datastore"
diff --git a/internal/datastore/postgres/common/pgx.go b/internal/datastore/postgres/common/pgx.go
index cb843018..46b80a7b 100644
--- a/internal/datastore/postgres/common/pgx.go
+++ b/internal/datastore/postgres/common/pgx.go
@@ -6,7 +6,7 @@ import (
 	"strings"
 	"time"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/exaring/otelpgx"
 	"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/retry"
 	zerologadapter "github.com/jackc/pgx-zerolog"
diff --git a/internal/datastore/postgres/readwrite.go b/internal/datastore/postgres/readwrite.go
index 0d224937..26cde323 100644
--- a/internal/datastore/postgres/readwrite.go
+++ b/internal/datastore/postgres/readwrite.go
@@ -7,7 +7,7 @@ import (
 	"fmt"
 
 	sq "github.com/Masterminds/squirrel"
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/jackc/pgx/v5"
 
 	v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
diff --git a/internal/datastore/postgres/revisions.go b/internal/datastore/postgres/revisions.go
index 043c2938..1d8780bb 100644
--- a/internal/datastore/postgres/revisions.go
+++ b/internal/datastore/postgres/revisions.go
@@ -9,7 +9,7 @@ import (
 	"strings"
 	"time"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/jackc/pgx/v5"
 
 	"github.com/authzed/spicedb/internal/datastore/postgres/common"
diff --git a/internal/datastore/postgres/snapshot.go b/internal/datastore/postgres/snapshot.go
index 5a08a1b7..1387aef9 100644
--- a/internal/datastore/postgres/snapshot.go
+++ b/internal/datastore/postgres/snapshot.go
@@ -7,7 +7,7 @@ import (
 	"strconv"
 	"strings"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/jackc/pgx/v5/pgtype"
 )
 
@@ -279,7 +279,11 @@ func (s pgSnapshot) markInProgress(txid uint64) pgSnapshot {
 	startingXipLen := len(newSnapshot.xipList)
 	for numToDrop = 0; numToDrop < startingXipLen; numToDrop++ {
 		// numToDrop should be nonnegative
-		uintNumToDrop, _ := safecast.Convert[uint64](numToDrop)
+		uintNumToDrop, err := safecast.Convert[uint64](numToDrop)
+		if err != nil {
+			uintNumToDrop = 0
+		}
+
 		if newSnapshot.xipList[startingXipLen-1-numToDrop] != newSnapshot.xmax-uintNumToDrop-1 {
 			break
 		}
diff --git a/internal/datastore/postgres/watch.go b/internal/datastore/postgres/watch.go
index 4a157fe6..32d8cbad 100644
--- a/internal/datastore/postgres/watch.go
+++ b/internal/datastore/postgres/watch.go
@@ -7,7 +7,7 @@ import (
 	"time"
 
 	sq "github.com/Masterminds/squirrel"
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/jackc/pgx/v5"
 	"google.golang.org/protobuf/types/known/structpb"
 
diff --git a/internal/datastore/revisions/hlcrevision.go b/internal/datastore/revisions/hlcrevision.go
index e0387b67..8699450c 100644
--- a/internal/datastore/revisions/hlcrevision.go
+++ b/internal/datastore/revisions/hlcrevision.go
@@ -7,7 +7,7 @@ import (
 	"strings"
 	"time"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/shopspring/decimal"
 
 	"github.com/authzed/spicedb/pkg/datastore"
diff --git a/internal/datastore/spanner/readwrite.go b/internal/datastore/spanner/readwrite.go
index 9a565e77..72e07826 100644
--- a/internal/datastore/spanner/readwrite.go
+++ b/internal/datastore/spanner/readwrite.go
@@ -7,7 +7,7 @@ import (
 
 	"cloud.google.com/go/spanner"
 	sq "github.com/Masterminds/squirrel"
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 
 	v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
 
diff --git a/internal/datastore/spanner/stats.go b/internal/datastore/spanner/stats.go
index c6859d3e..d05e0505 100644
--- a/internal/datastore/spanner/stats.go
+++ b/internal/datastore/spanner/stats.go
@@ -6,7 +6,7 @@ import (
 	"strings"
 
 	"cloud.google.com/go/spanner"
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"go.opentelemetry.io/otel/trace"
 
 	"github.com/authzed/spicedb/pkg/datastore"
diff --git a/internal/developmentmembership/onrset.go b/internal/developmentmembership/onrset.go
index c7b53f70..ba53fe0a 100644
--- a/internal/developmentmembership/onrset.go
+++ b/internal/developmentmembership/onrset.go
@@ -1,7 +1,7 @@
 package developmentmembership
 
 import (
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 
 	"github.com/authzed/spicedb/pkg/genutil/mapz"
 	"github.com/authzed/spicedb/pkg/tuple"
@@ -26,7 +26,10 @@ func NewONRSet(onrs ...tuple.ObjectAndRelation) ONRSet {
 // Length returns the size of the set.
 func (ons ONRSet) Length() uint64 {
 	// This is the length of a set so we should never fall out of bounds.
-	length, _ := safecast.Convert[uint64](ons.onrs.Len())
+	length, err := safecast.Convert[uint64](ons.onrs.Len())
+	if err != nil {
+		return 0
+	}
 	return length
 }
 
diff --git a/internal/gateway/gateway.go b/internal/gateway/gateway.go
index 5f54f61d..c3f3640e 100644
--- a/internal/gateway/gateway.go
+++ b/internal/gateway/gateway.go
@@ -5,6 +5,7 @@ import (
 	"errors"
 	"io"
 	"net/http"
+	"strings"
 
 	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
 	"github.com/prometheus/client_golang/prometheus"
@@ -20,6 +21,7 @@ import (
 	"google.golang.org/grpc/credentials/insecure"
 	healthpb "google.golang.org/grpc/health/grpc_health_v1"
 	"google.golang.org/grpc/metadata"
+	protobuf "google.golang.org/protobuf/proto"
 
 	"github.com/authzed/authzed-go/proto"
 	v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
@@ -65,7 +67,12 @@ func NewHandler(ctx context.Context, upstreamAddr, upstreamTLSCertPath string) (
 		return nil, err
 	}
 
-	gwMux := runtime.NewServeMux(runtime.WithMetadata(OtelAnnotator), runtime.WithHealthzEndpoint(healthpb.NewHealthClient(healthConn)))
+	gwMux := runtime.NewServeMux(
+		runtime.WithMetadata(OtelAnnotator),
+		runtime.WithIncomingHeaderMatcher(customIncomingHeaderMatcher),
+		runtime.WithForwardResponseOption(forwardRequestIDTrailer),
+		runtime.WithHealthzEndpoint(healthpb.NewHealthClient(healthConn)),
+	)
 	schemaConn, err := registerHandler(ctx, gwMux, upstreamAddr, opts, v1.RegisterSchemaServiceHandler)
 	if err != nil {
 		return nil, err
@@ -170,3 +177,37 @@ func OtelAnnotator(ctx context.Context, r *http.Request) metadata.MD {
 	otelgrpc.Inject(ctx, &metadataCopy, defaultOtelOpts...) // nolint:staticcheck
 	return metadataCopy
 }
+
+// forwardRequestIDTrailer copies the request ID from gRPC trailers to HTTP response headers.
+// This ensures x-request-id is visible to HTTP clients while maintaining gRPC retry policy compliance.
+func forwardRequestIDTrailer(ctx context.Context, w http.ResponseWriter, _ protobuf.Message) error {
+	md, ok := runtime.ServerMetadataFromContext(ctx)
+	if !ok {
+		return nil
+	}
+
+	// Check standard x-request-id key
+	if vals := md.TrailerMD.Get("x-request-id"); len(vals) > 0 {
+		w.Header().Set("X-Request-Id", vals[0])
+		return nil
+	}
+
+	// Check legacy key for backward compatibility
+	if vals := md.TrailerMD.Get("io.spicedb.respmeta.requestid"); len(vals) > 0 {
+		w.Header().Set("X-Request-Id", vals[0])
+	}
+
+	return nil
+}
+
+// customIncomingHeaderMatcher translates HTTP headers to gRPC metadata.
+// This ensures x-request-id from HTTP clients is passed through without the
+// grpcgateway- prefix, making it available to the request ID middleware.
+func customIncomingHeaderMatcher(key string) (string, bool) {
+	switch strings.ToLower(key) {
+	case "x-request-id":
+		return "x-request-id", true
+	default:
+		return runtime.DefaultHeaderMatcher(key)
+	}
+}
diff --git a/internal/graph/cursors.go b/internal/graph/cursors.go
index b3760d2b..70ad9f32 100644
--- a/internal/graph/cursors.go
+++ b/internal/graph/cursors.go
@@ -6,7 +6,7 @@ import (
 	"strconv"
 	"sync"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 
 	"github.com/authzed/spicedb/internal/dispatch"
 	"github.com/authzed/spicedb/internal/taskrunner"
diff --git a/internal/graph/lookupresources3.go b/internal/graph/lookupresources3.go
index 0de4d994..133188c6 100644
--- a/internal/graph/lookupresources3.go
+++ b/internal/graph/lookupresources3.go
@@ -7,7 +7,7 @@ import (
 	"sort"
 	"strings"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/google/uuid"
 	"go.opentelemetry.io/otel/attribute"
 	"go.opentelemetry.io/otel/trace"
diff --git a/internal/middleware/perfinsights/perfinsights.go b/internal/middleware/perfinsights/perfinsights.go
index c8a77a8c..8c48f994 100644
--- a/internal/middleware/perfinsights/perfinsights.go
+++ b/internal/middleware/perfinsights/perfinsights.go
@@ -6,7 +6,7 @@ import (
 	"strconv"
 	"time"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors"
 	"github.com/prometheus/client_golang/prometheus"
 	"github.com/prometheus/client_golang/prometheus/promauto"
diff --git a/internal/services/integrationtesting/testconfigs/arrowsublr.yaml b/internal/services/integrationtesting/testconfigs/arrowsublr.yaml
new file mode 100644
index 00000000..69a050bb
--- /dev/null
+++ b/internal/services/integrationtesting/testconfigs/arrowsublr.yaml
@@ -0,0 +1,31 @@
+---
+schema: |+
+  definition special_user {}
+
+  definition user {
+  	relation special_user_mapping: special_user
+  	permission special_user = special_user_mapping
+  }
+
+  definition group {
+  	relation member: user
+  	permission membership = member + member->special_user
+  }
+
+  definition system {
+  	relation viewer: user | group#membership
+  	permission view = viewer + viewer->special_user
+  }
+
+
+relationships: |-
+  system:somesystem#viewer@group:somegroup#membership
+
+  group:somegroup#member@user:someuser1
+
+  user:someuser1#special_user_mapping@special_user:specialuser
+assertions:
+  assertTrue:
+    - "system:somesystem#view@special_user:specialuser"
+  assertFalse: []
+validation: {}
diff --git a/internal/services/v1/experimental.go b/internal/services/v1/experimental.go
index 3ad86a3d..f05549bd 100644
--- a/internal/services/v1/experimental.go
+++ b/internal/services/v1/experimental.go
@@ -12,7 +12,7 @@ import (
 	"strings"
 	"time"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	grpcvalidate "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/validator"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/codes"
diff --git a/magefiles/go.mod b/magefiles/go.mod
index 8f5afbf0..3f2c7b2e 100644
--- a/magefiles/go.mod
+++ b/magefiles/go.mod
@@ -10,7 +10,7 @@ require (
 	github.com/magefile/mage v1.15.0
 	github.com/planetscale/vtprotobuf v0.6.1-0.20240917153116-6f2963f01587
 	go.uber.org/mock v0.6.0
-	golang.org/x/sync v0.17.0
+	golang.org/x/sync v0.18.0
 	golang.org/x/tools v0.38.0
 	google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1
 	google.golang.org/protobuf v1.36.10
@@ -119,13 +119,13 @@ require (
 	go.opentelemetry.io/otel/trace v1.38.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	go.uber.org/zap v1.27.0 // indirect
-	golang.org/x/crypto v0.43.0 // indirect
+	golang.org/x/crypto v0.45.0 // indirect
 	golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b // indirect
 	golang.org/x/mod v0.29.0 // indirect
-	golang.org/x/net v0.46.0 // indirect
-	golang.org/x/sys v0.37.0 // indirect
-	golang.org/x/term v0.36.0 // indirect
-	golang.org/x/text v0.30.0 // indirect
+	golang.org/x/net v0.47.0 // indirect
+	golang.org/x/sys v0.38.0 // indirect
+	golang.org/x/term v0.37.0 // indirect
+	golang.org/x/text v0.31.0 // indirect
 	golang.org/x/time v0.14.0 // indirect
 	google.golang.org/genproto/googleapis/api v0.0.0-20251007200510-49b9836ed3ff // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
diff --git a/magefiles/go.sum b/magefiles/go.sum
index 17cd3ebf..f6dfa471 100644
--- a/magefiles/go.sum
+++ b/magefiles/go.sum
@@ -274,28 +274,28 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
 go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
 go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
-golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
-golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
+golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
+golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
 golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b h1:18qgiDvlvH7kk8Ioa8Ov+K6xCi0GMvmGfGW0sgd/SYA=
 golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
 golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
 golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
-golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
-golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
-golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
-golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
+golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
+golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
+golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
 golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
-golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
-golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
-golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
-golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
-golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
+golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
+golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
+golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
+golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
+golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
 golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
 golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
 golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go
index c834f823..05500664 100644
--- a/pkg/cache/cache.go
+++ b/pkg/cache/cache.go
@@ -3,7 +3,7 @@ package cache
 import (
 	"time"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/dustin/go-humanize"
 	"github.com/rs/zerolog"
 )
@@ -53,7 +53,10 @@ type Config struct {
 }
 
 func (c *Config) MarshalZerologObject(e *zerolog.Event) {
-	maxCost, _ := safecast.Convert[uint64](c.MaxCost)
+	maxCost, err := safecast.Convert[uint64](c.MaxCost)
+	if err != nil {
+		maxCost = 0
+	}
 	e.
 		Str("maxCost", humanize.IBytes(maxCost)).
 		Int64("numCounters", c.NumCounters).
diff --git a/pkg/cache/cache_otter.go b/pkg/cache/cache_otter.go
index 924da319..84472b96 100644
--- a/pkg/cache/cache_otter.go
+++ b/pkg/cache/cache_otter.go
@@ -6,7 +6,7 @@ import (
 	"math"
 	"sync/atomic"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/maypok86/otter/v2"
 	"github.com/maypok86/otter/v2/stats"
 	"github.com/rs/zerolog"
diff --git a/pkg/cmd/datastore/datastore.go b/pkg/cmd/datastore/datastore.go
index 25d74c6b..f0612180 100644
--- a/pkg/cmd/datastore/datastore.go
+++ b/pkg/cmd/datastore/datastore.go
@@ -9,7 +9,7 @@ import (
 	"strings"
 	"time"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/spf13/pflag"
 
 	"github.com/authzed/spicedb/internal/datastore/crdb"
diff --git a/pkg/cmd/server/cacheconfig.go b/pkg/cmd/server/cacheconfig.go
index 90494f03..c4367a48 100644
--- a/pkg/cmd/server/cacheconfig.go
+++ b/pkg/cmd/server/cacheconfig.go
@@ -8,7 +8,7 @@ import (
 	"strings"
 	"time"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/dustin/go-humanize"
 	"github.com/pbnjay/memory"
 	"github.com/spf13/pflag"
diff --git a/pkg/composableschemadsl/compiler/translator.go b/pkg/composableschemadsl/compiler/translator.go
index 4fc50b3d..a62118ce 100644
--- a/pkg/composableschemadsl/compiler/translator.go
+++ b/pkg/composableschemadsl/compiler/translator.go
@@ -9,7 +9,7 @@ import (
 	"slices"
 	"strings"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/jzelinskie/stringz"
 	"github.com/rs/zerolog/log"
 
@@ -334,9 +334,14 @@ func getSourcePosition(dslNode *dslNode, mapper input.PositionMapper) *core.Sour
 		return nil
 	}
 
-	// We're okay with these being zero if the cast fails.
-	uintLine, _ := safecast.Convert[uint64](line)
-	uintCol, _ := safecast.Convert[uint64](col)
+	uintLine, err := safecast.Convert[uint64](line)
+	if err != nil {
+		uintLine = 0
+	}
+	uintCol, err := safecast.Convert[uint64](col)
+	if err != nil {
+		uintCol = 0
+	}
 
 	return &core.SourcePosition{
 		ZeroIndexedLineNumber:     uintLine,
diff --git a/pkg/development/assertions.go b/pkg/development/assertions.go
index c1145219..b8f6ef63 100644
--- a/pkg/development/assertions.go
+++ b/pkg/development/assertions.go
@@ -3,7 +3,7 @@ package development
 import (
 	"fmt"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 
 	log "github.com/authzed/spicedb/internal/logging"
 	devinterface "github.com/authzed/spicedb/pkg/proto/developer/v1"
diff --git a/pkg/development/devcontext.go b/pkg/development/devcontext.go
index 95134897..8d01a4fc 100644
--- a/pkg/development/devcontext.go
+++ b/pkg/development/devcontext.go
@@ -6,7 +6,7 @@ import (
 	"net"
 	"time"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	humanize "github.com/dustin/go-humanize"
 	"google.golang.org/genproto/googleapis/rpc/errdetails"
 	"google.golang.org/grpc"
diff --git a/pkg/development/parsing.go b/pkg/development/parsing.go
index e6badac6..3c85d782 100644
--- a/pkg/development/parsing.go
+++ b/pkg/development/parsing.go
@@ -1,7 +1,7 @@
 package development
 
 import (
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 
 	log "github.com/authzed/spicedb/internal/logging"
 	devinterface "github.com/authzed/spicedb/pkg/proto/developer/v1"
diff --git a/pkg/development/resolver.go b/pkg/development/resolver.go
index 13e6e923..de43cd91 100644
--- a/pkg/development/resolver.go
+++ b/pkg/development/resolver.go
@@ -5,7 +5,7 @@ import (
 	"fmt"
 	"strings"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 
 	log "github.com/authzed/spicedb/internal/logging"
 	"github.com/authzed/spicedb/pkg/caveats"
diff --git a/pkg/development/schema.go b/pkg/development/schema.go
index c7b97ef1..c94ae828 100644
--- a/pkg/development/schema.go
+++ b/pkg/development/schema.go
@@ -3,7 +3,7 @@ package development
 import (
 	"errors"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 
 	log "github.com/authzed/spicedb/internal/logging"
 	devinterface "github.com/authzed/spicedb/pkg/proto/developer/v1"
diff --git a/pkg/development/validation.go b/pkg/development/validation.go
index b6de72cd..49c5625d 100644
--- a/pkg/development/validation.go
+++ b/pkg/development/validation.go
@@ -5,7 +5,7 @@ import (
 	"sort"
 	"strings"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/google/go-cmp/cmp"
 	yaml "gopkg.in/yaml.v2"
 
diff --git a/pkg/development/warnings.go b/pkg/development/warnings.go
index a607c6ff..5a6b5a78 100644
--- a/pkg/development/warnings.go
+++ b/pkg/development/warnings.go
@@ -4,7 +4,7 @@ import (
 	"context"
 	"fmt"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 
 	log "github.com/authzed/spicedb/internal/logging"
 	"github.com/authzed/spicedb/pkg/namespace"
diff --git a/pkg/genutil/ensure.go b/pkg/genutil/ensure.go
index 8d1069f7..18787beb 100644
--- a/pkg/genutil/ensure.go
+++ b/pkg/genutil/ensure.go
@@ -1,7 +1,7 @@
 package genutil
 
 import (
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 
 	"github.com/authzed/spicedb/pkg/spiceerrors"
 )
diff --git a/pkg/query/build_tree.go b/pkg/query/build_tree.go
index f7f47400..e0be6450 100644
--- a/pkg/query/build_tree.go
+++ b/pkg/query/build_tree.go
@@ -1,6 +1,7 @@
 package query
 
 import (
+	"errors"
 	"fmt"
 
 	core "github.com/authzed/spicedb/pkg/proto/core/v1"
@@ -89,7 +90,10 @@ func (b *iteratorBuilder) buildIteratorFromSchemaInternal(definitionName string,
 	} else if r, ok := def.GetRelation(relationName); ok {
 		result, err = b.buildIteratorFromRelation(r, withSubRelations)
 	} else {
-		err = fmt.Errorf("BuildIteratorFromSchema: couldn't find a relation or permission named `%s` in definition `%s`", relationName, definitionName)
+		err = RelationNotFoundError{
+			definitionName: definitionName,
+			relationName:   relationName,
+		}
 	}
 
 	// Remove from building after we're done (allows reuse in other branches)
@@ -284,6 +288,9 @@ func (b *iteratorBuilder) buildBaseRelationIterator(br *schema.BaseRelation, wit
 // buildArrowIterators creates a union of arrow iterators for the given relation and right-hand side
 func (b *iteratorBuilder) buildArrowIterators(rel *schema.Relation, rightSide string) (Iterator, error) {
 	union := NewUnion()
+	hasMultipleBaseRelations := len(rel.BaseRelations()) > 1
+	var lastNotFoundError error
+
 	for _, br := range rel.BaseRelations() {
 		left, err := b.buildBaseRelationIterator(br, false)
 		if err != nil {
@@ -291,17 +298,37 @@ func (b *iteratorBuilder) buildArrowIterators(rel *schema.Relation, rightSide st
 		}
 		right, err := b.buildIteratorFromSchemaInternal(br.Type(), rightSide, false)
 		if err != nil {
+			// If the right side doesn't exist on this type, the arrow produces an empty set.
+			// This is valid when a relation has multiple types and the arrow only
+			// applies to some of them. If there's only one base relation, we should error.
+			if errors.As(err, &RelationNotFoundError{}) {
+				if hasMultipleBaseRelations {
+					union.addSubIterator(NewEmptyFixedIterator())
+					continue
+				}
+				lastNotFoundError = err
+				continue
+			}
 			return nil, err
 		}
 		arrow := NewArrow(left, right)
 		union.addSubIterator(arrow)
 	}
+
+	// If we have no sub-iterators and only have a not-found error, return that error
+	if len(union.Subiterators()) == 0 && lastNotFoundError != nil {
+		return nil, lastNotFoundError
+	}
+
 	return union, nil
 }
 
 // buildIntersectionArrowIterators creates a union of intersection arrow iterators for the given relation and right-hand side
 func (b *iteratorBuilder) buildIntersectionArrowIterators(rel *schema.Relation, rightSide string) (Iterator, error) {
 	union := NewUnion()
+	hasMultipleBaseRelations := len(rel.BaseRelations()) > 1
+	var lastNotFoundError error
+
 	for _, br := range rel.BaseRelations() {
 		left, err := b.buildBaseRelationIterator(br, false)
 		if err != nil {
@@ -309,11 +336,28 @@ func (b *iteratorBuilder) buildIntersectionArrowIterators(rel *schema.Relation,
 		}
 		right, err := b.buildIteratorFromSchemaInternal(br.Type(), rightSide, false)
 		if err != nil {
+			// If the right side doesn't exist on this type, the intersection arrow produces an empty set.
+			// This is valid when a relation has multiple types and the arrow only
+			// applies to some of them. If there's only one base relation, we should error.
+			if errors.As(err, &RelationNotFoundError{}) {
+				if hasMultipleBaseRelations {
+					union.addSubIterator(NewEmptyFixedIterator())
+					continue
+				}
+				lastNotFoundError = err
+				continue
+			}
 			return nil, err
 		}
 		intersectionArrow := NewIntersectionArrow(left, right)
 		union.addSubIterator(intersectionArrow)
 	}
+
+	// If we have no sub-iterators and only have a not-found error, return that error
+	if len(union.Subiterators()) == 0 && lastNotFoundError != nil {
+		return nil, lastNotFoundError
+	}
+
 	return union, nil
 }
 
@@ -327,3 +371,13 @@ func functionTypeString(ft schema.FunctionType) string {
 		return "unknown"
 	}
 }
+
+// RelationNotFoundError is returned when a relation or permission is not found in a definition
+type RelationNotFoundError struct {
+	definitionName string
+	relationName   string
+}
+
+func (e RelationNotFoundError) Error() string {
+	return fmt.Sprintf("BuildIteratorFromSchema: couldn't find a relation or permission named `%s` in definition `%s`", e.relationName, e.definitionName)
+}
diff --git a/pkg/schema/reachabilitygraph.go b/pkg/schema/reachabilitygraph.go
index 85a4751f..50d3d28f 100644
--- a/pkg/schema/reachabilitygraph.go
+++ b/pkg/schema/reachabilitygraph.go
@@ -37,7 +37,7 @@ func (rg *DefinitionReachability) RelationsEncounteredForResource(
 	ctx context.Context,
 	resourceType *core.RelationReference,
 ) ([]*core.RelationReference, error) {
-	_, relationNames, err := rg.computeEntrypoints(ctx, resourceType, nil /* include all entrypoints */, reachabilityFull, entrypointLookupFindAll)
+	_, relationNames, err := rg.computeEntrypoints(ctx, resourceType, nil, reachabilityFull /* include all entrypoints */, entrypointLookupFindAll)
 	if err != nil {
 		return nil, err
 	}
@@ -85,11 +85,12 @@ func (rg *DefinitionReachability) RelationsEncounteredForSubject(
 						continue
 					}
 
-					encounteredRelations := map[string]struct{}{}
+					allEncounteredRelations := mapz.NewSet[string]()
+					encounteredRelationsForComputation := mapz.NewSet[string]()
 					err := nrg.collectEntrypoints(ctx, &core.RelationReference{
 						Namespace: nsDef.Name,
 						Relation:  relation.Name,
-					}, subjectType, collected, encounteredRelations, reachabilityFull, entrypointLookupFindAll)
+					}, subjectType, collected, allEncounteredRelations, encounteredRelationsForComputation, reachabilityFull, entrypointLookupFindAll)
 					if err != nil {
 						return nil, err
 					}
@@ -193,10 +194,13 @@ func (rg *DefinitionReachability) computeEntrypoints(
 	}
 
 	collected := &[]ReachabilityEntrypoint{}
-	encounteredRelations := map[string]struct{}{}
-	err := rg.collectEntrypoints(ctx, resourceType, optionalSubjectType, collected, encounteredRelations, reachabilityOption, entrypointLookupOption)
+
+	allEncounteredRelations := mapz.NewSet[string]()
+	encounteredRelationsForComputation := mapz.NewSet[string]()
+
+	err := rg.collectEntrypoints(ctx, resourceType, optionalSubjectType, collected, allEncounteredRelations, encounteredRelationsForComputation, reachabilityOption, entrypointLookupOption)
 	if err != nil {
-		return nil, slices.Collect(maps.Keys(encounteredRelations)), err
+		return nil, allEncounteredRelations.AsSlice(), err
 	}
 
 	collectedEntrypoints := *collected
@@ -213,7 +217,7 @@ func (rg *DefinitionReachability) computeEntrypoints(
 	for _, entrypoint := range collectedEntrypoints {
 		hash, err := entrypoint.Hash()
 		if err != nil {
-			return nil, slices.Collect(maps.Keys(encounteredRelations)), err
+			return nil, allEncounteredRelations.AsSlice(), err
 		}
 
 		if _, ok := entrypointMap[hash]; !ok {
@@ -222,7 +226,7 @@ func (rg *DefinitionReachability) computeEntrypoints(
 		}
 	}
 
-	return uniqueEntrypoints, slices.Collect(maps.Keys(encounteredRelations)), nil
+	return uniqueEntrypoints, allEncounteredRelations.AsSlice(), nil
 }
 
 func (rg *DefinitionReachability) getOrBuildGraph(ctx context.Context, resourceType *core.RelationReference, reachabilityOption reachabilityOption) (*core.ReachabilityGraph, error) {
@@ -253,17 +257,17 @@ func (rg *DefinitionReachability) collectEntrypoints(
 	resourceType *core.RelationReference,
 	optionalSubjectType *core.RelationReference,
 	collected *[]ReachabilityEntrypoint,
-	encounteredRelations map[string]struct{},
+	allEncounteredRelations *mapz.Set[string],
+	encounteredRelationsForComputation *mapz.Set[string],
 	reachabilityOption reachabilityOption,
 	entrypointLookupOption entrypointLookupOption,
 ) error {
 	// Ensure that we only process each relation once.
 	key := tuple.JoinRelRef(resourceType.Namespace, resourceType.Relation)
-	if _, ok := encounteredRelations[key]; ok {
+	if !encounteredRelationsForComputation.Add(key) {
 		return nil
 	}
-
-	encounteredRelations[key] = struct{}{}
+	allEncounteredRelations.Add(key)
 
 	rrg, err := rg.getOrBuildGraph(ctx, resourceType, reachabilityOption)
 	if err != nil {
@@ -274,7 +278,7 @@ func (rg *DefinitionReachability) collectEntrypoints(
 		// Add subject type entrypoints.
 		subjectTypeEntrypoints, ok := rrg.EntrypointsBySubjectType[optionalSubjectType.Namespace]
 		if ok {
-			addEntrypoints(subjectTypeEntrypoints, resourceType, collected, encounteredRelations)
+			addEntrypoints(subjectTypeEntrypoints, resourceType, collected, allEncounteredRelations, encounteredRelationsForComputation)
 		}
 
 		if entrypointLookupOption == entrypointLookupFindOne && len(*collected) > 0 {
@@ -284,7 +288,7 @@ func (rg *DefinitionReachability) collectEntrypoints(
 		// Add subject relation entrypoints.
 		subjectRelationEntrypoints, ok := rrg.EntrypointsBySubjectRelation[tuple.JoinRelRef(optionalSubjectType.Namespace, optionalSubjectType.Relation)]
 		if ok {
-			addEntrypoints(subjectRelationEntrypoints, resourceType, collected, encounteredRelations)
+			addEntrypoints(subjectRelationEntrypoints, resourceType, collected, allEncounteredRelations, encounteredRelationsForComputation)
 		}
 
 		if entrypointLookupOption == entrypointLookupFindOne && len(*collected) > 0 {
@@ -293,11 +297,11 @@ func (rg *DefinitionReachability) collectEntrypoints(
 	} else {
 		// Add all entrypoints.
 		for _, entrypoints := range rrg.EntrypointsBySubjectType {
-			addEntrypoints(entrypoints, resourceType, collected, encounteredRelations)
+			addEntrypoints(entrypoints, resourceType, collected, allEncounteredRelations, encounteredRelationsForComputation)
 		}
 
 		for _, entrypoints := range rrg.EntrypointsBySubjectRelation {
-			addEntrypoints(entrypoints, resourceType, collected, encounteredRelations)
+			addEntrypoints(entrypoints, resourceType, collected, allEncounteredRelations, encounteredRelationsForComputation)
 		}
 	}
 
@@ -309,7 +313,7 @@ func (rg *DefinitionReachability) collectEntrypoints(
 	for _, entrypointSetKey := range keys {
 		entrypointSet := rrg.EntrypointsBySubjectRelation[entrypointSetKey]
 		if entrypointSet.SubjectRelation != nil && entrypointSet.SubjectRelation.Relation != tuple.Ellipsis {
-			err := rg.collectEntrypoints(ctx, entrypointSet.SubjectRelation, optionalSubjectType, collected, encounteredRelations, reachabilityOption, entrypointLookupOption)
+			err := rg.collectEntrypoints(ctx, entrypointSet.SubjectRelation, optionalSubjectType, collected, allEncounteredRelations, encounteredRelationsForComputation, reachabilityOption, entrypointLookupOption)
 			if err != nil {
 				return err
 			}
@@ -323,13 +327,12 @@ func (rg *DefinitionReachability) collectEntrypoints(
 	return nil
 }
 
-func addEntrypoints(entrypoints *core.ReachabilityEntrypoints, parentRelation *core.RelationReference, collected *[]ReachabilityEntrypoint, encounteredRelations map[string]struct{}) {
+func addEntrypoints(entrypoints *core.ReachabilityEntrypoints, parentRelation *core.RelationReference, collected *[]ReachabilityEntrypoint, allEncounteredRelations *mapz.Set[string], encounteredRelationsForComputation *mapz.Set[string]) {
 	for _, entrypoint := range entrypoints.Entrypoints {
 		if entrypoint.TuplesetRelation != "" {
 			key := tuple.JoinRelRef(entrypoint.TargetRelation.Namespace, entrypoint.TuplesetRelation)
-			encounteredRelations[key] = struct{}{}
+			allEncounteredRelations.Add(key)
 		}
-
 		*collected = append(*collected, ReachabilityEntrypoint{entrypoint, parentRelation})
 	}
 }
diff --git a/pkg/schemadsl/compiler/translator.go b/pkg/schemadsl/compiler/translator.go
index b79bd219..ff9dc69f 100644
--- a/pkg/schemadsl/compiler/translator.go
+++ b/pkg/schemadsl/compiler/translator.go
@@ -7,7 +7,7 @@ import (
 	"slices"
 	"strings"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/jzelinskie/stringz"
 
 	"github.com/authzed/spicedb/pkg/caveats"
@@ -287,9 +287,14 @@ func getSourcePosition(dslNode *dslNode, mapper input.PositionMapper) *core.Sour
 		return nil
 	}
 
-	// We're okay with these being zero if the cast fails.
-	uintLine, _ := safecast.Convert[uint64](line)
-	uintCol, _ := safecast.Convert[uint64](col)
+	uintLine, err := safecast.Convert[uint64](line)
+	if err != nil {
+		uintLine = 0
+	}
+	uintCol, err := safecast.Convert[uint64](col)
+	if err != nil {
+		uintCol = 0
+	}
 
 	return &core.SourcePosition{
 		ZeroIndexedLineNumber:     uintLine,
diff --git a/pkg/validationfile/blocks/assertions.go b/pkg/validationfile/blocks/assertions.go
index 7143ddc8..3da0c6f0 100644
--- a/pkg/validationfile/blocks/assertions.go
+++ b/pkg/validationfile/blocks/assertions.go
@@ -5,7 +5,7 @@ import (
 	"fmt"
 	"strings"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	yamlv3 "gopkg.in/yaml.v3"
 
 	"github.com/authzed/spicedb/pkg/spiceerrors"
diff --git a/pkg/validationfile/blocks/expectedrelations.go b/pkg/validationfile/blocks/expectedrelations.go
index 18752cd5..95b7c75e 100644
--- a/pkg/validationfile/blocks/expectedrelations.go
+++ b/pkg/validationfile/blocks/expectedrelations.go
@@ -6,7 +6,7 @@ import (
 	"slices"
 	"strings"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	yamlv3 "gopkg.in/yaml.v3"
 
 	"github.com/authzed/spicedb/pkg/spiceerrors"
diff --git a/pkg/validationfile/blocks/relationships.go b/pkg/validationfile/blocks/relationships.go
index acdd4e4d..439258e7 100644
--- a/pkg/validationfile/blocks/relationships.go
+++ b/pkg/validationfile/blocks/relationships.go
@@ -4,7 +4,7 @@ import (
 	"fmt"
 	"strings"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	yamlv3 "gopkg.in/yaml.v3"
 
 	"github.com/authzed/spicedb/pkg/spiceerrors"
diff --git a/pkg/validationfile/loader.go b/pkg/validationfile/loader.go
index 0f7d2c5d..83bc6008 100644
--- a/pkg/validationfile/loader.go
+++ b/pkg/validationfile/loader.go
@@ -9,7 +9,7 @@ import (
 	"path/filepath"
 	"strings"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 
 	log "github.com/authzed/spicedb/internal/logging"
 	dsctx "github.com/authzed/spicedb/internal/middleware/datastore"
diff --git a/tools/analyzers/go.mod b/tools/analyzers/go.mod
index 2c34c6dc..3349455f 100644
--- a/tools/analyzers/go.mod
+++ b/tools/analyzers/go.mod
@@ -4,11 +4,11 @@ go 1.25.3
 
 require (
 	github.com/samber/lo v1.52.0
-	golang.org/x/tools v0.37.0
+	golang.org/x/tools v0.38.0
 )
 
 require (
 	golang.org/x/mod v0.29.0 // indirect
-	golang.org/x/sync v0.17.0 // indirect
-	golang.org/x/text v0.22.0 // indirect
+	golang.org/x/sync v0.18.0 // indirect
+	golang.org/x/text v0.31.0 // indirect
 )
diff --git a/tools/analyzers/go.sum b/tools/analyzers/go.sum
index ae2463e3..78b423f6 100644
--- a/tools/analyzers/go.sum
+++ b/tools/analyzers/go.sum
@@ -4,9 +4,9 @@ github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
 github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
 golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
 golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
-golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
-golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
-golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
-golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
-golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
-golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
+golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
+golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
+golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
+golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
+golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=



Test patch:
diff --git a/internal/datastore/postgres/revisions_test.go b/internal/datastore/postgres/revisions_test.go
index 105397fb..271d065b 100644
--- a/internal/datastore/postgres/revisions_test.go
+++ b/internal/datastore/postgres/revisions_test.go
@@ -6,7 +6,7 @@ import (
 	"testing"
 	"time"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/stretchr/testify/require"
 )
 
@@ -52,7 +52,7 @@ func TestRevisionSerDe(t *testing.T) {
 	maxSizeList := make([]uint64, 20)
 	for i := range maxSizeList {
 		// i should be nonnegative
-		index, _ := safecast.Convert[uint64](i)
+		index := safecast.RequireConvert[uint64](t, i)
 		maxSizeList[i] = maxInt - uint64(len(maxSizeList)) + index
 	}
 
@@ -97,7 +97,7 @@ func TestRevisionSerDe(t *testing.T) {
 
 func TestTxIDTimestampAvailable(t *testing.T) {
 	// Timestamps should be non-negative
-	testTimestamp, _ := safecast.Convert[uint64](time.Now().Unix())
+	testTimestamp := safecast.RequireConvert[uint64](t, time.Now().Unix())
 	snapshot := snap(0, 5, 1)
 	pgr := postgresRevision{snapshot: snapshot, optionalTxID: NewXid8(1), optionalNanosTimestamp: testTimestamp}
 	receivedTimestamp, ok := pgr.OptionalNanosTimestamp()
diff --git a/internal/datastore/proxy/proxy_test/mock.go b/internal/datastore/proxy/proxy_test/mock.go
index d44e56b0..ec07da05 100644
--- a/internal/datastore/proxy/proxy_test/mock.go
+++ b/internal/datastore/proxy/proxy_test/mock.go
@@ -3,7 +3,7 @@ package proxy_test
 import (
 	"context"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/stretchr/testify/mock"
 
 	v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
@@ -308,7 +308,10 @@ func (dm *MockReadWriteTransaction) DeleteNamespaces(_ context.Context, nsNames
 func (dm *MockReadWriteTransaction) BulkLoad(_ context.Context, iter datastore.BulkWriteRelationshipSource) (uint64, error) {
 	args := dm.Called(iter)
 	// We're assuming this is non-negative.
-	uintArg, _ := safecast.Convert[uint64](args.Int(0))
+	uintArg, err := safecast.Convert[uint64](args.Int(0))
+	if err != nil {
+		uintArg = 0
+	}
 	return uintArg, args.Error(1)
 }
 
diff --git a/internal/datastore/proxy/schemacaching/estimatedsize_test.go b/internal/datastore/proxy/schemacaching/estimatedsize_test.go
index 700d9884..7e5a162e 100644
--- a/internal/datastore/proxy/schemacaching/estimatedsize_test.go
+++ b/internal/datastore/proxy/schemacaching/estimatedsize_test.go
@@ -10,7 +10,7 @@ import (
 	"testing"
 	"time"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/stretchr/testify/require"
 
 	"github.com/authzed/spicedb/internal/datastore/dsfortesting"
diff --git a/internal/datastore/revisions/optimized_test.go b/internal/datastore/revisions/optimized_test.go
index 91b0269f..5adcd184 100644
--- a/internal/datastore/revisions/optimized_test.go
+++ b/internal/datastore/revisions/optimized_test.go
@@ -7,7 +7,7 @@ import (
 	"time"
 
 	"github.com/benbjohnson/clock"
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/stretchr/testify/mock"
 	"github.com/stretchr/testify/require"
 	"golang.org/x/sync/errgroup"
@@ -193,7 +193,7 @@ func BenchmarkOptimizedRevisions(b *testing.B) {
 		validForNS := nowNS % quantization.Nanoseconds()
 		roundedNS := nowNS - validForNS
 		// This should be non-negative.
-		uintRoundedNs, _ := safecast.Convert[uint64](roundedNS)
+		uintRoundedNs := safecast.RequireConvert[uint64](b, roundedNS)
 		rev := NewForTransactionID(uintRoundedNs)
 		return rev, time.Duration(validForNS) * time.Nanosecond, nil
 	})
diff --git a/internal/dispatch/graph/lookupresources2_test.go b/internal/dispatch/graph/lookupresources2_test.go
index 6cb0771e..2244392f 100644
--- a/internal/dispatch/graph/lookupresources2_test.go
+++ b/internal/dispatch/graph/lookupresources2_test.go
@@ -8,7 +8,7 @@ import (
 	"testing"
 	"time"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/stretchr/testify/require"
 	"go.uber.org/goleak"
 	"google.golang.org/protobuf/types/known/structpb"
diff --git a/internal/dispatch/graph/lookupresources3_test.go b/internal/dispatch/graph/lookupresources3_test.go
index 6439457b..2de87f4b 100644
--- a/internal/dispatch/graph/lookupresources3_test.go
+++ b/internal/dispatch/graph/lookupresources3_test.go
@@ -8,7 +8,7 @@ import (
 	"testing"
 	"time"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/stretchr/testify/require"
 	"go.uber.org/goleak"
 	"google.golang.org/protobuf/types/known/structpb"
diff --git a/internal/dispatch/remote/cluster_test.go b/internal/dispatch/remote/cluster_test.go
index ef9a392c..db5f4458 100644
--- a/internal/dispatch/remote/cluster_test.go
+++ b/internal/dispatch/remote/cluster_test.go
@@ -10,7 +10,7 @@ import (
 	"time"
 
 	"github.com/caio/go-tdigest/v4"
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/dustin/go-humanize"
 	"github.com/stretchr/testify/require"
 	"google.golang.org/genproto/googleapis/rpc/errdetails"
diff --git a/internal/gateway/gateway_test.go b/internal/gateway/gateway_test.go
index 883703e4..03434931 100644
--- a/internal/gateway/gateway_test.go
+++ b/internal/gateway/gateway_test.go
@@ -4,12 +4,14 @@ import (
 	"net/http"
 	"testing"
 
+	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
 	"github.com/stretchr/testify/require"
 	"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
 	"go.opentelemetry.io/otel"
 	"go.opentelemetry.io/otel/propagation"
 	"go.opentelemetry.io/otel/trace"
 	"go.uber.org/goleak"
+	"google.golang.org/grpc/metadata"
 
 	"github.com/authzed/spicedb/pkg/testutil"
 )
@@ -56,3 +58,99 @@ func TestCloseConnections(t *testing.T) {
 	// if connections are not closed, goleak would detect it
 	require.NoError(t, gatewayHandler.Close())
 }
+
+func TestCustomIncomingHeaderMatcher(t *testing.T) {
+	// Test x-request-id is passed through without grpcgateway- prefix
+	key, ok := customIncomingHeaderMatcher("x-request-id")
+	require.True(t, ok)
+	require.Equal(t, "x-request-id", key)
+
+	// Test case insensitivity
+	key, ok = customIncomingHeaderMatcher("X-Request-ID")
+	require.True(t, ok)
+	require.Equal(t, "x-request-id", key)
+
+	key, ok = customIncomingHeaderMatcher("X-REQUEST-ID")
+	require.True(t, ok)
+	require.Equal(t, "x-request-id", key)
+
+	// Test other headers use default behaviour
+	key, ok = customIncomingHeaderMatcher("authorization")
+	require.True(t, ok)
+	require.Equal(t, "grpcgateway-Authorization", key)
+}
+
+func TestForwardRequestIDTrailer(t *testing.T) {
+	t.Run("with standard x-request-id key", func(t *testing.T) {
+		md := runtime.ServerMetadata{
+			TrailerMD: metadata.Pairs("x-request-id", "test-id-123"),
+		}
+		ctx := runtime.NewServerMetadataContext(t.Context(), md)
+
+		recorder := &headerRecorder{header: http.Header{}}
+		err := forwardRequestIDTrailer(ctx, recorder, nil)
+		require.NoError(t, err)
+		require.Equal(t, "test-id-123", recorder.Header().Get("X-Request-Id"))
+	})
+
+	t.Run("with legacy io.spicedb.respmeta.requestid key", func(t *testing.T) {
+		md := runtime.ServerMetadata{
+			TrailerMD: metadata.Pairs("io.spicedb.respmeta.requestid", "legacy-id-456"),
+		}
+		ctx := runtime.NewServerMetadataContext(t.Context(), md)
+
+		recorder := &headerRecorder{header: http.Header{}}
+		err := forwardRequestIDTrailer(ctx, recorder, nil)
+		require.NoError(t, err)
+		require.Equal(t, "legacy-id-456", recorder.Header().Get("X-Request-Id"))
+	})
+
+	t.Run("prefers standard key over legacy", func(t *testing.T) {
+		md := runtime.ServerMetadata{
+			TrailerMD: metadata.Join(
+				metadata.Pairs("x-request-id", "standard-id"),
+				metadata.Pairs("io.spicedb.respmeta.requestid", "legacy-id"),
+			),
+		}
+		ctx := runtime.NewServerMetadataContext(t.Context(), md)
+
+		recorder := &headerRecorder{header: http.Header{}}
+		err := forwardRequestIDTrailer(ctx, recorder, nil)
+		require.NoError(t, err)
+		require.Equal(t, "standard-id", recorder.Header().Get("X-Request-Id"))
+	})
+
+	t.Run("without metadata", func(t *testing.T) {
+		recorder := &headerRecorder{header: http.Header{}}
+		err := forwardRequestIDTrailer(t.Context(), recorder, nil)
+		require.NoError(t, err)
+		require.Empty(t, recorder.Header().Get("X-Request-Id"))
+	})
+
+	t.Run("without request ID in metadata", func(t *testing.T) {
+		md := runtime.ServerMetadata{
+			TrailerMD: metadata.Pairs("some-other-header", "value"),
+		}
+		ctx := runtime.NewServerMetadataContext(t.Context(), md)
+
+		recorder := &headerRecorder{header: http.Header{}}
+		err := forwardRequestIDTrailer(ctx, recorder, nil)
+		require.NoError(t, err)
+		require.Empty(t, recorder.Header().Get("X-Request-Id"))
+	})
+}
+
+// headerRecorder is a test helper that implements http.ResponseWriter
+type headerRecorder struct {
+	header http.Header
+}
+
+func (h *headerRecorder) Header() http.Header {
+	return h.header
+}
+
+func (h *headerRecorder) Write([]byte) (int, error) {
+	return 0, nil
+}
+
+func (h *headerRecorder) WriteHeader(statusCode int) {}
diff --git a/internal/services/v1/experimental_test.go b/internal/services/v1/experimental_test.go
index 33f451fa..631f0e4f 100644
--- a/internal/services/v1/experimental_test.go
+++ b/internal/services/v1/experimental_test.go
@@ -10,7 +10,7 @@ import (
 	"strconv"
 	"testing"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/jzelinskie/stringz"
 	"github.com/stretchr/testify/require"
 	"go.uber.org/goleak"
@@ -413,8 +413,7 @@ func TestBulkExportRelationshipsWithFilter(t *testing.T) {
 				cancel()
 			}
 
-			// These are statically defined.
-			expectedCount, _ := safecast.Convert[uint64](tc.expectedCount)
+			expectedCount := safecast.RequireConvert[uint64](t, tc.expectedCount)
 			require.Equal(expectedCount, totalRead, "found: %v", foundRels.AsSlice())
 			require.True(remainingRels.IsEmpty(), "rels were not exported %#v", remainingRels.AsSlice())
 		})
@@ -1641,7 +1640,7 @@ func TestExperimentalCountRelationships(t *testing.T) {
 	// Write the schema.
 	_, err := schemaClient.WriteSchema(t.Context(), &v1.WriteSchemaRequest{
 		Schema: `definition user {}
-		
+
 		definition document {
 			relation viewer: user
 			relation editor: user
diff --git a/internal/services/v1/permissions_test.go b/internal/services/v1/permissions_test.go
index becaccbe..a1599c53 100644
--- a/internal/services/v1/permissions_test.go
+++ b/internal/services/v1/permissions_test.go
@@ -14,7 +14,7 @@ import (
 	"testing"
 	"time"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/stretchr/testify/require"
 	"go.uber.org/goleak"
 	"google.golang.org/genproto/googleapis/rpc/errdetails"
@@ -2458,8 +2458,7 @@ func TestExportBulkRelationshipsWithFilter(t *testing.T) {
 				cancel()
 			}
 
-			// These are statically defined.
-			expectedCount, _ := safecast.Convert[uint64](tc.expectedCount)
+			expectedCount := safecast.RequireConvert[uint64](t, tc.expectedCount)
 			require.Equal(expectedCount, totalRead, "found: %v", foundRels.AsSlice())
 			require.True(remainingRels.IsEmpty(), "rels were not exported %#v", remainingRels.AsSlice())
 		})
diff --git a/internal/services/v1/relationships_test.go b/internal/services/v1/relationships_test.go
index e3773666..56cadca2 100644
--- a/internal/services/v1/relationships_test.go
+++ b/internal/services/v1/relationships_test.go
@@ -11,7 +11,7 @@ import (
 	"testing"
 	"time"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/stretchr/testify/require"
 	"golang.org/x/sync/errgroup"
 	"google.golang.org/grpc/codes"
@@ -1385,7 +1385,7 @@ func TestDeleteRelationshipsBeyondLimitPartial(t *testing.T) {
 				afterDelete := readOfType(require, "document", client, zedtoken.MustNewFromRevisionForTesting(headRev))
 				require.LessOrEqual(len(beforeDelete)-len(afterDelete), batchSize)
 
-				bs, _ := safecast.Convert[uint64](batchSize)
+				bs := safecast.RequireConvert[uint64](t, batchSize)
 				require.LessOrEqual(resp.RelationshipsDeletedCount, bs)
 
 				if i == 0 {
diff --git a/pkg/datastore/pagination/iterator_test.go b/pkg/datastore/pagination/iterator_test.go
index 6c7419c1..1b63febf 100644
--- a/pkg/datastore/pagination/iterator_test.go
+++ b/pkg/datastore/pagination/iterator_test.go
@@ -6,7 +6,7 @@ import (
 	"strconv"
 	"testing"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/stretchr/testify/mock"
 	"github.com/stretchr/testify/require"
 
diff --git a/pkg/datastore/test/bulk.go b/pkg/datastore/test/bulk.go
index d2894eb6..42073196 100644
--- a/pkg/datastore/test/bulk.go
+++ b/pkg/datastore/test/bulk.go
@@ -6,7 +6,7 @@ import (
 	"strconv"
 	"testing"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/stretchr/testify/require"
 	"google.golang.org/grpc/codes"
 
@@ -40,8 +40,7 @@ func BulkUploadTest(t *testing.T, tester DatastoreTester) {
 				t,
 			)
 
-			// This is statically defined so we can cast straight.
-			uintTc, _ := safecast.Convert[uint64](tc)
+			uintTc := safecast.RequireConvert[uint64](t, tc)
 			_, err = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error {
 				loaded, err := rwt.BulkLoad(ctx, bulkSource)
 				require.NoError(err)
@@ -141,8 +140,7 @@ func BulkUploadWithCaveats(t *testing.T, tester DatastoreTester) {
 	)
 	bulkSource.WithCaveat = true
 
-	// This is statically defined so we can cast straight.
-	uintTc, _ := safecast.Convert[uint64](tc)
+	uintTc := safecast.RequireConvert[uint64](t, tc)
 	lastRevision, err := ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error {
 		loaded, err := rwt.BulkLoad(ctx, bulkSource)
 		require.NoError(err)
@@ -184,8 +182,7 @@ func BulkUploadWithExpiration(t *testing.T, tester DatastoreTester) {
 	)
 	bulkSource.WithExpiration = true
 
-	// This is statically defined so we can cast straight.
-	uintTc, _ := safecast.Convert[uint64](tc)
+	uintTc := safecast.RequireConvert[uint64](t, tc)
 	lastRevision, err := ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error {
 		loaded, err := rwt.BulkLoad(ctx, bulkSource)
 		require.NoError(err)
@@ -222,8 +219,7 @@ func BulkUploadEditCaveat(t *testing.T, tester DatastoreTester) {
 		t,
 	)
 
-	// This is statically defined so we can cast straight.
-	uintTc, _ := safecast.Convert[uint64](tc)
+	uintTc := safecast.RequireConvert[uint64](t, tc)
 	lastRevision, err := ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error {
 		loaded, err := rwt.BulkLoad(ctx, bulkSource)
 		require.NoError(err)
diff --git a/pkg/datastore/test/pagination.go b/pkg/datastore/test/pagination.go
index 5ee6784e..98f597cd 100644
--- a/pkg/datastore/test/pagination.go
+++ b/pkg/datastore/test/pagination.go
@@ -6,7 +6,7 @@ import (
 	"sort"
 	"testing"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/stretchr/testify/require"
 
 	"github.com/authzed/spicedb/internal/testfixtures"
@@ -85,7 +85,7 @@ func LimitTest(t *testing.T, tester DatastoreTester) {
 	for _, objectType := range testCases {
 		expected := sortedStandardData(objectType, options.ByResource)
 		for limit := 1; limit <= len(expected)+1; limit++ {
-			testLimit, _ := safecast.Convert[uint64](limit)
+			testLimit := safecast.RequireConvert[uint64](t, limit)
 			t.Run(fmt.Sprintf("%s-%d", objectType, limit), func(t *testing.T) {
 				require := require.New(t)
 				ctx := t.Context()
@@ -189,7 +189,7 @@ func OrderedLimitTest(t *testing.T, tester DatastoreTester) {
 		}
 
 		for limit := 1; limit <= len(expected); limit++ {
-			testLimit, _ := safecast.Convert[uint64](limit)
+			testLimit := safecast.RequireConvert[uint64](t, limit)
 
 			t.Run(tc.name, func(t *testing.T) {
 				require := require.New(t)
@@ -227,7 +227,7 @@ func ResumeTest(t *testing.T, tester DatastoreTester) {
 		}
 
 		for batchSize := 1; batchSize <= len(expected); batchSize++ {
-			testLimit, _ := safecast.Convert[uint64](batchSize)
+			testLimit := safecast.RequireConvert[uint64](t, batchSize)
 			expected := expected
 
 			t.Run(fmt.Sprintf("%s-batches-%d", tc.name, batchSize), func(t *testing.T) {
diff --git a/pkg/datastore/test/relationships.go b/pkg/datastore/test/relationships.go
index dcae6a91..606d1613 100644
--- a/pkg/datastore/test/relationships.go
+++ b/pkg/datastore/test/relationships.go
@@ -8,7 +8,7 @@ import (
 	"testing"
 	"time"
 
-	"github.com/ccoveille/go-safecast"
+	"github.com/ccoveille/go-safecast/v2"
 	"github.com/stretchr/testify/require"
 	"golang.org/x/sync/errgroup"
 	"google.golang.org/grpc/codes"
@@ -184,7 +184,7 @@ func SimpleTest(t *testing.T, tester DatastoreTester) {
 
 			// Check limit.
 			if len(testRels) > 1 {
-				limit, _ := safecast.Convert[uint64](len(testRels) - 1)
+				limit := safecast.RequireConvert[uint64](t, len(testRels)-1)
 				iter, err := dsReader.ReverseQueryRelationships(ctx, datastore.SubjectsFilter{
 					SubjectType: testUserNamespace,
 				}, options.WithLimitForReverse(&limit), options.WithQueryShapeForReverse(queryshape.Varying))
diff --git a/pkg/schema/reachabilitygraph_test.go b/pkg/schema/reachabilitygraph_test.go
index 5733964f..7105d758 100644
--- a/pkg/schema/reachabilitygraph_test.go
+++ b/pkg/schema/reachabilitygraph_test.go
@@ -1180,6 +1180,62 @@ func TestReachabilityGraph(t *testing.T) {
 				rrt("document", "view", false),
 			},
 		},
+		{
+			"multiple arrows with same starting point",
+			`definition special_user {}
+
+  definition user {
+  	relation special_user_mapping: special_user
+  	permission special_user = special_user_mapping
+  }
+
+  definition group {
+  	relation member: user
+  	permission membership = member + member->special_user
+  }
+
+  definition system {
+  	relation viewer: user | group#membership
+  	permission view = viewer + viewer->special_user
+  }`,
+			rr("system", "view"),
+			rr("special_user", "..."),
+			[]rrtStruct{
+				rrt("user", "special_user_mapping", true),
+			},
+			[]rrtStruct{
+				rrt("user", "special_user_mapping", true),
+			},
+		},
+		{
+			"multiple arrows with same non-terminal starting point",
+			`definition special_user {}
+
+  definition user {
+  	relation special_user_mapping: special_user
+  	permission special_user = special_user_mapping
+  }
+
+  definition group {
+  	relation member: user
+  	permission membership = member + member->special_user
+  }
+
+  definition system {
+  	relation viewer: user | group#membership
+  	permission view = viewer + viewer->special_user
+  }`,
+			rr("system", "view"),
+			rr("user", "special_user"),
+			[]rrtStruct{
+				rrt("system", "view", true),
+				rrt("group", "membership", true),
+			},
+			[]rrtStruct{
+				rrt("system", "view", true),
+				rrt("group", "membership", true),
+			},
+		},
 	}
 
 	for _, tc := range testCases {