Skip to content

Conversation

@productdevbook
Copy link
Owner

@productdevbook productdevbook commented Dec 11, 2025

Summary

This PR adds real-time GraphQL subscription support to nitro-graphql with a unified, developer-friendly API that supports multiple transport mechanisms.

Key Features

  • WebSocket Transport (default) - Full-duplex communication using graphql-transport-ws protocol
  • SSE Transport - Firewall-friendly fallback using native EventSource
  • Auto Mode - Tries WebSocket first, automatically falls back to SSE if blocked
  • Session Multiplexing - Multiple subscriptions share a single WebSocket connection

Session Multiplexing

Multiple subscriptions can share a single WebSocket connection, reducing resource usage:

import { useCountdown, useServerTime, useSubscriptionSession } from '~/graphql/default/sdk'

// Create shared session (keep the whole object for passing to composables)
const subscriptionSession = useSubscriptionSession()
const { isConnected, state, subscriptionCount } = subscriptionSession

// All composables share 1 WebSocket connection
const { data: countdown, start, stop } = useCountdown({ from: 10 }, { session: subscriptionSession })
const { data: serverTime } = useServerTime({ session: subscriptionSession })

// With callbacks
const { data } = useCountdown({ from: 10 }, {
  session: subscriptionSession,
  immediate: true,
  onData: (count) => console.log('Count:', count),
  onError: (err) => console.error(err.message),
})

// Session auto-closes on component unmount

Framework-agnostic API:

const session = createSubscriptionSession()
const sub = session.subscribe(query, variables, onData, onError)
sub.unsubscribe()
session.close()

Usage Guide

1. Define Schema (server/graphql/schema.graphql)

type Subscription {
  """Simple counter that increments every second"""
  countdown(from: Int!): Int!

  """Receives a greeting message every 2 seconds"""
  greetings: String!

  """Current server time updated every second"""
  serverTime: DateTime!
}

2. Create Resolver (server/graphql/subscriptions.resolver.ts)

import { defineSubscription } from 'nitro-graphql/define'

export const subscriptionResolvers = defineSubscription({
  countdown: {
    async* subscribe(_parent, { from }, context) {
      for (let i = from; i >= 0; i--) {
        yield { countdown: i }
        await new Promise(resolve => setTimeout(resolve, 1000))
      }
    },
  },

  greetings: {
    async* subscribe() {
      const greetings = ['Merhaba!', 'Hello!', 'Bonjour!', 'Hola!']
      let index = 0
      while (true) {
        yield { greetings: greetings[index % greetings.length] }
        index++
        await new Promise(resolve => setTimeout(resolve, 2000))
      }
    },
  },

  serverTime: {
    async* subscribe() {
      while (true) {
        yield { serverTime: new Date().toISOString() }
        await new Promise(resolve => setTimeout(resolve, 1000))
      }
    },
  },
})

3. Define Client Query (app/graphql/subscriptions/subscriptions.graphql)

subscription Countdown($from: Int!) {
  countdown(from: $from)
}

subscription Greetings {
  greetings
}

subscription ServerTime {
  serverTime
}

4. Use in Vue Component

// Auto-generated composables - zero boilerplate!
import { useCountdown, useGreetings, useServerTime } from '~/graphql/default/sdk'

// WebSocket (default)
const { data, start, stop } = useCountdown({ from: 10 })

// SSE transport
const { data, start, stop } = useCountdown({ from: 10 }, { transport: 'sse' })

// Auto mode - WS first, SSE fallback
const { data, transport } = useCountdown({ from: 10 }, { transport: 'auto' })

Auto-Generated Config (app/graphql/default/subscribe.ts)

A subscribe.ts file is auto-generated once on first run. You can customize it for authentication, custom endpoints, or retry settings:

import { createSubscriptionClient } from 'nitro-graphql/subscribe'

const config = {
  // WebSocket endpoint (default: '/api/graphql/ws')
  wsEndpoint: '/api/graphql/ws',

  // SSE endpoint for SSE transport (default: '/api/graphql')
  sseEndpoint: '/api/graphql',

  // Connection timeout in ms (default: 10000)
  connectionTimeoutMs: 10000,

  // Max reconnection attempts (default: 5)
  maxRetries: 5,

  // Authentication params sent with connection_init
  // Can be a function for dynamic values (e.g., JWT tokens)
  connectionParams: () => ({
    authorization: `Bearer ${getToken()}`,
  }),
}

export const subscriptionClient = createSubscriptionClient(config)

Note: This file is generated only once - your customizations are preserved!


Developer Experience

Before (SSE with boilerplate - 20+ lines):

const sseCountdown = ref<number | null>(null)
const isSseActive = ref(false)
let sseHandle: SseSubscriptionHandle | null = null

function startSseCountdown() {
  sseHandle = createSseSubscription<number>('/api/graphql', {
    query: 'subscription { countdown(from: 10) }',
    onData: (data) => { sseCountdown.value = data },
    onError: (error) => { stopSseCountdown() },
  })
  isSseActive.value = true
}

function stopSseCountdown() {
  sseHandle?.close()
  sseHandle = null
  isSseActive.value = false
}

onUnmounted(() => stopSseCountdown())

After (same API as WebSocket - 1 line!):

const { data: sseCountdown, isActive, start, stop } = useCountdown({ from: 10 }, { transport: 'sse' })

Technical Details

Server-Side

  • WebSocket Handler (src/routes/graphql-yoga-ws.ts, apollo-server-ws.ts)
    • Uses crossws for H3/Nitro WebSocket support
    • Implements graphql-transport-ws protocol
    • Server-initiated keep-alive (ping/pong every 25s)
    • Graceful shutdown with complete message handling
    • Per-peer subscription tracking (max 20 subs per connection)

Client-Side

  • Subscription Client (src/subscribe/index.ts)
    • Framework-agnostic core with Vue composables generated
    • Automatic reconnection with exponential backoff + jitter
    • Session multiplexing (multiple subscriptions on single connection)
    • Connection state management: idle → connecting → connected → reconnecting → error

Code Generation

  • Auto-generates Vue composables for each subscription
  • Type-safe with full TypeScript support
  • Drizzle-style builder API also available:
    subscription.Countdown({ from: 10 })
      .onData(data => console.log(data))
      .start()

Files Changed

Area Files
Core src/subscribe/index.ts, src/routes/*-ws.ts
Codegen src/utils/client-codegen.ts
Types src/types/index.ts
Templates src/templates/subscribe-client.ts
Playgrounds playgrounds/nuxt/*, playgrounds/nitro/*

Test Plan

  • WebSocket subscriptions work in Nuxt playground
  • SSE subscriptions work with { transport: 'sse' } option
  • Auto mode falls back to SSE when WS is unavailable
  • Session multiplexing works (multiple subs on one connection)
  • Reconnection works after server restart
  • Graceful shutdown sends complete message
  • subscribe.ts is generated only once (customizations preserved)

🤖 Generated with Claude Code

productdevbook and others added 14 commits December 11, 2025 13:42
- Add WebSocket handlers for GraphQL Yoga and Apollo Server
- Implement graphql-ws protocol for real-time subscriptions
- Add SubscriptionsConfig type and endpoint configuration
- Auto-enable experimental websocket flag when subscriptions enabled
- Add playground examples with PubSub chat implementation

Usage:
```typescript
// nitro.config.ts
graphql: {
  framework: 'graphql-yoga',
  subscriptions: {
    enabled: true,
  },
}
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
…port

- Changed import paths for Vue composables in subscriptions.vue to use sdk.ts.
- Enhanced subscribe-client.ts to support multiplexing with createSession() for shared connections.
- Updated subscription client methods to return SubscriptionHandle instead of SubscriptionClient.
- Added new useSubscriptionSession hook for managing multiplexed subscriptions.
- Deprecated generateNuxtSubscriptionComposables function; noted that composables are now generated directly in sdk.ts.
- Improved error handling and connection management in subscription logic.
…ment

- Updated `useSubscriptionSession` to maintain reactivity by subscribing to session state changes.
- Refactored multiplexing logic in `multiplexing-test.vue` to use a single session object for all composables.
- Enhanced connection management in `subscribe-client.ts` with a Timer Manager for better handling of timeouts and intervals.
- Introduced keep-alive functionality to maintain WebSocket connections.
- Improved error handling and state notifications for subscriptions.
- Added state change callbacks to `SubscriptionSession` for better reactivity.
- Updated client code generation to include detailed JSDoc comments for subscription composables.
…prove code organization

- Simplified the subscription client template for easier customization.
- Reorganized imports and exports in subscription-related files for clarity.
- Updated type generation to include subscription composables and external services.
- Enhanced the tsdown configuration to include subscription-related files.
@productdevbook productdevbook changed the base branch from main to v1 December 11, 2025 18:57
- Bump @antfu/eslint-config from ^6.3.0 to ^6.6.1
- Update @apollo/subgraph from ^2.12.1 to ^2.12.2
- Upgrade @nuxt/kit and @nuxt/schema from ^4.2.1 to ^4.2.2
- Update @types/node from ^24.10.1 to ^24.10.3
- Bump nuxt from ^4.2.1 to ^4.2.2
- Upgrade oxc-parser from ^0.101.0 to ^0.102.0
- Update tailwindcss from ^4.1.17 to ^4.1.18
- Bump tsdown from ^0.16.8 to ^0.17.2
- Update vue-router from ^4.6.3 to ^4.6.4
@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Dec 11, 2025

Deploying nitro-graphql with  Cloudflare Pages  Cloudflare Pages

Latest commit: b70cda7
Status:🚫  Build failed.

View logs

productdevbook and others added 9 commits December 11, 2025 22:25
Remove the `sse: boolean` property from TransportOptions interface
to simplify the API. Users should now use `transport: 'sse'` instead.

Changes:
- Remove `sse` property from TransportOptions (subscribe/index.ts)
- Simplify resolveTransport() function
- Remove `sse` property from UseSubscriptionOptions (client-codegen.ts)
- Update transportOptions object construction

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Add changelog configuration (changelogithub.config.ts)
- Simplify release workflow for changelog generation
- Add security configuration types
- Fix unused error parameter in apollo-server.ts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@productdevbook productdevbook merged commit 870ed34 into v1 Dec 21, 2025
@productdevbook productdevbook deleted the feat/websocket-v1 branch December 21, 2025 18:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants