{"id":13482324,"url":"https://github.com/dominictarr/scuttlebutt","last_synced_at":"2026-10-19T14:10:19.400Z","repository":{"id":4078137,"uuid":"5183533","full_name":"dominictarr/scuttlebutt","owner":"dominictarr","description":"peer-to-peer replicatable data structure","archived":false,"fork":false,"pushed_at":"2017-04-12T23:43:06.000Z","size":170,"stargazers_count":1317,"open_issues_count":19,"forks_count":66,"subscribers_count":47,"default_branch":"master","last_synced_at":"2026-10-15T04:19:03.521Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"JavaScript","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/dominictarr.png","metadata":{"files":{"readme":"README.markdown","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.APACHE2","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"security.js","support":null}},"created_at":"2012-07-25T19:31:03.000Z","updated_at":"2026-10-02T19:55:57.000Z","dependencies_parsed_at":"2022-08-08T06:01:37.763Z","dependency_job_id":null,"html_url":"https://github.com/dominictarr/scuttlebutt","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/dominictarr/scuttlebutt","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dominictarr%2Fscuttlebutt","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dominictarr%2Fscuttlebutt/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dominictarr%2Fscuttlebutt/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dominictarr%2Fscuttlebutt/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dominictarr","download_url":"https://codeload.github.com/dominictarr/scuttlebutt/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dominictarr%2Fscuttlebutt/sbom","scorecard":{"id":351033,"data":{"date":"2025-08-11","repo":{"name":"github.com/dominictarr/scuttlebutt","commit":"7b7f89ab569329334daaa1ed4e9a3ecbed224a66"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3.3,"checks":[{"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":"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":"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":"Code-Review","score":2,"reason":"Found 7/28 approved changesets -- score normalized to 2","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":"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":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"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":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"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":9,"reason":"license file detected","details":["Info: project has a license file: LICENSE.APACHE2:0","Warn: project license file does not contain an FSF or OSI license."],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"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":"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":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 9 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}}]},"last_synced_at":"2025-08-18T08:18:34.645Z","repository_id":4078137,"created_at":"2025-08-18T08:18:34.645Z","updated_at":"2025-08-18T08:18:34.645Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":344872684,"owners_count":38357901,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-10-03T21:59:58.778Z","status":"online","status_checked_at":"2026-10-19T02:00:06.414Z","response_time":53,"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":[],"created_at":"2024-07-31T17:01:00.877Z","updated_at":"2026-10-19T14:10:19.176Z","avatar_url":"https://github.com/dominictarr.png","language":"JavaScript","funding_links":[],"categories":["JavaScript","Modules","others"],"sub_categories":[],"readme":"# scuttlebutt\n\nA base-class for real-time replication.\n\n[![travis](https://secure.travis-ci.org/dominictarr/scuttlebutt.png?branch=master)](https://travis-ci.org/dominictarr/scuttlebutt)\n\n[![browser support](http://ci.testling.com/dominictarr/scuttlebutt.png)](http://ci.testling.com/dominictarr/scuttlebutt)\n\nThis seems like a silly name, but I assure you, this is real science. \nRead this: http://www.cs.cornell.edu/home/rvr/papers/flowgossip.pdf \n\nOr, if you're lazy: http://en.wikipedia.org/wiki/Scuttlebutt (laziness will get you nowhere, btw)\n\n## secure-scuttlebutt\n\nCreating this module eventually lead me to [secure-scuttlebutt](https://github.com/dominictarr/secure-scuttlebutt)\nit is based on a similar replication protocol as scuttlebutt, but puts security at the forefront.\n\n## Subclasses\n\nScuttlebutt is intended to be subclassed into a variety of data-models.\n\nTwo implementations are provided as examples [scuttlebutt/model](#scuttlebuttmodel) and\n[scuttlebutt/events](#scuttlebuttevents)\n\nsubclasses:\n\n  * [crdt](https://github.com/dominictarr/crdt) higher-level, with sets and sequences.\n  * [r-array](https://github.com/dominictarr/r-array) Replicatable Array.\n  * [r-edit](https://github.com/dominictarr/r-edit) Collaborative Text Editing.\n  * [append-only](https://github.com/Raynos/append-only) news feed.\n  * [scuttlebucket](https://github.com/dominictarr/scuttlebucket) combine multiple scuttlebutts into one.\n  * [expiry-model](https://github.com/Raynos/expiry-model) memory capped model with expiring keys.\n  * [r-value](https://github.com/dominictarr/r-value) replicate a single value.\n  * [gossip-object](https://github.com/vigour-io/gossip-object) like scuttlebutt/model, but supports nested objects.\n  * [scuttleboat](https://github.com/kumavis/scuttleboat) a dynamic scuttlebucket!\n  * [r-string](https://github.com/marcelklehr/r-string) character-wise collaborative text editing.\n\n### Replication\n\nAny Scuttlebutt subclass is replicated with createStream.\nCreate a server and then connect a client to it:\n\n``` js\nvar Model = require('scuttlebutt/model')\nvar net   = require('net')\n\nvar s = new Model()\nvar z = new Model()\n\nnet.createServer(function (stream) {\n\n  stream.pipe(s.createStream()).pipe(stream)\n\n}).listen(8000, function () {\n\n  //then connect the client!\n  var stream = net.connect(8000)\n  stream.pipe(z.createStream()).pipe(stream)  \n\n})\n```\n\n### Gotchas\n\nScuttlebutt is always duplex.\nScuttlebutt does a handshake on connecting to another scuttlebutt,\nand this won't work unless both sides are connected.\n\n#### Right\n\n``` js\nstream.pipe(model.createStream()).pipe(stream)\n```\n\n#### WRONG!\n\n``` js\nwrongStream.pipe(model2.createStream())\n```\n\nAlso, when creating a server, scuttlebutt needs a stream for EACH connection.\n\n#### Right\n\n``` js\nnet.createServer(function (stream) {\n  stream.pipe(model.createStream()).pipe(stream)\n}).listen(port)\n```\n\n#### WRONG!\nthis will use one stream for many connections!\n``` js\nvar wrongStream = model.createStream()\nnet.createServer(function (stream) {\n  stream.pipe(wrongStream).pipe(stream)\n}).listen(port)\n```\n\n### Errors and use in PRODUCTION\n\nIf have are using scuttlebutt in production, you must register\non `'error'` listener in case someone sends invalid data to it.\n\n** Any stream that gets parsed should have an error listener! **\n\n``` js\nnet.createServer(function (stream) {\n  var ms = m.createStream()\n  stream.pipe(ms).pipe(stream)\n  ms.on('error', function () {\n    stream.destroy()\n  })\n  stream.on('error', function () {\n    ms.destroy()\n  })\n}).listen(9999)\n```\n\nOtherwise, if someone tries to connect to port `9999` with a different\nprotocol (say, HTTP) this will emit an error. You must handle this and\nclose the connection / log the error.\n\nAlso, you should handle errors on `stream`, stream may error if the client\nresponsible for it crashes.\n\n### Persistence\n\nPersist by saving to at least one writable stream.\n\n``` js\nvar Model = require('scuttlebutt/model') //or some other subclass...\nvar fs = require('fs')\nvar m = new Model()\n\n//stream FROM disk.\nfs.createReadStream(file).pipe(m.createWriteStream())\n\n//stream TO disk.\nm.on('sync', function () {\n  m.createReadStream().pipe(fs.createWriteStream(file))\n})\n```\nUse `on('sync',...` to wait until the persisted state is in the file\nbefore writing to disk.\n(Make sure you rotate files, else there is an edge case where if the process\ncrashes before the history has been written some data will be lost\n/*this is where link to module for that will go*/)\n\nYou may use [kv](https://github.com/dominictarr/kv) to get streams \nto local storage.\n\n## read only mode.\n\nSometimes you want to use scuttlebutt to send data one way,\nfrom a `master` instance to a `slave` instance.\n\n``` js\nvar s1 = master.createStream({writable: false, sendClock: true})\nvar s2 = slave.createStream({readable: false, sendClock: true})\n```\n\n`master` will emit updates, but not accept them, over this stream.\nThis checking is per stream - so it's possible to attach `master` to \nanother master node and have master nodes replicate each way.\n\n## Implementing Custom Scuttlebutts\n\nA custom Scuttlebutt is a data model that inherits from `Scuttlebutt`. It must provide an implementation of `history()` and `applyUpdate()`.\n\nSee [r-value](https://github.com/dominictarr/r-value) for a demonstration of the simplest possible Scuttlebutt, one that replicates a a single value.\n\n### Scuttlebutt#history(sources)\n\n`sources` is a hash of source_ids: timestamps. \nHistory must return an array of all known events from all sources\nThat occur after the given timestamps for each source.\n\nThe array MUST be in order by timestamp.\n\n### Scuttlebutt#applyUpdate (update)\n\nEach update is of the form `[change, timestamp, source]` (see [Protocol](#Protocol) below).\n\nPossibly apply a given update to the subclasses model.\nReturn 'true' if the update was applied. (See scuttlebutt/model.js\nfor an example of a subclass that does not apply every update.)\n\n### Scuttlebutt#createStream (opts)\n\nCreate a duplex stream to replicate with a remote endpoint.\n\nThe stream returned here emits a special `'header'` event with the id of the\nlocal and remote nodes and the vector clock. You can set metadata on the header\nobject using `opts.meta`.\n\n#### Examples\n\nConnect two `Model` scuttlebutts locally.\n\n``` js\nvar Model = require('scuttlebutt/model')\n\nvar a = new Model()\nvar b = new Model()\n\na.set(key, value)\n\nb.on('update', console.log)\n\nvar s = a.createStream()\ns.pipe(b.createStream()).pipe(s)\n```\n\n### scuttlebutt/events\n\nA reliable event emmitter. Multiple instances of an emitter\nmay be connected to each other and will remember events,\nso that they may be present after a disconnection or crash.\n\nWith this approach it is also possible to persist events to disk,\nmaking them durable over crashes.\n\n``` js\nvar Emitter = require('scuttlebutt/events')\nvar emitter = new Emitter()\n```\n\n#### emit (event, data)\n\nEmit an event. Only one argument is permitted.\n\n#### on (event, listener)\n\nAdd an event listener.\n\n### scuttlebutt/model\n\nA replicateable `Model` object, a simple key-value store.\n\n``` js\nvar Model = require('scuttlebutt/model')\nvar model = new Model()\n```\n\n\n#### get (key)\n\nGet a property.\n\n#### set (key, value)\n\nSet a property.\n\n#### on('update', function ([key, value], source, updateId))\n\nEmmitted when a property changes. \nIf `source !== this.id`\nthen it was a remote update.\n\n## Protocol\n\nMessages are sent in this format:\n\n``` js\n[change, timestamp, source]\n```\n\n`source` is the id of the node which originated this message.\nTimestamp is the time when the message was created. \nThis message is created using `Scuttlebutt#localUpdate(key, value)`.\n\nWhen two `Scuttlebutts` are piped together, they both exchange their current list\nof sources. This is an object of `{source_id: latest_timestamp_for_source_id}`\nAfter receiving this message, `Scuttlebutt` sends any messages not yet \nknown by the other end. This is the heart of Scuttlebutt Reconciliation.\n\n## Security\n\nScuttlebutt has an (optional) heavy duty security model using public keys. \nThis enables a high level of security even in peer-to-peer applications.\nYou can be sure that a given message is from the node that sent it, \neven if you did not receive the messasge from them directly.\n\n## Enabling Security\n\n``` js\nvar model = require('scuttlebutt/model')\nvar security = require('scuttlebutt/security')\nvar keys = {}\nvar m = new Model(security(keys, PRIVATE, PUBLIC))\n```\n\n## Security API\n\nWhen security is enabled, each scuttlebutt message is signed with a private key.\nIt is then possible for any scuttlebutt instance to be confident about the\nauthenticity of the message by verifying it against the source's public key.\n\nThis is possible even if the verifying node received the message from an intermediate node.\n\nSecurity is activated by passing in a security object to the contructor of a scuttlebutt\nsubclass. \n\nUse the included implementation:\n\n``` js\nvar security = require('scuttlebutt/security')(keys, PRIVATE, PUBLIC)\nvar Model = require('scuttlebutt/model')\n\nvar m = new Model(security)\n```\n\nSee \n[scuttlebutt/security.js](https://github.com/dominictarr/scuttlebutt/blob/master/security.js)\nfor a simple example implementation.\n\n`sign(update)` should sign the `update` with the instance's private key.\n`verify(update, cb)` should verify the update, using public key associated with the\n`source` field in the update. Verification may be asynchronous. `verify` must callback\n`cb(err, boolean)` where boolean indicates whether or not the signature is valid.\nOnly callback in error in the most extreme circumstances. \nIf there was no known key for the required source then that should be treated as a \nverification failure. If it is not possible to reach the key database (or whatever)\nthen the request should be retried until it is available. \n\n\u003e Note: although the API supports asynchronous verification,\n\u003e it's probably a good idea to load keys into memory so that messages can be verified\n\u003e and signed synchronously.\n\n`createId()` returns a new id for the current node. This is used in the example security \nimplementation to return a id that is a hash of the public key. This makes it impossible\nfor rogue nodes to attempt to associate a old node id with a new public key.\n\n## Generating Keys.\n\nGenerate an ssh private key, and a PEM encoded public key.\n```\nssh-keygen -f $KEYNAME -b $LENGTH -N $PASSWORD -q\nssh-keygen -e -f $KEYNAME.pub -m PEM \u003e $KEYNAME.pem\n\n```\n`$LENGTH` must be `\u003e= 786`, shorter is faster but less secure.\npassword may be empty `''`.\n\n`$KEYNAME` is the private key, and `$KEYNAME.pem` is the public key\nto use with Scuttlebutt.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdominictarr%2Fscuttlebutt","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdominictarr%2Fscuttlebutt","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdominictarr%2Fscuttlebutt/lists"}