{"id":18797645,"url":"https://github.com/reconbot/graphql-lambda-subscriptions","last_synced_at":"2025-04-05T14:10:02.466Z","repository":{"id":37866265,"uuid":"393498038","full_name":"reconbot/graphql-lambda-subscriptions","owner":"reconbot","description":"Graphql-WS compatible Lambda Powered Subscriptions","archived":false,"fork":false,"pushed_at":"2025-04-02T19:28:42.000Z","size":6105,"stargazers_count":50,"open_issues_count":39,"forks_count":13,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-04-04T18:11:33.924Z","etag":null,"topics":["api-gateway","aws-lambda","dynamodb","graphql","graphql-subscriptions","serverless","serverless-framework"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/graphql-lambda-subscriptions","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/reconbot.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-08-06T20:41:47.000Z","updated_at":"2025-04-02T11:28:24.000Z","dependencies_parsed_at":"2024-03-10T18:29:38.122Z","dependency_job_id":"a9f1adf5-2def-477d-a788-53baebf89cbd","html_url":"https://github.com/reconbot/graphql-lambda-subscriptions","commit_stats":{"total_commits":1553,"total_committers":5,"mean_commits":310.6,"dds":"0.11332904056664517","last_synced_commit":"66215a2c658c0c84fac14049c5a7f0670d538d65"},"previous_names":[],"tags_count":37,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reconbot%2Fgraphql-lambda-subscriptions","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reconbot%2Fgraphql-lambda-subscriptions/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reconbot%2Fgraphql-lambda-subscriptions/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reconbot%2Fgraphql-lambda-subscriptions/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/reconbot","download_url":"https://codeload.github.com/reconbot/graphql-lambda-subscriptions/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247226214,"owners_count":20904465,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api-gateway","aws-lambda","dynamodb","graphql","graphql-subscriptions","serverless","serverless-framework"],"created_at":"2024-11-07T22:08:59.351Z","updated_at":"2025-04-05T14:10:02.433Z","avatar_url":"https://github.com/reconbot.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Graphql Lambda Subscriptions\n\n[![Release](https://github.com/reconbot/graphql-lambda-subscriptions/actions/workflows/test.yml/badge.svg)](https://github.com/reconbot/graphql-lambda-subscriptions/actions/workflows/test.yml)\n\nAmazon Lambda Powered GraphQL Subscriptions. This is an Amazon Lambda Serverless equivalent to [`graphql-ws`](https://github.com/enisdenjo/graphql-ws). It follows the [`graphql-ws prototcol`](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md). It is tested with the [Architect Sandbox](https://arc.codes/docs/en/reference/cli/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.\n\nAs `subscriptionless`'s tagline goes;\n\n\u003e Have all the functionality of GraphQL subscriptions on a stateful server without the cost.\n\n## Why a fork?\n\nI had different requirements and needed more features. This project wouldn't exist without [`subscriptionless`](https://github.com/andyrichardson/subscriptionless) and you should totally check it out.\n\n## Features\n\n- Only needs DynamoDB, API Gateway and Lambda (no [app sync](https://www.serverless.com/aws-appsync#benefits) or other managed graphql platform required, can use step functions for ping/pong support)\n- Provides a Pub/Sub system to broadcast events to subscriptions\n- Provides hooks for the full lifecycle of a subscription\n- Type compatible with GraphQL and [`nexus.js`](https://nexusjs.org)\n- Optional Logging\n\n## Quick Start\n\nSince there are many ways to deploy to amazon lambda I'm going to have to get opinionated in the quick start and pick [Architect](https://arc.codes). `graphql-lambda-subscriptions` should work on Lambda regardless of your deployment and packaging framework. Take a look at the [arc-basic-events](mocks/arc-basic-events) mock used for integration testing for an example of using it with Architect.\n\n## API Docs\n\nCan be found in our [docs folder](docs/README.md). You'll want to start with [`makeServer()`](docs/README.md#makeserver) and [`subscribe()`](dosc/README.md#subscribe).\n\n## Setup\n\n### Create a graphql-lambda-subscriptions server\n\n```ts\nimport { makeServer } from 'graphql-lambda-subscriptions'\n\n// define a schema and create a configured DynamoDB instance from aws-sdk\n// and make a schema with resolvers (maybe look at) '@graphql-tools/schema\n\nconst subscriptionServer = makeServer({\n  dynamodb,\n  schema,\n})\n```\n\n### Export the handler\n\n```ts\nexport const handler = subscriptionServer.webSocketHandler\n```\n\n### Configure API Gateway\n\nSet up API Gateway to route WebSocket events to the exported handler.\n\n\n\u003cdetails\u003e\n\u003csummary\u003e📖  Architect Example\u003c/summary\u003e\n\n```arc\n@app\nbasic-events\n\n@ws\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e📖  Serverless Framework Example\u003c/summary\u003e\n\n```yaml\nfunctions:\n  websocket:\n    name: my-subscription-lambda\n    handler: ./handler.handler\n    events:\n      - websocket:\n          route: $connect\n      - websocket:\n          route: $disconnect\n      - websocket:\n          route: $default\n```\n\n\u003c/details\u003e\n\n### Create DynamoDB tables for state\n\nIn-flight connections and subscriptions need to be persisted.\n\n\n#### Changing DynamoDB table names\n\nUse the `tableNames` argument to override the default table names.\n\n```ts\nconst instance = makeServer({\n  /* ... */\n  tableNames: {\n    connections: 'my_connections',\n    subscriptions: 'my_subscriptions',\n  },\n})\n\n// or use an async function to retrieve the names\n\nconst fetchTableNames = async () =\u003e {\n  // do some work to get your table names\n  return {\n    connections,\n    subscriptions,\n  }\n}\nconst instance = makeServer({\n  /* ... */\n  tableNames: fetchTableNames(),\n})\n\n```\n\n\u003cdetails\u003e\n\n\u003csummary\u003e💾 Architect Example\u003c/summary\u003e\n\n```arc\n@tables\nConnection\n  id *String\n  ttl TTL\nSubscription\n  id *String\n  ttl TTL\n\n@indexes\n\nSubscription\n  connectionId *String\n  name ConnectionIndex\n\nSubscription\n  topic *String\n  name TopicIndex\n```\n\n```ts\nimport { tables as arcTables } from '@architect/functions'\n\nconst fetchTableNames = async () =\u003e {\n  const tables = await arcTables()\n\n  const ensureName = (table) =\u003e {\n    const actualTableName = tables.name(table)\n    if (!actualTableName) {\n      throw new Error(`No table found for ${table}`)\n    }\n    return actualTableName\n  }\n\n  return {\n    connections: ensureName('Connection'),\n    subscriptions: ensureName('Subscription'),\n  }\n}\n\nconst subscriptionServer = makeServer({\n  dynamodb: tables._db,\n  schema,\n  tableNames: fetchTableNames(),\n})\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e💾 Serverless Framework Example\u003c/summary\u003e\n\n```yaml\nresources:\n  Resources:\n    # Table for tracking connections\n    connectionsTable:\n      Type: AWS::DynamoDB::Table\n      Properties:\n        TableName: ${self:provider.environment.CONNECTIONS_TABLE}\n        AttributeDefinitions:\n          - AttributeName: id\n            AttributeType: S\n        KeySchema:\n          - AttributeName: id\n            KeyType: HASH\n        ProvisionedThroughput:\n          ReadCapacityUnits: 1\n          WriteCapacityUnits: 1\n    # Table for tracking subscriptions\n    subscriptionsTable:\n      Type: AWS::DynamoDB::Table\n      Properties:\n        TableName: ${self:provider.environment.SUBSCRIPTIONS_TABLE}\n        AttributeDefinitions:\n          - AttributeName: id\n            AttributeType: S\n          - AttributeName: topic\n            AttributeType: S\n          - AttributeName: connectionId\n            AttributeType: S\n        KeySchema:\n          - AttributeName: id\n            KeyType: HASH\n        GlobalSecondaryIndexes:\n          - IndexName: ConnectionIndex\n            KeySchema:\n              - AttributeName: connectionId\n                KeyType: HASH\n            Projection:\n              ProjectionType: ALL\n            ProvisionedThroughput:\n              ReadCapacityUnits: 1\n              WriteCapacityUnits: 1\n          - IndexName: TopicIndex\n            KeySchema:\n              - AttributeName: topic\n                KeyType: HASH\n            Projection:\n              ProjectionType: ALL\n            ProvisionedThroughput:\n              ReadCapacityUnits: 1\n              WriteCapacityUnits: 1\n        ProvisionedThroughput:\n          ReadCapacityUnits: 1\n          WriteCapacityUnits: 1\n```\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e💾 terraform example\u003c/summary\u003e\n\n```tf\nresource \"aws_dynamodb_table\" \"connections-table\" {\n  name           = \"graphql_connections\"\n  billing_mode   = \"PROVISIONED\"\n  read_capacity  = 1\n  write_capacity = 1\n  hash_key = \"id\"\n\n  attribute {\n    name = \"id\"\n    type = \"S\"\n  }\n}\n\nresource \"aws_dynamodb_table\" \"subscriptions-table\" {\n  name           = \"graphql_subscriptions\"\n  billing_mode   = \"PROVISIONED\"\n  read_capacity  = 1\n  write_capacity = 1\n  hash_key = \"id\"\n\n  attribute {\n    name = \"id\"\n    type = \"S\"\n  }\n\n  attribute {\n    name = \"topic\"\n    type = \"S\"\n  }\n\n  attribute {\n    name = \"connectionId\"\n    type = \"S\"\n  }\n\n  global_secondary_index {\n    name               = \"ConnectionIndex\"\n    hash_key           = \"connectionId\"\n    write_capacity     = 1\n    read_capacity      = 1\n    projection_type    = \"ALL\"\n  }\n\n  global_secondary_index {\n    name               = \"TopicIndex\"\n    hash_key           = \"topic\"\n    write_capacity     = 1\n    read_capacity      = 1\n    projection_type    = \"ALL\"\n  }\n}\n```\n\n\u003c/details\u003e\n\n### PubSub\n\n`graphql-lambda-subscriptions` uses it's own _PubSub_ implementation.\n\n#### Subscribing to Topics\n\nUse the [`subscribe`](docs/README.md#subscribe) function to associate incoming subscriptions with a topic.\n\n```ts\nimport { subscribe } from 'graphql-lambda-subscriptions'\n\nexport const resolver = {\n  Subscribe: {\n    mySubscription: {\n      subscribe: subscribe('MY_TOPIC'),\n      resolve: (event, args, context) =\u003e {/* ... */}\n    }\n  }\n}\n```\n\n\u003cdetails\u003e\n\n\u003csummary\u003e📖 Filtering events\u003c/summary\u003e\n\nUse the [`subscribe`](docs/README.md#subscribe) with [`SubscribeOptions`](docs/interfaces/SubscribeOptions.md) to allow for filtering.\n\n\u003e Note: If a function is provided, it will be called **on subscription start** and must return a serializable object.\n\n```ts\nimport { subscribe } from 'graphql-lambda-subscriptions'\n\n// Subscription agnostic filter\nsubscribe('MY_TOPIC', {\n  filter: {\n    attr1: '`attr1` must have this value',\n    attr2: {\n      attr3: 'Nested attributes work fine',\n    },\n  }\n})\n\n// Subscription specific filter\nsubscribe('MY_TOPIC',{\n  filter: (root, args, context, info) =\u003e ({\n    userId: args.userId,\n  }),\n})\n```\n\n\u003c/details\u003e\n\n#### Publishing events\n\nUse the [`publish()`](docs/interfaces/SubscriptionServer.md#publish) function on your graphql-lambda-subscriptions server to publish events to active subscriptions. Payloads must be of type `Record\u003cstring, any\u003e` so they can be filtered and stored.\n\n```ts\nsubscriptionServer.publish({\n  topic: 'MY_TOPIC',\n  payload: {\n    message: 'Hey!',\n  },\n})\n```\n\nEvents can come from many sources\n\n```ts\n// SNS Event\nexport const snsHandler = (event) =\u003e\n  Promise.all(\n    event.Records.map((r) =\u003e\n      subscriptionServer.publish({\n        topic: r.Sns.TopicArn.substring(r.Sns.TopicArn.lastIndexOf(':') + 1), // Get topic name (e.g. \"MY_TOPIC\")\n        payload: JSON.parse(r.Sns.Message),\n      })\n    )\n  )\n\n// Manual Invocation\nexport const invocationHandler = (payload) =\u003e subscriptionServer.publish({ topic: 'MY_TOPIC', payload })\n```\n\n#### Completing Subscriptions\n\nUse the `complete` on your graphql-lambda-subscriptions server to complete active subscriptions. Payloads are optional and match against filters like events do.\n\n```ts\nsubscriptionServer.complete({\n  topic: 'MY_TOPIC',\n  // optional payload\n  payload: {\n    message: 'Hey!',\n  },\n})\n```\n\n### Context\n\n[Context](docs/interfaces/ServerArgs.md#context) is provided on the [`ServerArgs`](docs/interfaces/ServerArgs.md) object when creating a server. The values are accessible in all callback and resolver functions (eg. `resolve`, `filter`, `onAfterSubscribe`, `onSubscribe` and `onComplete`).\n\nAssuming no `context` argument is provided when creating the server, the default value is an object with `connectionInitPayload`, `connectionId` properties and the [`publish()`](docs/interfaces/SubscriptionServer.md#publish) and [`complete()`](docs/interfaces/SubscriptionServer.md#complete) functions. These properties are merged into a provided object or passed into a provided function.\n\n#### Setting static context value\n\nAn object can be provided via the `context` attribute when calling `makeServer`.\n\n```ts\nconst instance = makeServer({\n  /* ... */\n  context: {\n    myAttr: 'hello',\n  },\n})\n```\n\nThe default values (above) will be appended to this object prior to execution.\n\n#### Setting dynamic context value\n\nA function (optionally async) can be provided via the `context` attribute when calling `makeServer`.\n\nThe default context value is passed as an argument.\n\n```ts\nconst instance = makeServer({\n  /* ... */\n  context: ({ connectionInitPayload }) =\u003e ({\n    myAttr: 'hello',\n    user: connectionInitPayload.user,\n  }),\n})\n```\n\n#### Using the context\n\n```ts\nexport const resolver = {\n  Subscribe: {\n    mySubscription: {\n      subscribe: subscribe('GREETINGS', {\n        filter(_, _, context) {\n          console.log(context.connectionId) // the connectionId\n        },\n        async onAfterSubscribe(_, _, { connectionId, publish }) {\n          await publish('GREETINGS', { message: `HI from ${connectionId}!` })\n        }\n      })\n      resolve: (event, args, context) =\u003e {\n        console.log(context.connectionInitPayload) // payload from connection_init\n        return event.payload.message\n      },\n    },\n  },\n}\n```\n\n### Side effects\n\nSide effect handlers can be declared on subscription fields to handle `onSubscribe` (start) and `onComplete` (stop) events.\n\n\u003cdetails\u003e\n\n\u003csummary\u003e📖 Adding side-effect handlers\u003c/summary\u003e\n\n```ts\nexport const resolver = {\n  Subscribe: {\n    mySubscription: {\n      resolve: (event, args, context) =\u003e {\n        /* ... */\n      },\n      subscribe: subscribe('MY_TOPIC', {\n        // filter?: object | ((...args: SubscribeArgs) =\u003e object)\n        // onSubscribe?: (...args: SubscribeArgs) =\u003e void | Promise\u003cvoid\u003e\n        // onComplete?: (...args: SubscribeArgs) =\u003e void | Promise\u003cvoid\u003e\n        // onAfterSubscribe?: (...args: SubscribeArgs) =\u003e PubSubEvent | Promise\u003cPubSubEvent\u003e | undefined | Promise\u003cundefined\u003e\n      }),\n    },\n  },\n}\n```\n\n\u003c/details\u003e\n\n### Events\n\nGlobal events can be provided when calling `makeServer` to track the execution cycle of the lambda.\n\n\u003cdetails\u003e\n\n\u003csummary\u003e📖 Connect (onConnect)\u003c/summary\u003e\n\nCalled when a WebSocket connection is first established.\n\n```ts\nconst instance = makeServer({\n  /* ... */\n  onConnect: ({ event }) =\u003e {\n    /* */\n  },\n})\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\n\u003csummary\u003e📖 Disconnect (onDisconnect)\u003c/summary\u003e\n\nCalled when a WebSocket connection is disconnected.\n\n```ts\nconst instance = makeServer({\n  /* ... */\n  onDisconnect: ({ event }) =\u003e {\n    /* */\n  },\n})\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\n\u003csummary\u003e📖 Authorization (connection_init)\u003c/summary\u003e\n\n`onConnectionInit` can be used to verify the `connection_init` payload prior to persistence.\n\n\u003e **Note:** Any sensitive data in the incoming message should be removed at this stage.\n\n```ts\nconst instance = makeServer({\n  /* ... */\n  onConnectionInit: ({ message }) =\u003e {\n    const token = message.payload.token\n\n    if (!myValidation(token)) {\n      throw Error('Token validation failed')\n    }\n\n    // Prevent sensitive data from being written to DB\n    return {\n      ...message.payload,\n      token: undefined,\n    }\n  },\n})\n```\n\nBy default, the (optionally parsed) payload will be accessible via [context](#context).\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\n\u003csummary\u003e📖 Subscribe (onSubscribe)\u003c/summary\u003e\n\n#### Subscribe (onSubscribe)\n\nCalled when any subscription message is received.\n\n```ts\nconst instance = makeServer({\n  /* ... */\n  onSubscribe: ({ event, message }) =\u003e {\n    /* */\n  },\n})\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\n\u003csummary\u003e📖 Complete (onComplete)\u003c/summary\u003e\n\nCalled when any complete message is received.\n\n```ts\nconst instance = makeServer({\n  /* ... */\n  onComplete: ({ event, message }) =\u003e {\n    /* */\n  },\n})\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\n\u003csummary\u003e📖 Error (onError)\u003c/summary\u003e\n\nCalled when any error is encountered\n\n```ts\nconst instance = makeServer({\n  /* ... */\n  onError: (error, context) =\u003e {\n    /* */\n  },\n})\n```\n\n\u003c/details\u003e\n\n## Caveats\n\n### Ping/Pong\n\nFor whatever reason, AWS API Gateway does not support WebSocket protocol level ping/pong. So you can use Step Functions to do this. See [`pingPong`](docs/interfaces/ServerArgs.md#pingpong).\n\n### Socket idleness\n\nAPI Gateway considers an idle connection to be one where no messages have been sent on the socket for a fixed duration [(currently 10 minutes)](https://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html#apigateway-execution-service-websocket-limits-table). 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.\n\n### Socket Close Reasons\n\nAPI Gateway doesn't support custom reasons or codes for WebSockets being closed. So the codes and reason strings wont match `graphql-ws`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Freconbot%2Fgraphql-lambda-subscriptions","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Freconbot%2Fgraphql-lambda-subscriptions","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Freconbot%2Fgraphql-lambda-subscriptions/lists"}