Installations
npm install graphql-lambda-subscriptions-fix
Developer Guide
Typescript
Yes
Module System
CommonJS, ESM
Min. Node Version
^14.13 || >=16
Node Version
18.18.2
NPM Version
9.8.1
Score
67
Supply Chain
98.7
Quality
78.6
Maintenance
100
Vulnerability
99.6
License
Releases
Contributors
Unable to fetch Contributors
Languages
TypeScript (96.82%)
JavaScript (2.94%)
Arc (0.24%)
Love this project? Help keep it running — sponsor us today! 🚀
Developer
Download Statistics
Total Downloads
57,989
Last Day
69
Last Week
682
Last Month
2,978
Last Year
41,253
GitHub Statistics
NOASSERTION License
50 Stars
1,574 Commits
14 Forks
3 Watchers
22 Branches
3 Contributors
Updated on Feb 11, 2025
Bundle Size
69.33 kB
Minified
19.33 kB
Minified + Gzipped
Package Meta Information
Latest Version
0.0.2-development
Package Id
graphql-lambda-subscriptions-fix@0.0.2-development
Unpacked Size
166.80 kB
Size
39.63 kB
File Count
13
NPM Version
9.8.1
Node Version
18.18.2
Published on
Dec 15, 2024
Total Downloads
Cumulative downloads
Total Downloads
57,989
Last Day
-64.6%
69
Compared to previous day
Last Week
-16.4%
682
Compared to previous week
Last Month
-26.8%
2,978
Compared to previous month
Last Year
170.6%
41,253
Compared to previous year
Daily Downloads
Weekly Downloads
Monthly Downloads
Yearly Downloads
Dependencies
2
Peer Dependencies
4
Dev Dependencies
33
Graphql Lambda Subscriptions
Amazon Lambda Powered GraphQL Subscriptions. This is an Amazon Lambda Serverless equivalent to graphql-ws
. It follows the graphql-ws prototcol
. It is tested with the Architect Sandbox against graphql-ws
directly and run in production today. For many applications graphql-lambda-subscriptions
should do what graphql-ws
does for you today without having to run a server. This started as fork of subscriptionless
another library with similar goals.
As subscriptionless
's tagline goes;
Have all the functionality of GraphQL subscriptions on a stateful server without the cost.
Why a fork?
I had different requirements and needed more features. This project wouldn't exist without subscriptionless
and you should totally check it out.
Features
- Only needs DynamoDB, API Gateway and Lambda (no app sync or other managed graphql platform required, can use step functions for ping/pong support)
- Provides a Pub/Sub system to broadcast events to subscriptions
- Provides hooks for the full lifecycle of a subscription
- Type compatible with GraphQL and
nexus.js
- Optional Logging
Quick Start
Since there are many ways to deploy to amazon lambda I'm going to have to get opinionated in the quick start and pick Architect. graphql-lambda-subscriptions
should work on Lambda regardless of your deployment and packaging framework. Take a look at the arc-basic-events mock used for integration testing for an example of using it with Architect.
API Docs
Can be found in our docs folder. You'll want to start with makeServer()
and subscribe()
.
Setup
Create a graphql-lambda-subscriptions server
1import { makeServer } from 'graphql-lambda-subscriptions' 2 3// define a schema and create a configured DynamoDB instance from aws-sdk 4// and make a schema with resolvers (maybe look at) '@graphql-tools/schema 5 6const subscriptionServer = makeServer({ 7 dynamodb, 8 schema, 9})
Export the handler
1export const handler = subscriptionServer.webSocketHandler
Configure API Gateway
Set up API Gateway to route WebSocket events to the exported handler.
📖 Architect Example
1@app 2basic-events 3 4@ws
📖 Serverless Framework Example
1functions: 2 websocket: 3 name: my-subscription-lambda 4 handler: ./handler.handler 5 events: 6 - websocket: 7 route: $connect 8 - websocket: 9 route: $disconnect 10 - websocket: 11 route: $default
Create DynamoDB tables for state
In-flight connections and subscriptions need to be persisted.
Changing DynamoDB table names
Use the tableNames
argument to override the default table names.
1const instance = makeServer({ 2 /* ... */ 3 tableNames: { 4 connections: 'my_connections', 5 subscriptions: 'my_subscriptions', 6 }, 7}) 8 9// or use an async function to retrieve the names 10 11const fetchTableNames = async () => { 12 // do some work to get your table names 13 return { 14 connections, 15 subscriptions, 16 } 17} 18const instance = makeServer({ 19 /* ... */ 20 tableNames: fetchTableNames(), 21}) 22
💾 Architect Example
1@tables 2Connection 3 id *String 4 ttl TTL 5Subscription 6 id *String 7 ttl TTL 8 9@indexes 10 11Subscription 12 connectionId *String 13 name ConnectionIndex 14 15Subscription 16 topic *String 17 name TopicIndex
1import { tables as arcTables } from '@architect/functions' 2 3const fetchTableNames = async () => { 4 const tables = await arcTables() 5 6 const ensureName = (table) => { 7 const actualTableName = tables.name(table) 8 if (!actualTableName) { 9 throw new Error(`No table found for ${table}`) 10 } 11 return actualTableName 12 } 13 14 return { 15 connections: ensureName('Connection'), 16 subscriptions: ensureName('Subscription'), 17 } 18} 19 20const subscriptionServer = makeServer({ 21 dynamodb: tables._db, 22 schema, 23 tableNames: fetchTableNames(), 24})
💾 Serverless Framework Example
1resources: 2 Resources: 3 # Table for tracking connections 4 connectionsTable: 5 Type: AWS::DynamoDB::Table 6 Properties: 7 TableName: ${self:provider.environment.CONNECTIONS_TABLE} 8 AttributeDefinitions: 9 - AttributeName: id 10 AttributeType: S 11 KeySchema: 12 - AttributeName: id 13 KeyType: HASH 14 ProvisionedThroughput: 15 ReadCapacityUnits: 1 16 WriteCapacityUnits: 1 17 # Table for tracking subscriptions 18 subscriptionsTable: 19 Type: AWS::DynamoDB::Table 20 Properties: 21 TableName: ${self:provider.environment.SUBSCRIPTIONS_TABLE} 22 AttributeDefinitions: 23 - AttributeName: id 24 AttributeType: S 25 - AttributeName: topic 26 AttributeType: S 27 - AttributeName: connectionId 28 AttributeType: S 29 KeySchema: 30 - AttributeName: id 31 KeyType: HASH 32 GlobalSecondaryIndexes: 33 - IndexName: ConnectionIndex 34 KeySchema: 35 - AttributeName: connectionId 36 KeyType: HASH 37 Projection: 38 ProjectionType: ALL 39 ProvisionedThroughput: 40 ReadCapacityUnits: 1 41 WriteCapacityUnits: 1 42 - IndexName: TopicIndex 43 KeySchema: 44 - AttributeName: topic 45 KeyType: HASH 46 Projection: 47 ProjectionType: ALL 48 ProvisionedThroughput: 49 ReadCapacityUnits: 1 50 WriteCapacityUnits: 1 51 ProvisionedThroughput: 52 ReadCapacityUnits: 1 53 WriteCapacityUnits: 1
💾 terraform example
1resource "aws_dynamodb_table" "connections-table" { 2 name = "graphql_connections" 3 billing_mode = "PROVISIONED" 4 read_capacity = 1 5 write_capacity = 1 6 hash_key = "id" 7 8 attribute { 9 name = "id" 10 type = "S" 11 } 12} 13 14resource "aws_dynamodb_table" "subscriptions-table" { 15 name = "graphql_subscriptions" 16 billing_mode = "PROVISIONED" 17 read_capacity = 1 18 write_capacity = 1 19 hash_key = "id" 20 21 attribute { 22 name = "id" 23 type = "S" 24 } 25 26 attribute { 27 name = "topic" 28 type = "S" 29 } 30 31 attribute { 32 name = "connectionId" 33 type = "S" 34 } 35 36 global_secondary_index { 37 name = "ConnectionIndex" 38 hash_key = "connectionId" 39 write_capacity = 1 40 read_capacity = 1 41 projection_type = "ALL" 42 } 43 44 global_secondary_index { 45 name = "TopicIndex" 46 hash_key = "topic" 47 write_capacity = 1 48 read_capacity = 1 49 projection_type = "ALL" 50 } 51}
PubSub
graphql-lambda-subscriptions
uses it's own PubSub implementation.
Subscribing to Topics
Use the subscribe
function to associate incoming subscriptions with a topic.
1import { subscribe } from 'graphql-lambda-subscriptions' 2 3export const resolver = { 4 Subscribe: { 5 mySubscription: { 6 subscribe: subscribe('MY_TOPIC'), 7 resolve: (event, args, context) => {/* ... */} 8 } 9 } 10}
📖 Filtering events
Use the subscribe
with SubscribeOptions
to allow for filtering.
Note: If a function is provided, it will be called on subscription start and must return a serializable object.
1import { subscribe } from 'graphql-lambda-subscriptions' 2 3// Subscription agnostic filter 4subscribe('MY_TOPIC', { 5 filter: { 6 attr1: '`attr1` must have this value', 7 attr2: { 8 attr3: 'Nested attributes work fine', 9 }, 10 } 11}) 12 13// Subscription specific filter 14subscribe('MY_TOPIC',{ 15 filter: (root, args, context, info) => ({ 16 userId: args.userId, 17 }), 18})
Publishing events
Use the publish()
function on your graphql-lambda-subscriptions server to publish events to active subscriptions. Payloads must be of type Record<string, any>
so they can be filtered and stored.
1subscriptionServer.publish({ 2 topic: 'MY_TOPIC', 3 payload: { 4 message: 'Hey!', 5 }, 6})
Events can come from many sources
1// SNS Event
2export const snsHandler = (event) =>
3 Promise.all(
4 event.Records.map((r) =>
5 subscriptionServer.publish({
6 topic: r.Sns.TopicArn.substring(r.Sns.TopicArn.lastIndexOf(':') + 1), // Get topic name (e.g. "MY_TOPIC")
7 payload: JSON.parse(r.Sns.Message),
8 })
9 )
10 )
11
12// Manual Invocation
13export const invocationHandler = (payload) => subscriptionServer.publish({ topic: 'MY_TOPIC', payload })
Completing Subscriptions
Use the complete
on your graphql-lambda-subscriptions server to complete active subscriptions. Payloads are optional and match against filters like events do.
1subscriptionServer.complete({ 2 topic: 'MY_TOPIC', 3 // optional payload 4 payload: { 5 message: 'Hey!', 6 }, 7})
Context
Context is provided on the ServerArgs
object when creating a server. The values are accessible in all callback and resolver functions (eg. resolve
, filter
, onAfterSubscribe
, onSubscribe
and onComplete
).
Assuming no context
argument is provided when creating the server, the default value is an object with connectionInitPayload
, connectionId
properties and the publish()
and complete()
functions. These properties are merged into a provided object or passed into a provided function.
Setting static context value
An object can be provided via the context
attribute when calling makeServer
.
1const instance = makeServer({ 2 /* ... */ 3 context: { 4 myAttr: 'hello', 5 }, 6})
The default values (above) will be appended to this object prior to execution.
Setting dynamic context value
A function (optionally async) can be provided via the context
attribute when calling makeServer
.
The default context value is passed as an argument.
1const instance = makeServer({ 2 /* ... */ 3 context: ({ connectionInitPayload }) => ({ 4 myAttr: 'hello', 5 user: connectionInitPayload.user, 6 }), 7})
Using the context
1export const resolver = { 2 Subscribe: { 3 mySubscription: { 4 subscribe: subscribe('GREETINGS', { 5 filter(_, _, context) { 6 console.log(context.connectionId) // the connectionId 7 }, 8 async onAfterSubscribe(_, _, { connectionId, publish }) { 9 await publish('GREETINGS', { message: `HI from ${connectionId}!` }) 10 } 11 }) 12 resolve: (event, args, context) => { 13 console.log(context.connectionInitPayload) // payload from connection_init 14 return event.payload.message 15 }, 16 }, 17 }, 18}
Side effects
Side effect handlers can be declared on subscription fields to handle onSubscribe
(start) and onComplete
(stop) events.
📖 Adding side-effect handlers
1export const resolver = { 2 Subscribe: { 3 mySubscription: { 4 resolve: (event, args, context) => { 5 /* ... */ 6 }, 7 subscribe: subscribe('MY_TOPIC', { 8 // filter?: object | ((...args: SubscribeArgs) => object) 9 // onSubscribe?: (...args: SubscribeArgs) => void | Promise<void> 10 // onComplete?: (...args: SubscribeArgs) => void | Promise<void> 11 // onAfterSubscribe?: (...args: SubscribeArgs) => PubSubEvent | Promise<PubSubEvent> | undefined | Promise<undefined> 12 }), 13 }, 14 }, 15}
Events
Global events can be provided when calling makeServer
to track the execution cycle of the lambda.
📖 Connect (onConnect)
Called when a WebSocket connection is first established.
1const instance = makeServer({ 2 /* ... */ 3 onConnect: ({ event }) => { 4 /* */ 5 }, 6})
📖 Disconnect (onDisconnect)
Called when a WebSocket connection is disconnected.
1const instance = makeServer({ 2 /* ... */ 3 onDisconnect: ({ event }) => { 4 /* */ 5 }, 6})
📖 Authorization (connection_init)
onConnectionInit
can be used to verify the connection_init
payload prior to persistence.
Note: Any sensitive data in the incoming message should be removed at this stage.
1const instance = makeServer({ 2 /* ... */ 3 onConnectionInit: ({ message }) => { 4 const token = message.payload.token 5 6 if (!myValidation(token)) { 7 throw Error('Token validation failed') 8 } 9 10 // Prevent sensitive data from being written to DB 11 return { 12 ...message.payload, 13 token: undefined, 14 } 15 }, 16})
By default, the (optionally parsed) payload will be accessible via context.
📖 Subscribe (onSubscribe)
Subscribe (onSubscribe)
Called when any subscription message is received.
1const instance = makeServer({ 2 /* ... */ 3 onSubscribe: ({ event, message }) => { 4 /* */ 5 }, 6})
📖 Complete (onComplete)
Called when any complete message is received.
1const instance = makeServer({ 2 /* ... */ 3 onComplete: ({ event, message }) => { 4 /* */ 5 }, 6})
📖 Error (onError)
Called when any error is encountered
1const instance = makeServer({ 2 /* ... */ 3 onError: (error, context) => { 4 /* */ 5 }, 6})
Caveats
Ping/Pong
For whatever reason, AWS API Gateway does not support WebSocket protocol level ping/pong. So you can use Step Functions to do this. See pingPong
.
Socket idleness
API Gateway considers an idle connection to be one where no messages have been sent on the socket for a fixed duration (currently 10 minutes). The WebSocket spec has support for detecting idle connections (ping/pong) but API Gateway doesn't use it. This means, in the case where both parties are connected, and no message is sent on the socket for the defined duration (direction agnostic), API Gateway will close the socket. A fix for this is to set up immediate reconnection on the client side.
Socket Close Reasons
API Gateway doesn't support custom reasons or codes for WebSockets being closed. So the codes and reason strings wont match graphql-ws
.

No vulnerabilities found.
Reason
no dangerous workflow patterns detected
Reason
30 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
Reason
no binaries found in the repo
Reason
security policy file detected
Details
- Info: security policy file detected: SECURITY.md:1
- Info: Found linked content: SECURITY.md:1
- Warn: One or no descriptive hints of disclosure, vulnerability, and/or timelines in security policy
- Info: Found text in security policy: SECURITY.md:1
Reason
license file detected
Details
- Info: project has a license file: LICENSE:0
- Warn: project license file does not contain an FSF or OSI license.
Reason
dependency not pinned by hash detected -- score normalized to 3
Details
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/release.yml:11: update your workflow using https://app.stepsecurity.io/secureworkflow/reconbot/graphql-lambda-subscriptions/release.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/release.yml:12: update your workflow using https://app.stepsecurity.io/secureworkflow/reconbot/graphql-lambda-subscriptions/release.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test.yml:10: update your workflow using https://app.stepsecurity.io/secureworkflow/reconbot/graphql-lambda-subscriptions/test.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test.yml:11: update your workflow using https://app.stepsecurity.io/secureworkflow/reconbot/graphql-lambda-subscriptions/test.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test.yml:21: update your workflow using https://app.stepsecurity.io/secureworkflow/reconbot/graphql-lambda-subscriptions/test.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test.yml:22: update your workflow using https://app.stepsecurity.io/secureworkflow/reconbot/graphql-lambda-subscriptions/test.yml/main?enable=pin
- Info: 0 out of 6 GitHub-owned GitHubAction dependencies pinned
- Info: 3 out of 3 npmCommand dependencies pinned
Reason
Found 0/30 approved changesets -- score normalized to 0
Reason
detected GitHub workflow tokens with excessive permissions
Details
- Warn: no topLevel permission defined: .github/workflows/release.yml:1
- Warn: no topLevel permission defined: .github/workflows/test.yml:1
- Info: no jobLevel write permissions found
Reason
no SAST tool detected
Details
- Warn: no pull requests merged into dev branch
Reason
no effort to earn an OpenSSF best practices badge detected
Reason
project is not fuzzed
Details
- Warn: no fuzzer integrations found
Reason
branch protection not enabled on development/release branches
Details
- Warn: branch protection not enabled for branch 'main'
Reason
12 existing vulnerabilities detected
Details
- Warn: Project is vulnerable to: GHSA-grv7-fg5c-xmjg
- Warn: Project is vulnerable to: GHSA-pxg6-pf52-xh8x
- Warn: Project is vulnerable to: GHSA-3xgq-45jj-v275
- Warn: Project is vulnerable to: GHSA-jchw-25xp-jwwc
- Warn: Project is vulnerable to: GHSA-cxjh-pqwp-8mfp
- Warn: Project is vulnerable to: GHSA-952p-6rrq-rcjv
- Warn: Project is vulnerable to: GHSA-9wv6-86v2-598j
- Warn: Project is vulnerable to: GHSA-rhx6-c78j-4q9w
- Warn: Project is vulnerable to: GHSA-c2qf-rxjj-qqgw
- Warn: Project is vulnerable to: GHSA-m6fv-jmcg-4jfg
- Warn: Project is vulnerable to: GHSA-f5x3-32g6-xq36
- Warn: Project is vulnerable to: GHSA-3h5v-q93c-6h6q
Score
4.2
/10
Last Scanned on 2025-02-03
The Open Source Security Foundation is a cross-industry collaboration to improve the security of open source software (OSS). The Scorecard provides security health metrics for open source projects.
Learn More