{"id":19935691,"url":"https://github.com/jeffrafter/signaling","last_synced_at":"2025-09-19T08:31:59.432Z","repository":{"id":152408396,"uuid":"620351612","full_name":"jeffrafter/signaling","owner":"jeffrafter","description":"Serverless Signaling Server","archived":false,"fork":false,"pushed_at":"2023-10-08T20:50:41.000Z","size":226,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-01-10T00:52:54.766Z","etag":null,"topics":["signaling","terraform","webrtc","websockets","yjs"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jeffrafter.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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}},"created_at":"2023-03-28T14:09:06.000Z","updated_at":"2023-11-30T13:44:16.000Z","dependencies_parsed_at":null,"dependency_job_id":"71a17d0e-8b93-4fb0-b27c-e976901028fc","html_url":"https://github.com/jeffrafter/signaling","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jeffrafter%2Fsignaling","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jeffrafter%2Fsignaling/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jeffrafter%2Fsignaling/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jeffrafter%2Fsignaling/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jeffrafter","download_url":"https://codeload.github.com/jeffrafter/signaling/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":233558501,"owners_count":18694048,"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":["signaling","terraform","webrtc","websockets","yjs"],"created_at":"2024-11-12T23:21:26.408Z","updated_at":"2025-09-19T08:31:54.110Z","avatar_url":"https://github.com/jeffrafter.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Serverless Signaling Server\n\nThis is a signaling server that is intended to work with `yjs` WebRTC clients.\n\nConnections and topic subscriptions (rooms) are stored in DynamoDB.\n\nThis is based on the the `yjs` example signaling server:\n\nhttps://github.com/yjs/y-webrtc/blob/master/bin/server.js\n\nAnd the following posts and demos:\n\n- https://discuss.yjs.dev/t/serverless-signaling-server-with-aws-apigateway-dynamodb-and-lambda/680\n- https://medium.com/collaborne-engineering/serverless-yjs-72d0a84326a2\n- https://github.com/Collaborne/remirror-yjs-webrtc-demo\n\nThe primary differences in this implementation are around the storage.\n\nIn the `yjs` example the connections and subscriptions are maintained in memory. This is very efficient but requires an unbounded amount of memory and an always-on machine, often deployed to a VPS like fly.io.\n\nIn the serverless examples above, DynamoDB is used but each topic subscription is stored in a single document with a growing list of connections. This simplifies retrieval for publishing changes to clients but requires an expensive `scan` to handle disconnections.\n\nOur approach uses a hash and range for subscriptions and a global secondary index for disconnection lookups. The primary actions:\n\n- **subscribe**: add a connectionId to the TOPIC\n- **unsubscribe**: remove the connectionId from the TOPIC\n- **publish**: get all connectionIds associated with a TOPIC (and send)\n- **disconnect**: remove the connectionId from all TOPICs\n\nFor example:\n\n```\n| pk                    | sk                       | data\n|=======================|==========================|==================\n| topic-TOPIC           | connection-connectionId1 | ttl could go here\n| topic-TOPIC           | connection-connectionId2 |\n| topic-TOPIC           | connection-connectionId3 |\n| topic-TOPIC           | connection-connectionId4 |\n```\n\nNote, would could do the following optionally:\n\n- set a TTL on a subscription, make the client periodically resubscribe or be `ttl` disconnected\n\nIn terraform this is provisioned as (fitting into the free tier):\n\n```tf\nresource \"aws_dynamodb_table\" \"database\" {\n  name           = \"Database\"\n  billing_mode   = \"PROVISIONED\"\n  read_capacity  = 20\n  write_capacity = 20\n  hash_key       = \"pk\"\n  range_key      = \"sk\"\n\n  attribute {\n    name = \"pk\"\n    type = \"S\"\n  }\n\n  attribute {\n    name = \"sk\"\n    type = \"S\"\n  }\n\n  attribute {\n    name = \"data\"\n    type = \"S\"\n  }\n\n  global_secondary_index {\n    name            = \"gs1\"\n    hash_key        = \"sk\"\n    range_key       = \"data\"\n    write_capacity  = 5\n    read_capacity   = 5\n    projection_type = \"ALL\"\n  }\n\n  tags = {\n    Name        = \"Database\"\n    Environment = \"production\"\n  }\n}\n```\n\nWe assume the following:\n\n- Concurrent subscribers to a topic will be generally small (usually less than 10, almost always less than 100) so all query operations will consume a single page and only 1 read capacity unit per read (0.5 eventually consistent).\n- Because we are only signaling, there will be less broadcast publishes and the primary activity will be connections and disconnections. These should be fast and should avoid contention.\n\n\n# Deploying\n\nThis is deployed by running\n\n```\nnpm run deploy\n```\n\nIt assumes a terraform setup in a neighboring `ops` folder which is custom to my setup.\n\nYou should have an `.env`:\n\n```\nNODE_ENV=development\nLOCALSTACK_HOSTNAME=0.0.0.0\nAWS_DYNAMODB_ENDPOINT=http://0.0.0.0:4566\nAWS_ACCESS_KEY_ID=test\nAWS_SECRET_ACCESS_KEY=test\nAWS_REGION=us-east-1\nAWS_PROFILE=your-aws-profile\nTABLE_NAME=Database\nDEPLOY_PATH=../../path-to-your-ops-folder/ops\n```\n\n# Running this locally in development\n\nRunning this locally requires mimicking the AWS environment. I use `localstack` for this.\n\nRun Docker Desktop. Then run:\n\n```\nnpm run localstack\n```\n\nThis will start the AWS clone locally but you will need to initialize the local DynamoDB:\n\n```\n./scripts/create-local-db\n```\n\nOnce running, you should be able to run the server:\n\n```\nnpm run start\n```\n\nNote this is running the `local.ts` version of the web socket on port 4001. This isn't the same as the production version which is relying on AWS API Gateway and Lambda, but the main handler logic is the same for both.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjeffrafter%2Fsignaling","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjeffrafter%2Fsignaling","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjeffrafter%2Fsignaling/lists"}