{"id":18486585,"url":"https://github.com/textileio/go-threads","last_synced_at":"2025-04-08T12:10:50.303Z","repository":{"id":41293726,"uuid":"197858776","full_name":"textileio/go-threads","owner":"textileio","description":"Server-less p2p database built on libp2p","archived":false,"fork":false,"pushed_at":"2023-02-15T02:42:55.000Z","size":9062,"stargazers_count":450,"open_issues_count":89,"forks_count":65,"subscribers_count":18,"default_branch":"master","last_synced_at":"2024-04-25T15:01:32.023Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","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/textileio.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2019-07-20T00:24:31.000Z","updated_at":"2024-04-25T06:23:52.000Z","dependencies_parsed_at":"2023-07-13T22:01:04.412Z","dependency_job_id":null,"html_url":"https://github.com/textileio/go-threads","commit_stats":null,"previous_names":["textileio/go-textile-threads","textileio/go-textile-thread"],"tags_count":33,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/textileio%2Fgo-threads","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/textileio%2Fgo-threads/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/textileio%2Fgo-threads/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/textileio%2Fgo-threads/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/textileio","download_url":"https://codeload.github.com/textileio/go-threads/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247838445,"owners_count":21004580,"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":[],"created_at":"2024-11-06T12:49:37.800Z","updated_at":"2025-04-08T12:10:50.283Z","avatar_url":"https://github.com/textileio.png","language":"Go","funding_links":[],"categories":["Go"],"sub_categories":[],"readme":"# ThreadDB\n\n[![Made by Textile](https://img.shields.io/badge/made%20by-Textile-informational.svg?style=popout-square)](https://textile.io)\n[![Chat on Slack](https://img.shields.io/badge/slack-slack.textile.io-informational.svg?style=popout-square)](https://slack.textile.io)\n[![GitHub license](https://img.shields.io/github/license/textileio/go-threads.svg?style=popout-square)](./LICENSE)\n[![Go Report Card](https://goreportcard.com/badge/github.com/textileio/go-threads?style=flat-square)](https://goreportcard.com/report/github.com/textileio/go-threads?style=flat-square)\n[![GitHub action](https://github.com/textileio/go-threads/workflows/Tests/badge.svg?style=popout-square)](https://github.com/textileio/go-threads/actions)\n[![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=popout-square)](https://github.com/RichardLitt/standard-readme)\n\n\u003e Server-less p2p database built on libp2p\n\nJoin us on our [public Slack channel](https://slack.textile.io/) for news, discussions, and status updates. [Check out our blog](https://medium.com/textileio) for the latest posts and announcements.\n\n## Table of Contents\n\n* [Table of Contents](#table-of-contents)\n* [Security](#security)\n* [Background](#background)\n* [Install](#install)\n  * [Daemon](#daemon)\n  * [Client](#client)\n* [Getting Started](#getting-started)\n  * [Running ThreadDB](#running-threaddb)\n    * [Configuration values](#configuration-values)\n  * [The DB API](#the-db-api)\n    * [Starting the client](#starting-the-client)\n    * [Getting a thread token](#getting-a-thread-token)\n    * [Creating a new DB](#creating-a-new-db)\n    * [Creating a new DB from an existing address](#creating-a-new-db-from-an-existing-address)\n    * [Creating a collection](#creating-a-collection)\n      * [Write Validation](#write-validation)\n      * [Read Filtering](#read-filtering)\n    * [Updating a collection](#updating-a-collection)\n    * [Creating an instance](#creating-an-instance)\n    * [Saving an instance](#saving-an-instance)\n    * [Collection queries](#collection-queries)\n    * [Transactions](#transactions)\n      * [Write transactions](#write-transactions)\n      * [Read transactions](#read-transactions)\n    * [Listening for collection changes](#listening-for-collection-changes)\n  * [The Network API](#the-network-api)\n    * [Starting the client](#starting-the-client-1)\n    * [Getting a thread token](#getting-a-thread-token-1)\n    * [Creating a thread](#creating-a-thread)\n    * [Adding an existing thread](#adding-an-existing-thread)\n    * [Adding a thread replicator](#adding-a-thread-replicator)\n    * [Creating a thread record](#creating-a-thread-record)\n    * [Adding a thread record](#adding-a-thread-record)\n    * [Pulling a thread for new records](#pulling-a-thread-for-new-records)\n    * [Listening for new records](#listening-for-new-records)\n* [Developing](#developing)\n* [Contributing](#contributing)\n* [Changelog](#changelog)\n* [License](#license)\n\n## Security\n\nThreadDB is still under heavy development and no part of it should be used before a thorough review of the underlying code and an understanding APIs and protocols may change rapidly. There may be coding mistakes, and the underlying protocols may contain design flaws. Please [let us know](mailto:contact@textile.io) immediately if you have discovered a security vulnerability.\n\nPlease also read the [security note](https://github.com/ipfs/go-ipfs#security-issues) for [go-ipfs](https://github.com/ipfs/go-ipfs).\n\n## Background\n\nThreadDB is an implementation of the database described in the paper entitled [_A protocol \u0026 event-sourced database for decentralized user-siloed data_](https://docsend.com/view/gu3ywqi). \n\nGo to [the docs](https://docs.textile.io/) for more about the motivations behind ThreadDB and Textile.\n\n## Install\n\nThreadDB has two distinct layers:\n\n-   ***`db`***:  The database layer is a document store, which internally leverages the `net` API. Most applications will only interface with this layer.\n-   ***`net`***: The network layer maintains and orchestrates append-only event logs between network participants. Some applications, like event logging, may choose to rely on this layer directly.\n\nThis repo contains a daemon and client for interacting with these layers as a _remote service_. Depending on the application, Golang projects may choose to import the internal `db` and `net` packages directly.\n\n### Daemon\n\n-   **Prebuilt package**: See [release assets](https://github.com/textileio/go-threads/releases/latest)\n-   **Docker image**: See the `latest` tag on [Docker Hub](https://hub.docker.com/r/textile/go-threads/tags)\n-   **Build from the source**:\n\n```bash\ngit clone https://github.com/textileio/go-threads\ncd go-threads\ngo get ./threadsd\n```\n\n#### Client\n\n```go\nimport \"github.com/textileio/go-threads/api/client\"\n```\n\n## Getting Started\n\nYou can think of the [DB client](https://pkg.go.dev/github.com/textileio/go-threads/api/client) as a gRPC client wrapper around the internal `db` package API, and the [Network client](https://pkg.go.dev/github.com/textileio/go-threads/net/api/client) as a gRPC client wrapper around the internal `net` package API. This section will only focus on getting started with the gRPC clients, but Golang apps may choose to interact directly with `db` and/or `net`.\n\n### Running ThreadDB\n\nThe `threadsd` daemon can be run as a server or alongside desktop apps or command-line tools. The easiest way to run `threadsd` is by using the provided Docker Compose files. If you're new to Docker and/or Docker Compose, get started [here](https://docs.docker.com/compose/gettingstarted/). Once you are setup, you should have `docker-compose` in your `PATH`.\n\nCreate an `.env` file and add the following values:  \n\n```bash\nTHRDS_REPO=~/myrepo\nTHRDS_DEBUG=true\n```\n\nCopy [this compose file](https://github.com/textileio/go-threads/blob/master/docker-compose.yml) and run it with the following command.\n\n```bash\ndocker-compose -f docker-compose.yml up \n```\n\nYou should see some console output:\n\n```bash\nthreads_1  | 2020-09-19T16:34:06.420Z\tDEBUG\tthreadsd\trepo: /data/threads\nthreads_1  | 2020-09-19T16:34:06.420Z\tDEBUG\tthreadsd\thostAddr: /ip4/0.0.0.0/tcp/4006\nthreads_1  | 2020-09-19T16:34:06.421Z\tDEBUG\tthreadsd\tapiAddr: /ip4/0.0.0.0/tcp/6006\nthreads_1  | 2020-09-19T16:34:06.421Z\tDEBUG\tthreadsd\tapiProxyAddr: /ip4/0.0.0.0/tcp/6007\nthreads_1  | 2020-09-19T16:34:06.421Z\tDEBUG\tthreadsd\tconnLowWater: 100\nthreads_1  | 2020-09-19T16:34:06.421Z\tDEBUG\tthreadsd\tconnHighWater: 400\nthreads_1  | 2020-09-19T16:34:06.422Z\tDEBUG\tthreadsd\tconnGracePeriod: 20s\nthreads_1  | 2020-09-19T16:34:06.423Z\tDEBUG\tthreadsd\tkeepAliveInterval: 5s\nthreads_1  | 2020-09-19T16:34:06.423Z\tDEBUG\tthreadsd\tenableNetPubsub: false\nthreads_1  | 2020-09-19T16:34:06.424Z\tDEBUG\tthreadsd\tdebug: true\nthreads_1  | Welcome to Threads!\nthreads_1  | Your peer ID is 12D3KooWFCXqmQTwvpfYFWK3DjXChEc4NoPt8pp5jjC8REZ3g6NZ\n```\n\nCongrats! Now you have ThreadDB running locally.\n\n#### Configuration values\n\nNote the various configuration values shown in the output above. These can be modified with environment variables show below.\n\n-   ***`THRDS_REPO`***: Repo location. Mandatory when launching from docker compose.\n-   ***`THRDS_HOSTADDR`***: Libp2p host bind address. `/ip4/0.0.0.0/tcp/4006` by default.\n-   ***`THRDS_APIADDR`***: gRPC API bind address. `/ip4/0.0.0.0/tcp/6006` by default.\n-   ***`THRDS_APIPROXYADDR`***: gRPC API web proxy bind address. `/ip4/0.0.0.0/tcp/6007` by default.\n-   ***`THRDS_CONNLOWWATER`***: Low watermark of libp2p connections that'll be maintained. `100` by default.\n-   ***`THRDS_CONNHIGHWATER`***: High watermark of libp2p connections that'll be maintained. `400` by default.\n-   ***`THRDS_CONNGRACEPERIOD`***: Duration a new opened connection is not subject to pruning. `20` seconds by default.\n-   ***`THRDS_KEEPALIVEINTERVAL`***: Websocket keepalive interval (must be \u003e= 1s). `5` seconds by default.\n-   ***`THRDS_ENABLENETPUBSUB`***: Enables thread networking over libp2p pubsub. `false` by default.\n-   ***`THRDS_DEBUG`***: Enables debug logging. `false` by default.\n\n### The DB API\n\nThe database layer is a document store, which internally leverages the `net` API. Most applications will only interface with this layer.\n\nThe full API spec is available [here](https://pkg.go.dev/github.com/textileio/go-threads/api/client).\n\nAs described in the [paper](https://docsend.com/view/gu3ywqi), ThreadDB's network layer orchestrates groups of event logs, or _threads_. In the current implementation, a single database leverages a single network-layer thread for state orchestration.\n\n#### Starting the client\n\n```go\nimport \"github.com/textileio/go-threads/api/client\"\n...\n\ndb, err := client.NewClient(\"127.0.0.1:6006\", grpc.WithInsecure())\n```\n\n#### Getting a thread token\n\nThread _tokens_ ([JWTs](https://jwt.io/)) are used by the daemon to determine the _identity_ of the caller. Most APIs take a thread token as an optional argument, since whether or not they are needed usually depends on how the target collection is configured (see [Write Validation](#write-validation) and [Read Filtering](#read-filtering)). These tokens are obtained by performing a signing challenge with the daemon using a libp2p private key.\n\n```go\nprivateKey, _, err := crypto.GenerateEd25519Key(rand.Reader) // Private key is kept locally\nmyIdentity := thread.NewLibp2pIdentity(privateKey)\n\nthreadToken, err := db.GetToken(context.Background(), myIdentity)\n```\n\n#### Creating a new DB\n\n```go\nthreadID := thread.NewIDV1(thread.Raw, 32)\nerr := db.NewDB(context.Background(), threadID)\n```\n\n#### Creating a new DB from an existing address\n\nAn existing DB can be added to a different daemon by providing a valid host address and thread key.\n\n```go\nthreadID := thread.NewIDV1(thread.Raw, 32)\nerr := db1.NewDB(context.Background(), threadID)\ndbInfo, err := db1.GetDBInfo(context.Background(), threadID)\n...\n\n// db2 is a different client (this would normally be done on a different machine)\nerr := db2.NewDBFromAddr(context.Background(), dbInfo.Addrs[0], dbInfo.Key)\n```\n\n#### Creating a collection\n\nCollections are groups of documents or _instances_ and are analogous to tables in relational databases. Creating a collection involves defining the following configuration parameters:\n\n-   ***`Name`***: The name of the collection, e.g, \"Animals\" (must be unique per DB).\n-   ***`Schema`***: A [JSON Schema](https://json-schema.org/)), which is used for instance validation.\n-   ***`Indexes`***: An optional list of index configurations, which define how instances are indexed.\n-   ***`WriteValidator`***: An optional JavaScript (ECMAScript 5.1) function that is used to validate instances on write.\n-   ***`ReadFilter`***: An optional JavaScript (ECMAScript 5.1) function that is used to filter instances on read.\n\n##### Write Validation\n\nThe `WriteValidator` function receives three arguments:\n\n-   `writer`: The multibase-encoded public key identity of the writer.\n-   `event`: An object describing the update event (see [`core.db.Event`](https://pkg.go.dev/github.com/textileio/go-threads/core/db#Event)).\n-   `instance`: The current instance as a JavaScript object before the update event is applied.\n\nA [falsy](https://developer.mozilla.org/en-US/docs/Glossary/Falsy) return value indicates a failed validation.\n\nHaving access to `writer`, `event`, and `instance` opens the door to a variety of app-specific logic. Textile Buckets file-level access roles are implemented in part with a write validator.\n\n##### Read Filtering\n\nThe function receives three arguments:\n\n-   `reader`: The multibase-encoded public key identity of the reader.\n-   `instance`: The current instance as a JavaScript object.\n\nThe function must return a JavaScript object. Most implementation will modify and return the current instance.\n\nLike write validation, read filtering opens the door to a variety of app-specific logic. Textile Buckets file-level access roles are implemented in part with a read filter.\n\n```go\nimport \"github.com/alecthomas/jsonschema\"\n...\n\n// We can use a struct to define a collection schema\ntype Person struct {\n    ID        string `json:\"_id\"`\n    Name      string `json:\"name\"`\n    Age       int    `json:\"age\"`\n    CreatedAt int    `json:\"created_at\"`\n}\n\nreflector := jsonschema.Reflector{}\nmySchema = reflector.Reflect(\u0026Person{}) // Generate a JSON Schema from a struct\n\nerr := db.NewCollection(context.Background(), myThreadID, db.CollectionConfig{\n    Name:    \"Persons\",\n    Schema:  mySchema,\n    Indexes: []db.Index{{\n        Path:   \"name\", // Value matches json tags\n\t\tUnique: true, // Create a unique index on \"name\"\n    }},\n})\n\n...\n\n// We can use the same schema to create more collections.\nerr := db.NewCollection(context.Background(), myThreadID, db.CollectionConfig{\n    Name:    \"Persons\",\n    Schema:  mySchema,\n    Indexes: []db.Index{{\n        Path:   \"name\",\n\t\tUnique: true,\n    }},\n    WriteValidator: \n        var type = event.patch.type\n        var patch = event.patch.json_patch\n        switch (type) {\n          case \"delete\":\n            if (writer != \"the_boss\") {\n              return false // Not the boss? No deletes for you.\n            }\n          default:\n            return true\n        }\n    ,\n    ReadFilter: \n        if (instance.Age \u003e 50) {\n            delete instance.Age // Getting old, let's hide just _how_ old hehe\n        }\n        return instance\n    ,\n})\n```\n\n#### Updating a collection\n\nEach of the collection configuration parameters above can be updated.\n\n```go\n...\n\nerr := db.UpdateCollection(context.Background(), myThreadID, db.CollectionConfig{\n    Name:    \"Persons\",\n    Schema:  mySchema,\n    Indexes: []db.Index{{\n        Path:   \"name\",\n        Unique: true,\n    },\n    {\n        Path: \"created_at\", // Add an additional index on \"created_at\"\n    }},\n})\n```\n\n#### Creating an instance\n\nCreating a collection instance is analogous to inserting a row in a relational database table.\n\n```go\n...\n\n// ID is autogenerated when omitted\nalice := \u0026Person{\n    ID:        \"\",\n    Name:      \"Alice\",\n    Age:       30,\n    CreatedAt: time.Now().UnixNano(),\n}\n\nids, err := db.Create(context.Background(), threadID, \"Persons\", Instances{alice})\n\nalice.ID = ids[0] // ids contains autogenerated instance identifiers\n\n// We can also define a custom ID, it just has to be a collection-wide unique string\nbob := \u0026Person{\n    ID:        \"123\",\n    Name:      \"Bob\",\n    Age:       30,\n    CreatedAt: time.Now().UnixNano(),\n}\n\nids, err := db.Create(context.Background(), threadID, \"Persons\", Instances{bob})\n```\n\n#### Saving an instance\n\nSimilarly, we can update an instance with new values.\n\n```go\n...\n\nalice.Age = 31\nerr = db.Save(context.Background(), threadID, \"Persons\", Instances{alice})\n```\n\n#### Collection queries\n\nThere are three methods to query for collection instances: ***`Find`***, ***`FindByID`***, and ***`Has`***. As usual, queries are enhanced by indexes.\n\nCheck out [`db.Query`](https://pkg.go.dev/github.com/textileio/go-threads/db#Query) and [`db.Criterion`](https://pkg.go.dev/github.com/textileio/go-threads/db#Criterion) for more about constructing queries and ordering results.\n\n```go\n...\n\n// Find instances with a query\nquery := db.Where(\"name\").Eq(\"Alice\")\nresults, err := db.Find(context.Background(), threadID, \"Persons\", query, \u0026Person{})\n\nalice := results[0].(*Person)\n\n...\n\n// Find an instance by ID\nalice := \u0026Person{}\nerr = db.FindByID(context.Background(), threadID, \"Persons\", aliceID, alice)\n\n...\n\n// Determine if an instance exists by ID\nexists, err := db.Has(context.Background(), threadID, \"Persons\", []string{aliceID})\n```\n\n#### Transactions\n\nThreadDB transactions come in two flavors: `WriteTransaction` and `ReadTransaction`.\n\n##### Write transactions\n\n```go\n...\n\ntxn, err := db.WriteTransaction(context.Background(), threadID, \"Persons\")\nend, err := txn.Start()\n\nalice.Age = 32\nerr = txn.Save(alice)\n\nerr = txn.Create(\u0026Person{\n    Name:      \"Bob\",\n    Age:       30,\n    CreatedAt: time.Now().UnixNano(),\n})\n\nend() // Done writing, commit transaction updates\n```\n\n##### Read transactions\n\n```go\n...\n\ntxn, err := db.ReadTransaction(context.Background(), threadID, \"Persons\")\nend, err := txn.Start()\n\nhasAlice, err := txn.Has(alice.ID)\n\nresults, err := txn.Find(db.Where(\"name\").Eq(\"Bob\"), \u0026Person{})\n\nbob := results[0].(*Person)\n\nend() // Done reading\n```\n\n#### Listening for collection changes\n\nWe can listen for DB changes on three levels: DB, collection, or instance.\n\nCheck out [ListenOption](https://pkg.go.dev/github.com/textileio/go-threads/api/client#ListenOption) for more.\n\n```go\n...\n\nctx, cancel := context.WithCancel(context.Background())\ndefer cancel()\nevents, err := db.Listen(ctx, threadID, []db.ListenOption{{\n    Type: client.ListenAll,\n    Collection: \"Persons\",  // Omit to receive events from all collections\n    InstanceID: bob.ID,     // Omit to receive events from all instances\n}})\n\nfor event := range events {\n    // Handle event\n}\n```\n\n### The Network API\n\nThe network layer maintains and orchestrates append-only event logs between network participants and is used internally by the database layer. Some applications, like event logging, may choose to rely on this layer directly.\n\nThe full API spec is available [here](https://pkg.go.dev/github.com/textileio/go-threads/net/api/client).\n\n#### Starting the client\n\n```go\nimport \"github.com/textileio/go-threads/net/api/client\"\n...\n\nnet, err := client.NewClient(\"127.0.0.1:6006\", grpc.WithInsecure())\n```\n\n#### Getting a thread token\n\nThread _tokens_ ([JWTs](https://jwt.io/)) are used by the daemon to determine the _identity_ of the caller. Most APIs take a thread token as an optional argument.\n\n```go\nprivateKey, _, err := crypto.GenerateEd25519Key(rand.Reader) // Private key is kept locally\nmyIdentity := thread.NewLibp2pIdentity(privateKey)\n\nthreadToken, err := net.GetToken(context.Background(), myIdentity)\n```\n\n#### Creating a thread\n\n```go\nthreadID := thread.NewIDV1(thread.Raw, 32)\nthreadInfo, err := net.CreateThread(context.Background(), threadID)\n```\n\n#### Adding an existing thread\n\nAn existing thread can be added to a different daemon by providing a valid host address and thread key.\n\n```go\nthreadID := thread.NewIDV1(thread.Raw, 32)\nthreadInfo1, err := net1.CreateThread(context.Background(), threadID)\n...\n\n// net2 is a different client (this would normally be done on a different machine)\nthreadInfo2, err := net2.AddThread(context.Background(), threadInfo1.Addrs[0], core.WithThreadKey(threadInfo1.Key))\n```\n\n#### Adding a thread replicator\n\nWe can replicate a thread on a different host. All logs and records are pushed to the new host. However, it will not be able to read them since it won't receive _read_ portion of the thread key.\n\n```go\nthreadID := thread.NewIDV1(thread.Raw, 32)\nthreadInfo, err := net1.CreateThread(context.Background(), threadID)\n\nreplicatorAddr, err := multiaddr.NewMultiaddr(\"/ip4/\u003cREPLICATOR_IP_ADDRESS\u003e/tcp/4006/p2p/\u003cREPLICATOR_PEER_ID\u003e\")\nreplicatorID, err := net.AddReplicator(context.Background(), threadID, replicatorAddr)\n```\n\n#### Creating a thread record\n\nA thread record can have any body.\n\n```go\nimport ipldcbor \"github.com/ipfs/go-ipld-cbor\"\n...\n\nbody, err := ipldcbor.WrapObject(map[string]interface{}{\n    \"foo\": \"bar\",\n    \"baz\": []byte(\"howdy\"),\n}, multihash.SHA2_256, -1)\n\t\nrecord, err := net.CreateRecord(context.Background(), threadID, body)\n```\n\n#### Adding a thread record\n\nWe can also retain control over the _read_ portion of the thread key and the log private key and create records _locally_.\n\n```go\nimport ipldcbor \"github.com/ipfs/go-ipld-cbor\"\n...\n\nprivateKey, _, err := crypto.GenerateEd25519Key(rand.Reader)\nmyIdentity := thread.NewLibp2pIdentity(privateKey)\n\nthreadToken, err := net.GetToken(context.Background(), myIdentity)\n\nthreadID := thread.NewIDV1(thread.Raw, 32)\nthreadKey := thread.NewRandomKey()\nlogPrivateKey, logPublicKey, err := crypto.GenerateEd25519Key(rand.Reader)\nlogID, err := peer.IDFromPublicKey(logPublicKey)\n\nthreadInfo, err := net.CreateThread(\n    context.Background(),\n    threadID,\n    core.WithThreadKey(thread.NewServiceKey(threadKey.Service())), // Read key is kept locally\n    core.WithLogKey(logPublicKey),                                 // Private key is kept locally\n    core.WithNewThreadToken(threadToken))                          // Thread token for identity is needed to verify records\n\nbody, err := ipldcbor.WrapObject(map[string]interface{}{\n    \"foo\": \"bar\",\n    \"baz\": []byte(\"howdy\"),\n}, mh.SHA2_256, -1)\n\n// Create the event locally\nevent, err := cbor.CreateEvent(context.Background(), nil, body, threadKey.Read())\n\n// Create the record locally\nrecord, err := cbor.CreateRecord(context.Background(), nil, cbor.CreateRecordConfig{\n\tBlock:      event,\n\tPrev:       cid.Undef,              // No previous records because this is the first\n\tKey:        logPrivateKey,\n\tPubKey:     myIdentity.GetPublic(),\n\tServiceKey: threadKey.Service(),\n})\n\nerr = net.AddRecord(context.Background(), threadID, logID, record)\n```\n\n#### Pulling a thread for new records\n\nAlthough all known hosts of a particular thread are internally polled for new records (as part of the orchestration protocol), doing so manually can often be useful.\n\n```go\nerr := net.PullThread(context.Background(), info.ID)\n```\n\n#### Listening for new records\n\nWe can listen for new thread records across all or a subset of known threads.\n\n```go\nctx, cancel := context.WithCancel(context.Background())\ndefer cancel()\nrecords, err := net.Subscribe(ctx, core.WithSubFilter(threadID)) // Only receive new records from this thread\n\nfor record := range records {\n    // Handle record\n}\n```\n\n## Developing\n\nThe easiest way to develop against `threadsd` is to use the Docker Compose files. The `-dev` flavored file doesn't persist a repo via Docker Volumes, which may be desirable in some cases.\n\n## Contributing\n\nPull requests and bug reports are very welcome ❤️\n\nThis repository falls under the Textile [Code of Conduct](./CODE_OF_CONDUCT.md).\n\nFeel free to get in touch by:\n-   [Opening an issue](https://github.com/textileio/go-threads/issues/new)\n-   Joining the [public Slack channel](https://slack.textile.io/)\n-   Sending an email to contact@textile.io\n\n## Changelog\n\nA changelog is published along with each [release](https://github.com/textileio/go-threads/releases).\n\n## License\n\n[MIT](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftextileio%2Fgo-threads","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftextileio%2Fgo-threads","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftextileio%2Fgo-threads/lists"}