{"id":23492828,"url":"https://github.com/bubblydoo/graphql-workers-subscriptions","last_synced_at":"2025-10-22T10:44:33.793Z","repository":{"id":110913058,"uuid":"585095573","full_name":"bubblydoo/graphql-workers-subscriptions","owner":"bubblydoo","description":"Topic-based GraphQL subscriptions, with Cloudflare Workers, Durable Objects and D1","archived":false,"fork":false,"pushed_at":"2025-05-09T09:00:50.000Z","size":1153,"stargazers_count":73,"open_issues_count":2,"forks_count":6,"subscribers_count":5,"default_branch":"main","last_synced_at":"2025-10-22T10:44:32.587Z","etag":null,"topics":["cloudflare-d1","cloudflare-workers","durable-objects","graphql","graphql-subscriptions","websockets"],"latest_commit_sha":null,"homepage":"https://graphql-workers-subscriptions.bubblydoo.workers.dev/graphql","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bubblydoo.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":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2023-01-04T10:00:35.000Z","updated_at":"2025-08-20T13:15:31.000Z","dependencies_parsed_at":null,"dependency_job_id":"fc8b6d71-4d4a-4c08-9443-5a89c1b966e2","html_url":"https://github.com/bubblydoo/graphql-workers-subscriptions","commit_stats":null,"previous_names":["bubblydoo/graphql-workers-subscriptions"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/bubblydoo/graphql-workers-subscriptions","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bubblydoo%2Fgraphql-workers-subscriptions","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bubblydoo%2Fgraphql-workers-subscriptions/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bubblydoo%2Fgraphql-workers-subscriptions/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bubblydoo%2Fgraphql-workers-subscriptions/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bubblydoo","download_url":"https://codeload.github.com/bubblydoo/graphql-workers-subscriptions/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bubblydoo%2Fgraphql-workers-subscriptions/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":280424209,"owners_count":26328462,"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","status":"online","status_checked_at":"2025-10-22T02:00:06.515Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["cloudflare-d1","cloudflare-workers","durable-objects","graphql","graphql-subscriptions","websockets"],"created_at":"2024-12-25T02:14:48.187Z","updated_at":"2025-10-22T10:44:33.788Z","avatar_url":"https://github.com/bubblydoo.png","language":"TypeScript","readme":"# Cloudflare Workers Topic-based GraphQL Subscriptions\n\nThis library uses Cloudflare Workers, Durable Objects and D1 to provide powerful topic-based GraphQL subscriptions.\n\nFeatures:\n\n- 👀 Easy to integrate with your existing GraphQL stack\n- 🙌 Almost no setup\n- 🔍 In-database JSON filtering\n- 🗺 Publish from anywhere\n- 🎹 Great typings\n- 🔐 Authentication\n\n```ts\n// app.ts\nimport { makeExecutableSchema } from \"@graphql-tools/schema\";\nimport { createYoga } from \"graphql-yoga\";\nimport {\n  handleSubscriptions,\n  createWsConnectionPoolClass,\n  subscribe,\n  DefaultPublishableContext,\n  createDefaultPublishableContext,\n} from \"graphql-workers-subscriptions\";\n\nexport interface ENV {\n  WS_CONNECTION_POOL: DurableObjectNamespace;\n  SUBSCRIPTIONS: D1Database;\n}\n\nexport const schema = makeExecutableSchema\u003cDefaultPublishableContext\u003cENV\u003e\u003e({\n  typeDefs: /* GraphQL */ `\n    type Greeting {\n      greeting: String\n    }\n    type Query {\n      ping: String\n    }\n    type Subscription {\n      greetings(greeting: String): Greeting\n    }\n    type Mutation {\n      greet(greeting: String!): String\n    }\n  `,\n  resolvers: {\n    Query: {\n      ping: () =\u003e \"pong\",\n    },\n    Mutation: {\n      greet: async (root, args, context, info) =\u003e {\n        context.publish(\"GREETINGS\", {\n          greetings: { greeting: args.greeting },\n        });\n        return \"ok\";\n      },\n    },\n    Subscription: {\n      greetings: {\n        subscribe: subscribe(\"GREETINGS\", {\n          filter: (root, args, context, info) =\u003e {\n            return args.greeting\n              ? { greetings: { greeting: args.greeting } }\n              : {};\n          },\n        }),\n      },\n    },\n  },\n});\n\nconst settings = {\n  schema,\n  wsConnectionPool: (env: ENV) =\u003e env.WS_CONNECTION_POOL,\n  subscriptionsDb: (env: ENV) =\u003e env.SUBSCRIPTIONS,\n};\n\nconst yoga = createYoga\u003cDefaultPublishableContext\u003cENV\u003e\u003e({\n  schema,\n  graphiql: {\n    // Use WebSockets in GraphiQL\n    subscriptionsProtocol: \"WS\",\n  },\n});\n\nconst baseFetch: ExportedHandlerFetchHandler\u003cENV\u003e = (\n  request,\n  env,\n  executionCtx\n) =\u003e\n  yoga.handleRequest(\n    request,\n    createDefaultPublishableContext({\n      env,\n      executionCtx,\n      ...settings,\n    })\n  );\n\nconst fetch = handleSubscriptions({\n  fetch: baseFetch,\n  ...settings,\n});\n\nexport default { fetch };\n\nexport const WsConnectionPool = createWsConnectionPoolClass(settings);\n```\n\n```toml\n# wrangler.toml\n[[migrations]]\nnew_classes = [\"WsConnectionPool\"]\ntag = \"v1\"\n\n[build]\n# your build script\ncommand = 'npm run build'\n\n[[d1_databases]]\nbinding = \"SUBSCRIPTIONS\"\ndatabase_id = \"877f1123-088e-43ed-8d4d-37e71c77157c\"\ndatabase_name = \"SUBSCRIPTIONS\"\nmigrations_dir = \"node_modules/graphql-workers-subscriptions/migrations\"\npreview_database_id = \"877f1123-088e-43ed-8d4d-37e71c77157c\"\n\n[durable_objects]\nbindings = [{name = \"WS_CONNECTION_POOL\", class_name = \"WsConnectionPool\"}]\n```\n\n### Deployment\n\n```shell\n# create db\nwrangler d1 create SUBSCRIPTIONS\n# apply migrations\nwrangler d1 migrations apply SUBSCRIPTIONS\n# publish\nwrangler publish\n```\n\n### Local development\n\n```shell\n# create db\nwrangler d1 create SUBSCRIPTIONS --local\n# apply migrations\nwrangler d1 migrations apply SUBSCRIPTIONS --local\n# publish\nwrangler dev\n```\n\n### Authentication\n\nFor WebSocket authentication, you can pass `onConnect` to `createWsConnectionPoolClass` (read more documentation [here](https://the-guild.dev/graphql/ws/recipes#ws-server-and-client-auth-usage-with-token-expiration-validation-and-refresh)):\n\n```ts\nexport const WsConnectionPool = createWsConnectionPoolClass\u003cENV, { token: string }\u003e({\n  ...settings,\n  onConnect: (ctx) =\u003e {\n    const token = ctx.connectionParams.token;\n    return isTokenValid(token, ctx.extra.env.TOKEN);\n  },\n});\n```\n\nThen, on the frontend:\n\n```ts\nimport { GraphQLWsLink } from \"@apollo/client/link/subscriptions\";\nimport { createClient } from 'graphql-ws';\n\nconst wsLink = new GraphQLWsLink(\n  createClient({\n    url: \"wss://subscriptions.workers.dev\",\n    connectionParams: () =\u003e {\n      return { token: \"\u003cTOKEN\u003e\" };\n    }\n  })\n);\n```\n\nIf you want to verify individual subscriptions, use `onSubscribe`.\n\nTo verify the incoming HTTP requests, use `isPublishAuthorized` and `isConnectAuthorized`.\n\n### Publishing from outside Cloudflare\n\nYou can use `POST /publish` on your Worker to publish events.\n\n```shell\ncurl -X POST https://graphql-workers-subscriptions.bubblydoo.workers.dev/publish -H 'Content-Type: application/json' -d '{\"topic\": \"GREETINGS\", \"payload\":{\"greetings\": {\"greeting\": \"hi!\"}}}'\n```\n\nTo disable this, pass `isPublishAuthorized: () =\u003e false` to `handleSubscriptions`, or add custom authorization logic there.\n\n### Pooling\n\nTo minimize Durable Objects costs, one Pool can manage many WebSocket connections.\n\nThere are 3 pooling options:\n- `global`: There is 1 global Pool that manages all WebSocket connections.\n- `colo`: There's 1 Pool per datacenter, based on [`request.cf.colo`](https://developers.cloudflare.com/workers/runtime-apis/request/#incomingrequestcfproperties)\n- `continent`: There's 1 Pool per continent, based on [`request.cf.continent`](https://developers.cloudflare.com/workers/runtime-apis/request/#incomingrequestcfproperties) (default)\n- `none`: Every connection uses its own Pool (its own Durable Object)\n\nYou can also pass a custom pooling function to `handleSubscriptions`.\n\n### Expected costs\n\nConsidering you used up all your included credits in the Paid Plan:\n\nOne Pool (one Durable Object) is always charged at 128MB and will not sleep unless all its WebSockets are closed. Having one Pool run for 1 month (2600000 seconds) will be about 325000GBs, which would (at the time of writing) cost $4.0625. With a global pool, that would also be your maximum cost.\n\n1 new connection causes:\n- 1 D1 query\n- 1 Durable Object request (fetch)\n\n1 GraphQL subscription causes:\n- 1 Durable Object fetch (incoming WebSocket message)\n\n1 publish causes:\n- 1 D1 query\n- 1 Durable Object request (fetch) per Pool that has a connection that the publish will send to\n\nIf you would publish a message every second for 1 month to 100 WebSockets in 1 Pool, this would cause only 1 D1 query and 1 Durable Object request per second, and would cost you about $0.648/month. D1 is still free for now.\n\nAccording to the pricing docs, there is no charge for outgoing WebSocket messages.\n\n### Internal details\n\nSubscriptions are stored inside D1.\n\nThe D1 database has 6 columns:\n\n- id (websocket message id that will be matched with the client-side subscription, a string, generated by the client)\n- connectionId (a uuid, a string, identifying the WebSocket connection, generated by the Worker)\n- connectionPoolId (a Durable Object id identifying the pool, a string, e.g. `global`, or `US`)\n- subscription (the query the subscriber has requested, a JSON string)\n- topic (a string)\n- filter (the filter against which payloads are checked, a JSON string or null)\n\nThe Durable Object has a reference to the WebSocket, which can then be used to publish data to.\n\nFilters are compared in-database using:\n\n```sql\nSELECT * FROM Subscriptions WHERE topic = ?1 AND (filter is null OR json_patch(?2, filter) = ?2);\n```\n\nwith `?1: topic and ?2: payload`.\n\n### Contributing\n\nCheck out this repo, then run:\n\n```shell\nyarn\n\nyarn build\n```\n\nThen link the package to your project (you can take the example as start).\n\n### Bundling issue\n\nDue to the dual package hazard in GraphQL (see [this issue](https://github.com/graphql/graphql-js/pull/3617)) you might get `duplicate \"graphql\" modules cannot be used at the same time` errors.\n\nThis is because both the CJS and ESM version of `graphql` are loaded.\n\nIn that case, you might have to bundle yourself. When using `esbuild`, the option `--resolve-extensions=.mts,.mjs,.ts,.js,.json` works. See the build-app script in package.json for an example.\n\n### Credits 🙏\n\nThis project was inspired by [cloudflare-worker-graphql-ws-template](https://github.com/enisdenjo/cloudflare-worker-graphql-ws-template) and [subscriptionless](https://github.com/andyrichardson/subscriptionless).\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbubblydoo%2Fgraphql-workers-subscriptions","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbubblydoo%2Fgraphql-workers-subscriptions","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbubblydoo%2Fgraphql-workers-subscriptions/lists"}