{"id":50150028,"url":"https://github.com/just1689/entity-sync","last_synced_at":"2026-05-24T07:38:06.136Z","repository":{"id":57545803,"uuid":"189861606","full_name":"just1689/entity-sync","owner":"just1689","description":"Unopinionated secure queue-backed pub sub over websockets","archived":false,"fork":false,"pushed_at":"2019-06-13T10:01:37.000Z","size":152,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2026-05-24T07:38:02.878Z","etag":null,"topics":["distributed-systems","go","high-availability","nsq","push-notifications","queues","scalable","synchronization","websockets"],"latest_commit_sha":null,"homepage":"https://just1689.github.io/entity-sync/","language":"Go","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/just1689.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}},"created_at":"2019-06-02T15:13:15.000Z","updated_at":"2019-08-27T22:15:44.000Z","dependencies_parsed_at":"2022-09-05T10:50:49.844Z","dependency_job_id":null,"html_url":"https://github.com/just1689/entity-sync","commit_stats":null,"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/just1689/entity-sync","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/just1689%2Fentity-sync","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/just1689%2Fentity-sync/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/just1689%2Fentity-sync/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/just1689%2Fentity-sync/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/just1689","download_url":"https://codeload.github.com/just1689/entity-sync/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/just1689%2Fentity-sync/sbom","scorecard":{"id":543542,"data":{"date":"2025-08-11","repo":{"name":"github.com/just1689/entity-sync","commit":"755dc07e86561c7116d25a28ecab2afabf793c8a"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":2.7,"checks":[{"name":"Code-Review","score":0,"reason":"Found 0/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: containerImage not pinned by hash: Dockerfile:2","Info:   0 out of   1 containerImage dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Vulnerabilities","score":9,"reason":"1 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GO-2020-0019 / GHSA-3xh2-74w9-5vxm"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-20T08:49:53.496Z","repository_id":57545803,"created_at":"2025-08-20T08:49:53.496Z","updated_at":"2025-08-20T08:49:53.496Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33426013,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-23T22:14:44.296Z","status":"online","status_checked_at":"2026-05-24T02:00:06.296Z","response_time":57,"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":["distributed-systems","go","high-availability","nsq","push-notifications","queues","scalable","synchronization","websockets"],"created_at":"2026-05-24T07:38:02.183Z","updated_at":"2026-05-24T07:38:06.131Z","avatar_url":"https://github.com/just1689.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Entity Sync\n\u003cimg align=\"right\" width=\"350\" src=\"docs/diagram-v2.svg\" /\u003e\n\n\u003cimg src=\"https://goreportcard.com/badge/github.com/just1689/entity-sync\"\u003e\u0026nbsp;\u003ca href=\"https://codebeat.co/projects/github-com-just1689-entity-sync-master\"\u003e\u003cimg alt=\"codebeat badge\" src=\"https://codebeat.co/badges/db75c6df-77e3-4f84-9464-ca1d2062566c\" /\u003e\u003c/a\u003e\u0026nbsp;\u003ca href=\"https://codeclimate.com/github/just1689/entity-sync/maintainability\"\u003e\u003cimg src=\"https://api.codeclimate.com/v1/badges/4ccbe11fba6a8037fa76/maintainability\" /\u003e\u003c/a\u003e\u0026nbsp;![version](https://img.shields.io/badge/version-5.2.0-green.svg?cacheSeconds=2592000)\n\u003cbr /\u003e\nWant to push entities on change to websocket clients while scaling horizontally? The goal of this library is  to make that easy. Wire Entity Sync into your application and you can keep clients up to date. Each client can subscribe to particular entities. You can change something on one server and all the websocket clients subscribed in the cluster will be pushed the updated entity.   \n\n## Features\n\n- Stateless server. Servers do not need to know about each other or which clients are connected to other servers. This allows the server to scale without synchronizing them.\n- When you change something on the server side, provide the EntityKey to the bridge and all clients will be pushed the entity.\n- Multiple subscriptions. Each client can subscribe to multiple entities and multiples keys in each entity. \n- Multiple responses. You can send back several rows. This is great if updating the client means sending them rows from tables in foreign keys etc.\n- Database / repository agnostic. This library can take a function that you implement to use whichever database, driver, client or interface you choose to implement. \n- A helper package for a simple one call setup (see es/entitysync.go)\n- Queue agnostic. Comes with working NSQ integration but you can choose to provide anything you can wrap in `shared.EntityHandler` and `shared.EntityByteHandler`.\n- Client secrets. The client can send a secret and when processed you can choose to use the secret to determine identity and authorization for example.\n- Function for incoming websocket requests that don't match any concern for this library to pass through.\n\n## Roadmap\n- Enable custom websocket listen path.\n- Consider improving the security model.\n- Consider a different fetch model (fetch per user vs fetch per server).\n- Queue prefix - provide a prepend string to ensure es topics are kept apart from others.\n\n## Example\n\n### Server setup\nConnect the server to EntitySync. Wire the your mux to the bridge and provide a method that can resolve an `EntityKey`.\n```go\n// Provide a configuration\nconfig := es.Config{\n    Mux:     mux.NewRouter(),\n    NSQAddr: *nsqAddr,\n}\n//Setup entitySync with that configuration\nentitySync := es.Setup(config)\n\n//Register an entity and tell the library how to fetch and what to write to the client\nentitySync.RegisterEntityAndDBHandler(entityType, func(entityKey shared.EntityKey, secret string, handler shared.ByteHandler) {\n    item := fetch(entityKey)\n    b, _ := json.Marshal(item)\n    handler(b)\n})\n\n//Start a listener and provide the mux for routes / handling\nl, _ = net.Listen(\"tcp\", *listenLocal)\nhttp.Serve(l, config.Mux)\n```\n\n### Connect clients\nConnect any number of clients:\n1. Connect to the server over websocket ws://host:port/ws/entity-sync/\n2. Send a subscription request\n \n```json\n{\n    \"action\": \"subscribe\",\n    \"body\": {\n        \"id\": \"100\",\n        \"entity\": \"items\"\n    }\n}\n```\n### Mutate entity \u0026 notify\n\nMake some change to the item in question where it is persisted and then call\n`bridge.NotifyAllOfChange(entityKey)` where entityKey is a `shared.EntityKey`.\n\nAll connected clients over websockets will receive messages for the EntityKey/s to which they are subscribed.\n\n\n### Sending a secret from the client\n```json\n{\n    \"action\": \"secret\",\n    \"body\": \"my-super-secret-secret-123\"\n}\n```\n\n\n## Other examples\n\n### Sending multiple rows to the client\n```go\nentitySync.RegisterEntityAndDBHandler(\"report\", func(entityKey shared.EntityKey, secret string, handler shared.ByteHandler) {\n    item := controller.GetReportByID(entityKey.ID)\n    b, _ := json.Marshal(item)\n    handler(b)\n        \n    user := controller.GetUserByID(item.CreatedBy)\n    b, _ = json.Marshal(user)\n    handler(b)\n\n    department := controller.GetDeparmentByID(item.departmentID)\n    b, _ = json.Marshal(department)\n    handler(b)\n    \n})\n```\n\n### Checking if a user is allowed to receive the push notification\n```go\nentitySync.RegisterEntityAndDBHandler(\"report\", func(entityKey shared.EntityKey, secret string, handler shared.ByteHandler) {\n    session, err := contoller.GetSessionBySecret(secret)\n    if err != nil {\n        logrus.Errorln(err)\n        return\n    }\n\t\n    item := controller.GetReportByID(entityKey.ID)\n    b, _ := json.Marshal(item)\n    handler(b)\n    \n})\n```\n\n### Provide your own queue\nEntitySync is built using NSQ. In theory you can use whatever you like. You will need to provide two functions to the library. The one will allow it to produce a publisher and return a method that will be called to publish. The other is a subscriber and  is provided a method for sending.\n```go\n\nvar queueAddr = \"localhost:4000\"\n\nfunc setup() {\n    mux := http.NewServeMux()\n    databaseHub := esdb.NewDatabaseHub()\n    \n    // The bridge matches communication from ws to nsq and from nsq to ws.\n    // It also calls on the db to resolve entityKey\n    bridge := esbridge.BuildBridge(\n        BuildPublisher(queueAddr),\n        BuildSubscriber(queueAddr),\n        databaseHub.PullDataAndPush,\n    )\n    \n    //Pass the mux and a client builder to the libraries handlers\n    esweb.SetupMuxBridge(mux, bridge.ClientBuilder)\n    \n    ...\n}\n\nvar BuildPublisher shared.AddressableEntityHandler = func(addr string) shared.EntityHandler {\n\treturn func(entityType shared.EntityType) shared.ByteHandler {\n\t\treturn func(b []byte) {\n\t\t\t//TODO: setup the publisher client\n\t\t\t...\n\t\t\tqPublisher.publish(entityType.GetQueueName(), b)\n\t\t}\n\t}\n}\n\nvar BuildSubscriber shared.AddressableEntityByteHandler = func(addr string) shared.EntityByteHandler {\n\treturn func(entityType shared.EntityType, callback shared.ByteHandler) {\n\t\tfunc subscribeNSQ(qAddr string, entityType shared.EntityType, f shared.ByteHandler) {\n\t\t}(qArr, entityType, f func(b []byte {\n\t\t\t//TODO: connect to the nats client\n\t\t\t...\n\t\t\tnatsHandler(in []byte) {\n\t\t\t\tf(in)\n\t\t\t}\n\t\t}))\n\t}\n}\n```\n\n### Pass through ws\nYou can provide a method that will allow for pass-through handling of websocket messages.\n```go\n// Provide a configuration\nconfig := es.Config{\n    Mux:            mux.NewRouter(),\n    NSQAddr:        *nsqAddr,\n    WSPassThrough:  func(secret string, b []byte) {\n    \t//TODO: handle incoming websocket message\n    } \n}\n//Setup entitySync with that configuration\nentitySync := es.Setup(config)\n\n//Register an entity and tell the library how to fetch and what to write to the client\nentitySync.RegisterEntityAndDBHandler(entityType, func(entityKey shared.EntityKey, secret string, handler shared.ByteHandler) {\n    item := fetch(entityKey)\n    b, _ := json.Marshal(item)\n    handler(b)\n})\n\n//Start a listener and provide the mux for routes / handling\nl, _ = net.Listen(\"tcp\", *listenLocal)\nhttp.Serve(l, config.Mux)\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjust1689%2Fentity-sync","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjust1689%2Fentity-sync","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjust1689%2Fentity-sync/lists"}