# How the GraphQL API Layer Is Structured for the Web Client in Macro

> Discover how Macro structures its three-layer GraphQL API for the web client. Explore the async-GraphQL engine, shared schema, and Tauri client with SQLite cache.

- Repository: [Macro/macro](https://github.com/macro-inc/macro)
- Tags: architecture
- Published: 2026-08-15

---

**The Macro repository implements a three-layer GraphQL architecture:** a server-side async-GraphQL engine in the Document Storage Service, a shared schema/resolver crate named "Soup," and a Tauri-based web client with a normalized SQLite cache and WebSocket subscriptions.

The Macro platform's web client consumes its GraphQL API through the **Document Storage Service** (`document_storage_service`). This service exposes the **"Soup"** GraphQL layer—a unified interface for documents, projects, teams, and other entities. The web client, built with Tauri, leverages a normalized cache plugin for performance and real-time synchronization via WebSocket subscriptions.

## Server-Side GraphQL Engine

The server layer routes HTTP requests and WebSocket connections into the async-GraphQL runtime. It also constructs request-scoped context containing user identity and shared mutation services.

### Axum Route Registration

In [`services/document_storage_service/src/api/graphql_soup.rs`](https://github.com/macro-inc/macro/blob/main/services/document_storage_service/src/api/graphql_soup.rs), the service registers three endpoints on Axum:

- `GET /soup/graphql` → GraphiQL IDE (development only)
- `POST /soup/graphql` → Query/mutation handler (`graphql_handler`)
- `GET /soup/graphql/ws` → Subscription handler (`graphql_ws_handler`)

These routes merge into the main API router through [`services/document_storage_service/src/api/mod.rs`](https://github.com/macro-inc/macro/blob/main/services/document_storage_service/src/api/mod.rs).

### Request Context Injection

The `AppState` struct in [`services/document_storage_service/src/api/context.rs`](https://github.com/macro-inc/macro/blob/main/services/document_storage_service/src/api/context.rs) holds three critical components:

- `graphql_soup_schema` – the compiled async-GraphQL schema
- `graphql_notification_reader` – streams real-time updates
- `graphql_entity_mutation_service` – handles entity writes

For each request, `graphql_query_context_data` and `graphql_subscription_context_data` inject:

- `macro_user_id` and `organization_id` (from session)
- Shared service handles (mutation service, notification reader)

Resolvers access these via `ctx.data::<T>()` without explicit parameters.

## GraphQL Schema and Resolvers

The **Soup** schema lives in the `graphql_soup` crate, separating type definitions from resolver implementations.

### Object Types

[`crates/graphql_soup/src/objects.rs`](https://github.com/macro-inc/macro/blob/main/crates/graphql_soup/src/objects.rs) defines **GraphQL object types** including `GraphqlSoupEntity`—a polymorphic wrapper returned by the `entity(id: ID!)` query. Other types cover documents, projects, teams, permissions, and organizational structures.

### Resolver Implementation

[`crates/graphql_soup/src/resolvers.rs`](https://github.com/macro-inc/macro/blob/main/crates/graphql_soup/src/resolvers.rs) implements three operation categories:

| Operation Type | Example | Purpose |
|---------------|---------|---------|
| **Queries** | `entity(id: ID!)` | Fetch single polymorphic entity by global ID |
| **Mutations** | `entityMutation` | Bundle all entity modifications (rename, move, trash, restore) |
| **Subscriptions** | `soupUpdates`, `notificationUpdates` | Stream real-time changes to connected clients |

All resolvers are **async** and access injected services through the request context.

## Client-Side Consumption

The web client in `apps/web` combines Urql, a Tauri-hosted cache plugin, and generated TypeScript types.

### Urql Client Configuration

[`apps/web/src/lib/service-clients/service-storage/graphql-soup.ts`](https://github.com/macro-inc/macro/blob/main/apps/web/src/lib/service-clients/service-storage/graphql-soup.ts) creates the client:

```typescript
import { createClient, cacheExchange, fetchExchange } from '@urql/core';
import { normalizedCacheExchange } from '@urql/exchange-graphcache';

export const soupClient = createClient({
  url: '/soup/graphql',
  exchanges: [
    normalizedCacheExchange({
      // Type policies for polymorphic Soup entities
      updates: {
        Mutation: {
          entityMutation: (result, args, cache, info) => {
            // Optimistic update handling
          }
        }
      }
    }),
    fetchExchange
  ],
  fetchOptions: () => ({
    credentials: 'include' // Session cookie propagation
  })
});

```

The WebSocket subscription endpoint connects separately:

```typescript
// Subscription client for real-time updates
const wsClient = createWSClient({
  url: `wss://${host}/soup/graphql/ws`
});

```

### Tauri Cache Plugin

[`apps/web/tauri/graphql_cache_plugin/src/lib.rs`](https://github.com/macro-inc/macro/blob/main/apps/web/tauri/graphql_cache_plugin/src/lib.rs) exposes host commands to the JavaScript runtime:

- `graphql_cache_init()` – Initialize SQLite-backed normalized cache
- `graphql_cache_read(key: String)` – Retrieve cached entity
- `graphql_cache_write(key: String, data: String)` – Store normalized result
- `graphql_cache_enqueue_optimistic_mutation(op: String)` – Queue optimistic update

These commands enable the `normalizedCacheExchange` to persist across sessions and survive process restarts.

### Type Generation Pipeline

The build process maintains type safety through schema extraction:

```bash

# Run from repository root

cargo x graphql-soup-schema

```

This writes to `apps/web/static_assets/schema.graphql`, which feeds the TypeScript generator. Output lands in [`apps/web/src/lib/service-clients/service-storage/graphql/generated/graphql.ts`](https://github.com/macro-inc/macro/blob/main/apps/web/src/lib/service-clients/service-storage/graphql/generated/graphql.ts), providing fully typed operations.

## How the Layers Connect

1. **Schema extraction** – The custom cargo command exports the Rust-defined schema to a static asset
2. **Type generation** – GraphQL Code Generator produces TypeScript types from `schema.graphql`
3. **Request flow** – Urql operations hit Axum endpoints with session-authenticated context
4. **Resolution** – Resolvers use injected services to read/write entities and broadcast notifications
5. **Response handling** – Results normalize into the SQLite cache, trigger optimistic updates, and stream via WebSocket subscriptions

## Key Files Reference

| Component | File Path |
|-----------|-----------|
| Router integration | [`services/document_storage_service/src/api/mod.rs`](https://github.com/macro-inc/macro/blob/main/services/document_storage_service/src/api/mod.rs) |
| GraphQL endpoints | [`services/document_storage_service/src/api/graphql_soup.rs`](https://github.com/macro-inc/macro/blob/main/services/document_storage_service/src/api/graphql_soup.rs) |
| Request context | [`services/document_storage_service/src/api/context.rs`](https://github.com/macro-inc/macro/blob/main/services/document_storage_service/src/api/context.rs) |
| Schema objects | [`crates/graphql_soup/src/objects.rs`](https://github.com/macro-inc/macro/blob/main/crates/graphql_soup/src/objects.rs) |
| Resolvers | [`crates/graphql_soup/src/resolvers.rs`](https://github.com/macro-inc/macro/blob/main/crates/graphql_soup/src/resolvers.rs) |
| Static schema | `apps/web/static_assets/schema.graphql` |
| Urql client | [`apps/web/src/lib/service-clients/service-storage/graphql-soup.ts`](https://github.com/macro-inc/macro/blob/main/apps/web/src/lib/service-clients/service-storage/graphql-soup.ts) |
| Tauri cache plugin | [`apps/web/tauri/graphql_cache_plugin/src/lib.rs`](https://github.com/macro-inc/macro/blob/main/apps/web/tauri/graphql_cache_plugin/src/lib.rs) |
| Generated types | [`apps/web/src/lib/service-clients/service-storage/graphql/generated/graphql.ts`](https://github.com/macro-inc/macro/blob/main/apps/web/src/lib/service-clients/service-storage/graphql/generated/graphql.ts) |

## Summary

- The **GraphQL API layer** spans a Rust async-GraphQL server and a Tauri-hosted web client with normalized caching
- **Soup** provides a unified polymorphic API for all entity types through queries, mutations, and subscriptions
- **Request context injection** eliminates explicit service parameters while maintaining type safety
- **WebSocket subscriptions** enable real-time collaboration with server-sent notifications
- **SQLite-backed normalization** persists cache state across client restarts and supports optimistic UI patterns

## Frequently Asked Questions

### What is "Soup" in Macro's GraphQL API?

Soup is the internal name for Macro's unified GraphQL schema layer that exposes all entity types—documents, projects, teams, and more—through a polymorphic `GraphqlSoupEntity` type. Located in the `crates/graphql_soup` crate, it provides a single entry point for queries, mutations, and subscriptions rather than separate domain-specific APIs.

### How does the web client maintain cache consistency?

The web client uses a **normalized cache** implemented as a Tauri plugin with SQLite storage. The `normalizedCacheExchange` from Urql ensures that updates to shared entities propagate across all active queries. Optimistic mutations enqueue immediately while awaiting server confirmation, and subscription events (`soupUpdates`, `notificationUpdates`) invalidate or refresh cached data in real time.

### Why does Macro use WebSocket subscriptions over HTTP polling?

WebSocket subscriptions at `/soup/graphql/ws` provide **push-based real-time updates** essential for collaborative document editing. Compared to polling, this reduces latency, server load, and notification delay. The subscription endpoint shares the same context injection and resolver infrastructure as standard queries, ensuring consistent authorization and data access patterns.

### How are TypeScript types kept synchronized with the Rust schema?

A custom build tool (`cargo x graphql-soup-schema`) extracts the complete GraphQL schema from the running Rust code and writes it to `apps/web/static_assets/schema.graphql`. The web client's build pipeline runs GraphQL Code Generator against this file to produce [`generated/graphql.ts`](https://github.com/macro-inc/macro/blob/main/generated/graphql.ts). This guarantees that TypeScript operations remain type-safe against the actual server implementation.