GraphQL
POST /graphql, served by GraphqlController. /graphiql is mounted in development.
app/graphql/
The schema
Section titled “The schema”class UrisSchema < GraphQL::Schema query(Types::QueryType) mutation(Types::MutationType) subscription(Types::SubscriptionType)
use GraphQL::Subscriptions::ActionCableSubscriptions use GraphQL::Dataloader
max_depth(15) max_query_string_tokens(5000) validate_max_errors(100)endQueries
Section titled “Queries”| Field | Grant | |
|---|---|---|
tenant |
uris:read |
who answered |
item(id:) |
uris:read |
one item |
items(kind:, resourceId:, after:, limit:) |
uris:read |
a page of the catalog |
search(query:, kind:, limit:) |
uris:read |
the index, clamped to 1–200 |
kinds |
uris:read |
counts grouped by kind |
resources |
resources:read |
active resources, ordered by type and key |
runs(kind:, status:, after:, limit:) |
uris:read |
a page of runs |
mergeProposals(status:, after:, limit:) |
uris:read |
open proposals by default |
auditEvents(action:, status:, subject:, after:, limit:) |
uris:read |
a page of the audit log |
Paged fields go through Page.of(scope, after:, limit:) — keyset pagination on the primary key,
newest first, limit clamped to 1–200 with a default of 50. One extra row is fetched to answer
“is there more”; nothing counts.
Mutations
Section titled “Mutations”| Field | Grant |
|---|---|
analyzeItem mergeItems splitReference proposeMerges settleMergeProposal |
uris:write |
exportItems cancelRun |
uris:write |
syncResource checkResource setDefaultStorage setSyncInterval |
resources:command |
exportItems and cancelRun take uris:write here and resources:command over
MCP. Merging and dedupe are reachable here and nowhere else.
Subscriptions
Section titled “Subscriptions”itemAnalyzed, triggered by Item#announce_analyzed! both globally and per item id.
Subscriptions::BaseSubscription declares subscription_scope :tenant_id.
Authorization is the same Grant
Section titled “Authorization is the same Grant”Types::BaseField takes a grants: option and checks it in authorized?:
field :resources, [ Types::ResourceType ], null: false, grants: "resources:read"A field whose grant the token does not carry raises a GraphQL::ExecutionError naming the missing
scope. A field with no grants: is open to any authenticated caller.
Both interfaces reach Grant through the Granted concern. The browser’s token lives in the
encrypted session and is verified on the way in exactly as a presented bearer would be — see
Drive it from Claude.
The Ruby schema is the source of truth
Section titled “The Ruby schema is the source of truth”The TypeScript is generated from it and is not committed:
bin/rails graphql:dump_schema # writes web/schema.graphqlnpm run codegen # regenerates the TypeScriptbin/dev runs both watchers (Procfile.dev, the schema and codegen processes). CI runs the
same two commands before the typecheck, because web/schema.graphql and the generated types are build
products — a clean checkout has neither and the Vite build fails without them.
The client
Section titled “The client”urql, with @urql/exchange-graphcache and Action Cable subscriptions.