distributed_cli 4.0.0

The `distributed` CLI for Distributed applications: contracts check/accept, scaffold projects, describe manifests, compile clients, and render schema artifacts. Also a library so other CLIs (e.g. hops) can mount its commands.
Documentation
---
name: distributed-graphql
description: Expose generated GraphQL queries and typed causal commands over Distributed services. Use when adding a GraphQL API, roles, model exposure, subscriptions, command mutations, or generated clients.
---

# GraphQL query service

Enable the framework features and mount one endpoint:

```toml
distributed = { path = "...", features = ["graphql", "sqlite"] } # or postgres
```

## Service layout (`src/query/`)

```
src/query/
  mod.rs          # build_engine(repository, &service, protocol_key)
  roles.rs        # role name constants
  <model>.rs      # one file per exposed model: permissions()
```

Typed command declarations live beside their executable handlers. There is no
second GraphQL command registry.

### Add a model exposure

1. Ensure the read model is in `read_model_catalog()`.
2. Add `src/query/<table>.rs`:

```rust
use distributed::graphql::{read, col, claim, ModelPermissions};
use crate::read_models::OrderView;

pub type Model = OrderView;

pub fn permissions() -> ModelPermissions<OrderView> {
    ModelPermissions::new()
        .grant("user", read()
            .all_columns()
            .rows(col("customer_id").eq(claim("x-user-id"))))
        .grant("anonymous", read().columns(["order_id", "status"]))
}
```

3. Register it in `src/query/mod.rs` via `graphql_models!(builder, order_view)` or
   `.model::<OrderView>(order_view::permissions())`.

### Roles

Declare the vocabulary with `.roles(&["user", "anonymous"])`. Deny-by-default:
a role sees nothing until granted via `.model(...)` / `.grant_all(role)` /
`.permission(...)`.

## Wire the service

```rust
let service = build_service(repository.clone())
    // Browser writes go through the OIDC-protected GraphQL command proxy only.
    .without_http_command_routes();
let engine = query::build_engine(
    &repository,
    &service,
    protocol_token_key,
)?;
let service = service.try_with_graphql(engine)?;
// POST /graphql  — queries / mutations
// GET  /graphql  — GraphiQL IDE when `.graphiql(true)`
```

`without_http_command_routes()` disables only the generic `POST /{command}`
transport on this GraphQL-facing service. Non-GraphQL services may keep those
direct command routes when that transport is intentional and independently
authenticated.

Build the executable `Service` first and pass that exact instance to
`GraphqlEngineBuilder::service`. For `Atomic<M>`, pass the repository handle
itself as the GraphQL pool source; a separately cloned raw pool cannot prove the
same transactional storage identity. Configure a stable, nonzero 32-byte
protocol key on every replica serving the same endpoint.

### GraphiQL (local visual explorer)

```bash
# Framework playground (seeded orders, no scaffold needed):
cargo run --example graphiql --features "graphql,sqlite"
# http://127.0.0.1:4000/graphql

# Or run your scaffolded service and open GET /graphql
GRAPHIQL=0 cargo run   # disable IDE in production
```

Default GraphiQL headers: `x-roles: user`, `x-user-id: demo`. Edit in the IDE
Headers panel. See `README § GraphQL query service`.

### Identity (public GraphQL)

Scaffold **always** wires `public_oidc_identity_from_env()` → **`OidcBearer`** +
`require_auth=true` (D6). Set `OIDC_ISSUER` + `OIDC_AUDIENCE` (or `OIDC_CLIENT_ID`).
If unset, placeholder issuer still uses OidcBearer (401 without Bearer) — **never**
ambient `DevHeaders` on the public scaffold path.

| Mode | Use |
|---|---|
| `OidcBearer` | Public API — validate `Authorization: Bearer` JWT (access token) |
| `Hybrid` | Bearer preferred; else gateway-injected headers |
| `TrustedProxy` | Mesh — strip client identity denylist at process |
| `DevHeaders` | Local GraphiQL / unit tests only (explicit opt-in) |

Wire via `.identity(distributed::graphql::public_oidc_identity_from_env())`.
Never trust raw client `x-user-id` / `x-roles` on a public edge.

Pass `change_stream(repo.read_model_changes())` for live subscriptions.

### Typed causal commands

Declare each GraphQL mutation on the executable route:

```rust
let routes = Routes::new()
    .with_repo(repository.aggregate::<Order>())
    .command_transition::<
        domain_commands::Create,
        CreateOrderInput,
        Eventual<CreateOrderPayload>,
    >("order.create")
    .roles(["user"])
    .handle(create_order);
```

Handlers accept `CausalCommandContext`, stage aggregate/outbox work on that
context, and return `PreparedCommand<Succeeded<_>>`, `PreparedCommand<Eventual<_>>`,
or `PreparedCommand<Atomic<M>>`. Never commit outside the framework-owned
causal boundary. Projector obligations derive from the transition event set +
portable/modeled handlers (`mutation!`), not separately authored command
confirmations/effects.

### Command consistency modes (ship contract)

**Same portable mutation IR.** Different **response proof** — do not collapse them.

| Contract | Meaning | Mutation response | Client seal |
|----------|---------|-------------------|-------------|
| `Succeeded<T>` | Tx succeeded; no projection promise | Payload only | Revalidate / live |
| `Eventual<T>` | Events committed; Eventual projectors apply later | Payload + **projection-delta** + `expects` | Automatic event→mutation preview, then wait obligations |
| `Atomic<M>` | Exact row in **same** command tx | **Typed row `M`** + direct **`records[]`** (no eventual modeled metadata, empty `expects`) | Same automatic preview; **`confirmDirectProjection(row, records)`** before await settles |

Handler for Atomic — this *is* returning atomic read-model updates:

```rust
let row = save_*(...).from_state(...)?;
repo.readmodel(row).publish_events().commit(agg)?.atomic()
```

Rules:

1. Use `Atomic<M>` only when the exact row is staged in-handler
   (`readmodel(row).…commit()?.atomic()`). Server will not attach causal
   projection-delta metadata to same-tx commands (by design).
2. Use `command_transition::<domain_commands::Transition, _, _>` so the generated
   transition supplies the exact event set and values the compiler can prove.
   The role-visible projector arm is the only event→mutation mapping.
3. Direct and Eventual placements may both export that portable preview program
   (`is_preview_eligible`); only Eventual creates causal wait obligations
   (`is_causally_eligible`).
4. Do not board-sim Atomic UI — the returned row is authoritative.
5. Otherwise `Succeeded<T>`; never invent a projected row.

Surface IR: SDL is built via `build_surface` → `graphql_sdl_from_surface` (shared inventory
for dialect-honest comparison ops, role grants, typed commands, and generated
client manifests).

## SDL artifact (CI gate)

```bash
distributed schema --format graphql --out schema.graphql
git diff --exit-code schema.graphql
```

`--dialect` is ignored with `--format graphql` (dialect-independent core surface).

## Scaffold

```bash
distributed scaffold my-service --query-api --read-models --store sqlite
```

Emits typed causal handlers, a service-derived GraphQL engine, `src/query/`
permissions, the `graphql` feature, and repository/token-key wiring.

## Client / replica drift (dogfood)

If unit tests pass but the live UI 500s with schema/surface errors: the API
pod is still compiling or clients/`js/dist` are stale. Rebuild and wait —
don't redesign roles. Bare `__typename` (role fingerprint) ≠ application
surface clients.

## Reference

- Framework docs: `README § GraphQL query service` in the Distributed repo
- Distributed GitKB: `specs/query-layer/v1/client-replica` and
  `specs/query-layer/v1/causal-command-protocol` (normative)