Skip to content

wundergraph/cosmo-streams-demo

Repository files navigation

Comic art of an astronaut on a wooden raft riding down a river.

Cosmo Streams Demo

Cosmo Streams doesn’t try to make traditional GraphQL subscriptions less painful. It removes the root cause of the pain altogether.

Why GraphQL Subscriptions Are So Hard

GraphQL subscriptions are notoriously difficult to build and operate at scale.

In a classic setup, you’re expected to run a dedicated subscription service yourself. That service must keep long-lived WebSocket connections, implement the correct GraphQL subprotocol, manage heartbeats and reconnects, listen for domain events, fetch and compose data from other services, and finally fan out updates to thousands of connected clients — all in real time.

This architecture is inherently stateful. Every active subscription lives somewhere in memory. As traffic grows, so does connection overhead, memory pressure, and operational complexity. Subtle performance issues and edge-case failures are common, especially once you move beyond a few hundred clients.

Federation makes things even harder. Now the router is no longer the only place dealing with subscriptions — your subgraphs must participate too. They often need to hold WebSocket connections, implement subscription loops, or support proprietary callback protocols. This creates tight coupling between your graph architecture and your runtime environment, making serverless deployments difficult or outright impossible.

In short: traditional GraphQL subscriptions push state, complexity, and scalability concerns deep into every layer of your system.

Cosmo Streams: A Different Model

Cosmo Streams takes a fundamentally different approach.

Instead of asking subgraphs to host subscriptions, Cosmo Streams treats subscriptions as an event-driven problem, not a connection-driven one.

Subgraphs remain completely stateless. They don’t manage WebSockets. They don’t track subscriptions. They don’t run subscription loops. When something changes, they simply emit an event to an existing message broker like Apache Kafka, NATS, or Redis — infrastructure most event-driven systems already rely on.

The Cosmo Router listens to those events.

When an event arrives, the router determines which active subscriptions are affected, efficiently deduplicates work, and fetches the required data from subgraphs using plain HTTP requests. The updated results are then broadcast to clients over WebSockets, SSE, or multipart responses — all fully managed at the gateway layer.

Why This Matters

This shift has important consequences:

  • Stateless subgraphs Subgraphs don’t hold connections or subscription state, making them ideal for Lambda, Cloud Run, or any serverless environment.

  • Centralized connection management All client connections live in the router, where they can be optimized, monitored, and scaled efficiently.

  • Event-native architecture Subscriptions integrate naturally with Kafka, NATS, or Redis instead of introducing yet another stateful protocol.

  • Better performance at scale The router deduplicates subscriptions, minimizes fetches, and uses highly efficient I/O primitives to handle tens of thousands of concurrent clients with low memory overhead.

  • Zero subscription logic in subgraphs No WebSocket servers, no callback protocols, no proprietary APIs — just HTTP.

🤖 How This Demo Works

This self-contained demo deploys three GraphQL subgraphs: Customers, Products, and Orders.

  • The Customers subgraph defines two customers: John Doe and Jane Doe.
  • The Products subgraph exposes three products that customers can order.
  • The Orders subgraph stores all orders created by customers, including their selected products.

None of these subgraphs serve subscriptions or mutations. On their api they can only be queried.

The demo also spawns a Cosmo Router. The router configures a special fourth subgraph, OrderEvents, which is an Event-Driven Graph. It is not backed by any subgraph and exposes:

  • the createOrder mutation
  • the orderCreated subscription

When createOrder is called, the router emits an orderRequested event to NATS.
The Orders subgraph receives this event, stores the order and publishes an orderCreated event.
The router then resolves the client requests and sends it to all subscribed clients.

Note

The Orders subgraph does not expose mutations. It stores orders exclusively through events.

A NATS server powers the event delivery layer.

flowchart LR
  subgraph clients["Clients"]
    direction TB
    mutator["Mutator Client"]
    subscriber["Subscriber Client<br/>(orderCreated subscription)"]
  end

  subgraph router["Cosmo Router"]
    direction TB
    routerCore["Router Core<br/>(EDFS)"]
    natsConsumer["NATS Consumer"]
  end

  nats[("NATS Server")]

  subgraph subgraphs["Subgraphs"]
    direction TB
    orders["Orders Subgraph"]
    products["Products Subgraph"]
    customers["Customers Subgraph"]
  end

  %% Mutation flow (EDFS publish)
  mutator -->|"① createOrder mutation"| routerCore
  routerCore -->|"② Publish to orderRequested"| nats
  nats -->|"③ Consume orderRequested"| orders
  orders -->|"④ Publish to orderCreated"| nats

  %% Subscription flow
  nats -->|"⑤ Consume orderCreated"| natsConsumer
  natsConsumer -->|"⑥ Trigger resolution"| routerCore
  routerCore -->|"⑦ Fetch entity data"| orders
  routerCore -->|"⑦ Fetch entity data"| products
  routerCore -->|"⑦ Fetch entity data"| customers
  routerCore -->|"⑧ Push subscription data"| subscriber
Loading

The demo also includes custom authentication and filtering logic using Cosmo Streams Custom Modules.

  • Clients must provide a valid JWT issued by a trusted authority.
  • orderRequested events are rejected if the JWT’s customer ID does not match the order’s customer ID.
  • Subscription streams are filtered so that customers only receive events related to their own orders.

Custom Module Hooks used in this demo

  1. SubscriptionOnStart

    • When: When a client starts a subscription.
    • Purpose: Validates the client’s JWT and its issuer.
  2. OnPublishEvent

    • When: After a createOrder mutation is invoked but before events are sent to NATS.
    • Purpose: Ensures the JWT’s customer ID matches the order’s customer ID.
  3. OnReceiveEvent

    • When: Whenever a new event is received from NATS before resolution.
    • Purpose: Filters events based on the client's claims (e.g., customers only receive their own orderCreated events).

These hooks enforce per-user authentication and event filtering for GraphQL subscriptions.
You can the code at router/module/module.go.

For more details, see the
Cosmo Streams Documentation.

▶️ Run the Demo

You must have Docker Desktop or Docker with Compose installed.

Start all subgraphs, the router, and a NATS server:

docker compose up -d

This command also starts two subscription clients (one for each customer). To view their logs:

docker compose logs -f subscriber-johndoe
docker compose logs -f subscriber-janedoe

To create orders, open another terminal:

source .env

# Create an order for John Doe (customer 1) with product IDs 2 and 3.
docker compose run --rm -e JWT_TOKEN=$JWT_JOHNDOE mutator 1 2 3

# Create an order for Jane Doe (customer 2) with product IDs 1 and 2.
docker compose run --rm -e JWT_TOKEN=$JWT_JANEDOE mutator 2 1 2

Only the correct customer’s subscription client will receive each event.

📝 License

This project is licensed under the MIT License.
See the LICENSE file for details.

About

Event-driven GraphQL subscriptions without stateful subgraphs.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •