Subscriptions enable real-time updates from your GraphQL API. They fetch initial data and keep results up-to-date through WebSocket connections.
subscription {
userUpdated(id: 1) {
id
name
status
}
}
Subscription Structure
A subscription consists of five elements:
- type -
subscription (required)
- name - Optional subscription name
- variables - Optional variable definitions
- directives - Optional directives
- selection - One field from schema subscription fields
subscription OnUserUpdate($userId: ID!) {
userUpdated(id: $userId) {
id
status
}
}
Unlike queries and mutations, subscriptions can only have a single root field.
Defining Subscription Fields
Define subscription fields in your schema:
app/graphql/app_schema.rb
module GraphQL
class AppSchema < GraphQL::Schema
subscription_fields do
field(:user_updated, 'User') do
argument :id, :id, null: false
scope :current_user
subscribed do
puts "New subscription: #{subscription.id}"
end
end
field(:post_created, 'Post') do
scope ->(subscription) { subscription.context.current_user }
end
end
end
end
ActionCable Integration
Subscriptions work with ActionCable by default. Configure the provider in your schema:
app/graphql/app_schema.rb
module GraphQL
class AppSchema < GraphQL::Schema
# Default ActionCable configuration
config.subscription_provider =
Rails::GraphQL::Subscription::Provider::ActionCable.new(
cable: ::ActionCable,
prefix: 'graphql',
store: Rails::GraphQL::Subscription::Store::Memory.new,
logger: GraphQL::AppSchema.logger,
)
end
end
ActionCable Channel
Create a channel for GraphQL subscriptions:
app/channels/graphql_channel.rb
class GraphqlChannel < ApplicationCable::Channel
def subscribed
@subscription_ids = []
end
def execute(data)
result = GraphQL::AppSchema.execute(
data['query'],
variables: data['variables'],
context: { current_user: current_user, channel: self },
operation_name: data['operationName']
)
transmit(result)
end
def unsubscribed
@subscription_ids.each do |sid|
GraphQL::AppSchema.remove_subscriptions(sid)
end
end
end
Client Setup
Connect to subscriptions from your client:
import { createConsumer } from "@rails/actioncable"
const cable = createConsumer()
const subscription = cable.subscriptions.create("GraphqlChannel", {
connected() {
this.perform("execute", {
query: `
subscription {
userUpdated(id: 1) {
id
name
status
}
}
`
})
},
received(data) {
console.log("Received:", data)
}
})
Subscription Options
Scope
Define conditions for delivering updates:
subscription_fields do
field(:user_updated, 'User') do
argument :id, :id, null: false
# Scope to current user
scope :current_user
# Or use a proc
scope ->(subscription) {
subscription.request.context.current_user
}
# Or multiple scopes
scope :current_user, :system_version
end
end
Scopes are evaluated when:
- A subscription is created
- Updates are triggered
Subscribed Callback
Execute code when a subscription is created:
subscription_fields do
field(:user_updated, 'User') do
argument :id, :id, null: false
subscribed do
# Access subscription details
puts subscription.id
puts subscription.field
puts subscription.args
puts subscription.scope
puts subscription.context
# Perform side effects
SubscriptionLog.create!(
user_id: context.current_user.id,
subscription_id: subscription.id
)
end
end
end
Triggering Updates
Trigger subscription updates from anywhere in your application:
trigger Method
# Get the subscription field
field = GraphQL::AppSchema[:subscription][:user_updated]
# Trigger for all subscriptions
field.trigger
# Trigger for specific arguments
field.trigger(args: { id: 1 })
# Trigger for multiple arguments
field.trigger(args: [{ id: 1 }, { id: 2 }])
# Trigger for specific scope
field.trigger(scope: current_user)
# Combine arguments and scope
field.trigger(args: { id: 1 }, scope: current_user)
trigger_for Method
Trigger with automatic argument extraction:
field = GraphQL::AppSchema[:subscription][:user_updated]
# Trigger from a single object
user = User.find(1)
field.trigger_for(user) # Extracts { id: 1 }
# Trigger from multiple objects
users = User.where(active: true)
field.trigger_for(users) # Extracts [{ id: 1 }, { id: 2 }, ...]
# Disable prepared data
field.trigger_for(user, and_prepare: false)
trigger_for automatically prepares data so the subscription doesn’t need to reload records.
In Model Callbacks
class User < ApplicationRecord
after_update :trigger_subscription
private
def trigger_subscription
field = GraphQL::AppSchema[:subscription][:user_updated]
field.trigger_for(self)
end
end
In Controllers
app/controllers/users_controller.rb
class UsersController < ApplicationController
def update
@user = User.find(params[:id])
if @user.update(user_params)
field = GraphQL::AppSchema[:subscription][:user_updated]
field.trigger_for(@user)
render json: @user
else
render json: @user.errors, status: :unprocessable_entity
end
end
end
Broadcasting
By default, subscriptions with identical criteria are broadcasted together:
field(:user_updated, 'User') do
argument :id, :id, null: false
# Mark a field as not broadcastable
field :is_following, :bool, broadcastable: false
end
When is_following is requested, each subscriber gets a personalized response:
subscription {
userUpdated(id: 1) {
name # Broadcasted to all
isFollowing # Personalized per subscriber
}
}
Control broadcasting at the schema level:
app/graphql/app_schema.rb
module GraphQL
class AppSchema < GraphQL::Schema
config.default_subscription_broadcastable = false
end
end
Unsubscribing
Force remove subscriptions:
field = GraphQL::AppSchema[:subscription][:user_updated]
# Unsubscribe with same pattern as trigger
field.unsubscribe(args: { id: 1 })
field.unsubscribe(scope: current_user)
# Unsubscribe using objects
field.unsubscribe_from(user)
field.unsubscribe_from(User.all)
Clients receive a final update indicating no more data will arrive.
Subscription Object
Subscription objects store information needed for re-evaluation:
subscribed do
subscription.id # Unique identifier
subscription.sid # Alias for id
subscription.schema # Schema namespace
subscription.context # Request context
subscription.scope # Scope values
subscription.operation_id # Cached operation ID
subscription.origin # Subscription origin
subscription.field # Subscription field
subscription.args # Field arguments
subscription.broadcastable # Can be broadcasted?
subscription.created_at # Creation timestamp
subscription.updated_at # Last update timestamp
end
Subscription Providers
Providers handle the pub-sub architecture:
ActionCable Provider (Default)
config.subscription_provider =
Rails::GraphQL::Subscription::Provider::ActionCable.new(
cable: ::ActionCable,
prefix: 'graphql',
store: Rails::GraphQL::Subscription::Store::Memory.new,
logger: Rails.logger,
)
Memory Store
The default store keeps subscriptions in memory:
store = Rails::GraphQL::Subscription::Store::Memory.new
Memory store subscriptions are lost on server restart. Use a persistent store for production.
Subscription Examples
User Updates
subscription {
userUpdated(id: 1) {
id
name
status
lastSeen
}
}
New Posts
subscription {
postCreated {
id
title
author {
id
name
}
}
}
Filtered Events
subscription OnPostInCategory($categoryId: ID!) {
postCreated(categoryId: $categoryId) {
id
title
}
}
Cache Integration
Subscriptions rely on cache for operation storage:
app/graphql/app_schema.rb
module GraphQL
class AppSchema < GraphQL::Schema
# Configure cache
config.cache = Rails.cache
# Cache methods available
GraphQL::AppSchema.cached?('key')
GraphQL::AppSchema.read_from_cache('key')
GraphQL::AppSchema.write_on_cache('key', value)
GraphQL::AppSchema.delete_from_cache('key')
end
end
Best Practices
- Use scopes - Limit updates to relevant subscribers
- Trigger selectively - Only trigger when data actually changes
- Use trigger_for - Let Rails extract arguments automatically
- Control broadcasting - Mark personalized fields as not broadcastable
- Handle unsubscribe - Clean up resources when clients disconnect
- Monitor subscriptions - Track active subscriptions and performance
- Use persistent store - Don’t rely on memory store in production
- Secure subscriptions - Check authorization in subscribed callback