{"id":13588797,"url":"https://github.com/stacks-network/gaia","last_synced_at":"2025-03-20T01:09:56.219Z","repository":{"id":23155247,"uuid":"26510718","full_name":"stacks-network/gaia","owner":"stacks-network","description":"A decentralized high-performance storage system","archived":false,"fork":false,"pushed_at":"2025-03-11T20:17:21.000Z","size":24453,"stargazers_count":764,"open_issues_count":49,"forks_count":149,"subscribers_count":60,"default_branch":"master","last_synced_at":"2025-03-12T18:06:15.818Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","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/stacks-network.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","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}},"created_at":"2014-11-11T23:54:31.000Z","updated_at":"2024-12-17T07:30:26.000Z","dependencies_parsed_at":"2025-02-12T15:00:28.358Z","dependency_job_id":"ac01d22c-cd91-451a-94d2-00019ad97928","html_url":"https://github.com/stacks-network/gaia","commit_stats":null,"previous_names":["blockstack/registrar","blockstack/gaia"],"tags_count":18,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stacks-network%2Fgaia","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stacks-network%2Fgaia/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stacks-network%2Fgaia/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stacks-network%2Fgaia/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stacks-network","download_url":"https://codeload.github.com/stacks-network/gaia/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244531000,"owners_count":20467391,"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-08-01T15:06:56.333Z","updated_at":"2025-03-20T01:09:55.524Z","avatar_url":"https://github.com/stacks-network.png","language":"TypeScript","funding_links":[],"categories":["TypeScript","others"],"sub_categories":[],"readme":"Gaia: A decentralized high-performance storage system\n====================================\n\n[![codecov](https://codecov.io/gh/stacks-network/gaia/branch/master/graph/badge.svg)](https://app.codecov.io/gh/stacks-network/gaia)\n\nThis document describes the high-level design and implementation of the\nGaia storage system, also briefly explained in the [docs.stacks.co](https://docs.stacks.co/build-apps/references/gaia). It includes specifications for backend storage drivers and interactions between developer APIs and the Gaia service.\n\nDevelopers who wish to use the Gaia storage system should see the `stacks.js` documentation [here](https://stacks.js.org/) and in particular the storage package [here](https://github.com/hirosystems/stacks.js/tree/master/packages/storage).\n\nInstructions on setting up, configuring and testing a Gaia Hub can be found [here](https://github.com/stacks-network/gaia/blob/master/deploy/README.md) and [here](https://forum.stacks.org/t/tech-preview-using-your-own-gaia-hub-with-the-cli/6160).\n\n# Overview\n\nGaia works by hosting data in one or more existing storage systems of the user's choice.\nThese storage systems are typically cloud storage systems.  We currently have driver support\nfor S3 and Azure Blob Storage, but the driver model allows for other backend support\nas well. The point is, the user gets to choose where their data lives, and Gaia enables\napplications to access it via a uniform API.\n\nBlockstack applications use the Gaia storage system to store data on behalf of a user.\nWhen the user logs in to an application, the authentication process gives the application\nthe URL of a Gaia hub, which performs writes on behalf of that user. The Gaia hub authenticates\nwrites to a location by requiring a valid authentication token, generated by a private key\nauthorized to write at that location.\n\n# User Control: How is Gaia Decentralized?\n\nGaia's approach to decentralization focuses on user-control of data and storage. If a user\ncan choose which gaia hub and which backend provider to store data with, then that is all\nthe decentralization required to enable user-controlled applications.\n\nIn Gaia, the control of user data lies in the way that user data is accessed. When an application\nfetches a file `data.txt` for a given user `alice.id`, the lookup will follow these steps:\n\n1. Fetch the zonefile for `alice.id`, and read her profile URL from that zonefile\n2. Fetch the Alice's profile and _verify_ that it is signed by `alice.id`'s key\n3. Read the application root URL (e.g. `https://gaia.alice.org/`) out of the profile\n4. Fetch file from `https://gaia.alice.org/data.txt`\n\nBecause `alice.id` controls her zonefile, she can change where her profile is stored,\nif the current storage of the profile is compromised. Similarly, if Alice wishes to change\nher gaia provider, or run her own gaia node, she can change the entry in her profile.\n\nFor applications writing directly on behalf of Alice, they do not need to perform this\nlookup. Instead, the stack.js [authentication flow](https://github.com/hirosystems/stacks.js/blob/master/packages/auth/README.md#handling-an-authentication-response-payload) provides Alice's chosen application root\nURL to the application. This authentication flow _is also_ within Alice's control, because the\nauthentication response _must_ be generated by Alice's browser.\n\nWhile it is true that many Gaia hubs will use backend providers like AWS or Azure, allowing\nusers to easily operate their own hubs, which may select different backend providers (and we'd\nlike to implement more backend drivers), enables truly user-controlled data, while enabling\nhigh performance and high availability for data reads and writes.\n\n# Write-to and Read-from URL Guarantees\n\nA performance and simplicity oriented guarantee of the Gaia\nspecification is that when an application submits a _write_ to a URL\n`https://myhub.service.org/store/foo/bar`, the application is guaranteed to\nbe able to read from a URL `https://myreads.com/foo/bar`. While the\n_prefix_ of the read-from URL may change between the two, the suffix\nmust be the same as the write-to URL.\n\nThis allows an application to know _exactly_ where a written file can\nbe read from, given the read prefix. To obtain that read prefix,\nthe Gaia service defines an endpoint:\n\n```\nGET /hub_info/\n```\n\nwhich returns a JSON object with a `read_url_prefix`.\n\n\nFor example, if my service returns:\n\n```javascript\n{ ...,\n  \"read_url_prefix\": \"https://myservice.org/read/\"\n}\n```\n\nI know that if I submit a write request to:\n\n```\nhttps://myservice.org/store/1DHvWDj834zPAkwMhpXdYbCYh4PomwQfzz/0/profile.json\n```\n\nThat I will be able to read that file from:\n\n```\nhttps://myservice.org/read/1DHvWDj834zPAkwMhpXdYbCYh4PomwQfzz/0/profile.json\n```\n\n\n# Address-based Access-Control\n\nAccess control in a gaia storage hub is performed on a per-address\nbasis.  Writes to URLs `/store/\u003caddress\u003e/\u003cfile\u003e` are only allowed if\nthe writer can demonstrate that they control _that_ address. This is\nachieved via an authentication token, which is a message _signed_ by\nthe private-key associated with that address. The message itself is a\nchallenge-text, returned via the `/hub_info/` endpoint.\n\n## V1 Authentication Scheme\n\nThe V1 authentication scheme uses a JWT, prefixed with `v1:` as a\nbearer token in the HTTP authorization field. The expected JWT payload\nstructure is:\n\n```\n{\n 'type': 'object',\n 'properties': {\n   'iss': { 'type': 'string' },\n   'exp': { 'type': 'IntDate' },\n   'iat': { 'type': 'IntDate' },\n   'gaiaChallenge': { 'type': 'string' },\n   'associationToken': { 'type': 'string' },\n   'salt': { 'type': 'string' }\n }\n 'required': [ 'iss', 'gaiaChallenge' ]\n}\n```\n\nIn addition to `iss`, `exp`, and `gaiaChallenge` claims, clients may\nadd other properties (e.g., a `salt` field) to the payload, and they will\nnot affect the validity of the JWT. Rather, the validity of the JWT is checked\nby ensuring:\n\n1. That the JWT is signed correctly by verifying with the pubkey hex provided as\n`iss`\n2. That `iss` matches the address associated with the bucket.\n3. That `gaiaChallenge` is equal to the server's challenge text.\n4. That the epoch time `exp` is greater than the server's current epoch time.\n5. That the epoch time `iat` (issued-at date) is greater than the bucket's revocation date (only if such a date has been set by the bucket owner).\n\n### Association Tokens\n\nThe association token specification is considered private, as it is mostly used\nfor internal Gaia use cases. This means that this specification can change or\nbecome deprecated in the future.\n\nOften times, a single user will use many different keys to\nstore data.  These keys may be generated on-the-fly.  Instead of requiring the\nuser to explicitly whitelist each key, the v1 authentication scheme allows\nthe user to bind a key to an already-whitelisted key via an *association token*.\n\nAn association token is a JWT signed by a whitelisted key that, in turn,\ncontains the public key that signs the authentication JWT that contains it.  Put\nanother way, the Gaia hub will accept a v1 authentication JWT if it contains an\n`associationToken` JWT that (1) was sigend by a whitelisted address, and (2)\nidentifies the signer of the authentication JWT.\n\nThe association token JWT has the following structure in its payload:\n\n```\n{\n  'type': 'object',\n  'properties': {\n    'iss': { 'type': 'string' },\n    'exp': { 'type': 'IntDate' },\n    'iat': { 'type': 'IntDate' },\n    'childToAssociate': { 'type': 'string' },\n    'salt': { 'type': 'string' },\n  },\n  'required': [ 'iss', 'exp', 'childToAssociate' ]\n}\n```\n\nHere, the `iss` field should be the public key of a whitelisted address.\nThe `childtoAssociate` should be equal to the `iss` field of the authentication\nJWT.  Note that the `exp` field is *required* in association tokens.\n\n## Legacy authentication scheme\n\nIn more detail, this signed message is:\n\n```\nBASE64({ \"signature\" : ECDSA_SIGN(SHA256(challenge-text)),\n         \"publickey\" : PUBLICKEY_HEX })\n```\n\nCurrently, challenge-text must match the _known_ challenge-text on\nthe gaia storage hub. However, as future work enables more extensible\nforms of authentication, we could extend this to allow the auth\ntoken to include the challenge-text as well, which the gaia storage hub\nwould then need to also validate.\n\n\n# Data storage format\n\nA gaia storage hub will store the written data _exactly_ as\ngiven. This means that the storage hub _does not_ provide many\ndifferent kinds of guarantees about the data. It does not ensure that\ndata is validly formatted, contains valid signatures, or is\nencrypted. Rather, the design philosophy is that these concerns are\nclient-side concerns. Client libraries (such as `stacks.js`) are\ncapable of providing these guarantees, and we use a liberal definition of\nthe end-to-end principle to guide this design decision.\n\n# Operation of a Gaia Hub\n\n## Configuration files\n\nA configuration TOML/JSON file should be stored either in the top-level directory\nof the hub server, or a file location may be specified in the environment\nvariable `CONFIG_PATH`.\n\nAn example configuration file is provided in (./hub/config.sample.json)\nYou can specify the logging level, the number of social proofs required\nfor addresses to write to the system, the backend driver, the credentials\nfor that backend driver, and the readURL for the storage provider.\n\n### Private hubs\n\nA private hub services requests for a single user. This is controlled\nvia _whitelisting_ the addresses allowed to write files. In order to\nsupport application storage, because each application uses a different\napp- and user-specific address, each application you wish to use must\nbe added to the whitelist separately.\n\nAlternatively, the user's client must use the v1 authentication scheme and generate\nan association token for each app.  The user should whitelist her address, and\nuse her associated private key to sign each app's association token.  This\nremoves the need to whitelist each application, but with the caveat that the\nuser needs to take care that her association tokens do not get misused.\n\n### Open-membership hubs\n\nAn open-membership hub will allow writes for _any_ address top-level directory,\neach request will still be validated such that write requests must provide valid\nauthentication tokens for that address. Operating in this mode is recommended\nfor service and identity providers who wish to support many different users.\n\nIn order to limit the users that may interact with such a hub to users\nwho provide social proofs of identity, we support an execution mode\nwhere the hub checks that a user's profile.json object contains\n_social proofs_ in order to be able to write to other locations. This\ncan be configured via the `config.json` or `config.toml`.\n\n# Driver model\n\nGaia hub drivers are fairly simple. The biggest requirement is the ability\nto fulfill the _write-to/read-from_ URL guarantee. \n\nA driver can expect that two modification operations to the same path will be mutually exclusive. \nNo writes, renames, or deletes to the same path will be concurrent.\n\nAs currently implemented\na gaia hub driver must implement the following functions:\n\n\n```ts\ninterface DriverModel {\n\n  /**\n   * Return the prefix for reading files from.\n   *  a write to the path `foo` should be readable from\n   *  `${getReadURLPrefix()}foo`\n   * @returns the read url prefix.\n   */\n  getReadURLPrefix(): string;\n\n  /**\n   * Performs the actual write of a file to `path`\n   *   the file must be readable at `${getReadURLPrefix()}/${storageToplevel}/${path}`\n   *\n   * @param options.path - path of the file.\n   * @param options.storageToplevel - the top level directory to store the file in\n   * @param options.contentType - the HTTP content-type of the file\n   * @param options.stream - the data to be stored at `path`\n   * @param options.contentLength - the bytes of content in the stream\n   * @param options.ifMatch - optional etag value to be used for optimistic concurrency control\n   * @param options.ifNoneMatch - used with the `*` value to save a file not known to exist,\n   * guaranteeing that another upload didn't happen before, losing the data of the previous\n   * @returns Promise that resolves to an object containing a public-readable URL of the stored content and the objects etag value\n   */\n  performWrite(options: {\n    path: string;\n    storageTopLevel: string;\n    stream: Readable;\n    contentLength: number;\n    contentType: string;\n    ifMatch?: string;\n    ifNoneMatch?: string;\n  }): Promise\u003c{\n    publicURL: string,\n    etag: string\n  }\u003e;\n\n  /**\n   * Deletes a file. Throws a `DoesNotExist` if the file does not exist. \n   * @param options.path - path of the file\n   * @param options.storageTopLevel - the top level directory\n   * @param  options.contentType - the HTTP content-type of the file\n   */\n  performDelete(options: {\n    path: string;\n    storageTopLevel: string;\n  }): Promise\u003cvoid\u003e;\n\n  /**\n   * Renames a file given a path. Some implementations do not support\n   * a first class move operation and this can be implemented as a copy and delete. \n   * @param options.path - path of the original file\n   * @param options.storageTopLevel - the top level directory for the original file\n   * @param options.newPath - new path for the file\n   */\n  performRename(options: {\n    path: string;\n    storageTopLevel: string;\n    newPath: string;\n  }): Promise\u003cvoid\u003e;\n\n  /**\n   * Retrieves metadata for a given file.\n   * @param options.path - path of the file\n   * @param options.storageTopLevel - the top level directory\n   */\n  performStat(options: {\n    path: string;\n    storageTopLevel: string;\n  }): Promise\u003c{\n    exists: boolean;\n    lastModifiedDate: number;\n    contentLength: number;\n    contentType: string;\n    etag: string;\n  }\u003e;\n\n  /**\n   * Returns an object with a NodeJS stream.Readable for the file content\n   * and metadata about the file.\n   * @param options.path - path of the file\n   * @param options.storageTopLevel - the top level directory\n   */\n  performRead(options: {\n    path: string;\n    storageTopLevel: string;\n  }): Promise\u003c{\n    data: Readable;\n    lastModifiedDate: number;\n    contentLength: number;\n    contentType: string;\n    etag: string;\n  }\u003e;\n\n  /**\n   * Return a list of files beginning with the given prefix,\n   * as well as a driver-specific page identifier for requesting\n   * the next page of entries.  The return structure should\n   * take the form { \"entries\": [string], \"page\"?: string }\n   * @returns {Promise} the list of files and a possible page identifier.\n   */\n  listFiles(options: {\n    pathPrefix: string;\n    page?: string;\n  }): Promise\u003c{\n    entries: string[];\n    page?: string;\n  }\u003e;\n\n  /**\n   * Return a list of files beginning with the given prefix,\n   * as well as file metadata, and a driver-specific page identifier\n   * for requesting the next page of entries.\n   */\n  listFilesStat(options: {\n    pathPrefix: string;\n    page?: string;\n  }): Promise\u003c{\n    entries: {\n        name: string;\n        lastModifiedDate: number;\n        contentLength: number;\n        etag: string;\n    }[];\n    page?: string;\n  }\u003e;\n  \n}\n```\n\n# HTTP API\n\nThe Gaia storage API defines the following endpoints:\n\n---\n\n##### `GET ${read-url-prefix}/${address}/${path}`\n\nThis returns the data stored by the gaia hub at `${path}`.\nThe response headers include `Content-Type` and `ETag`, along with\nthe required CORS headers `Access-Control-Allow-Origin` and `Access-Control-Allow-Methods`.\n\n---\n\n##### `HEAD ${read-url-prefix}/${address}/${path}`\n\nReturns the same headers as the corresponding `GET` request. `HEAD` requests\ndo not return a response body. \n\n---\n\n##### `POST ${hubUrl}/store/${address}/${path}`\n\nThis performs a write to the gaia hub at `${path}`. \n\nOn success, it returns a `202` status, and a JSON object:\n\n```javascript\n{\n \"publicURL\": \"${read-url-prefix}/${address}/${path}\",\n \"etag\": \"version-identifier\"\n}\n```\n\nThe `POST` must contain an authentication header with a bearer token.\nThe bearer token's content and generation is described in\nthe [access control](#address-based-access-control) section of this\ndocument.\n\nAdditionally, file ETags and conditional request headers are used as a \nconcurrency control mechanism. All requests to this endpoint should contain\neither an `If-Match` header or an `If-None-Match` header. The three request\ntypes are as follows:\n\n__Update existing file__: this request must specify an `If-Match` header \ncontaining the most up to date ETag. If the file has been updated elsewhere \nand the ETag supplied in the `If-Match` header doesn't match that of the file \nin gaia, a `412 Precondition Failed` error will be returned.\n\n__Create a new file__: this request must specify the `If-None-Match: *` \nheader. If the already exists at the given path, a `412 Precondition Failed` \nerror will be returned.\n\n__Overwrite a file__: this request must specify the `If-Match: *` header. \n__Note__ that this bypasses concurrency control and should be used with \ncaution. Improper use can cause bugs such as unintended data loss. \n\n\nThe file ETag is returned in the response body of the _store_ `POST` request, the \nresponse headers of `GET` and `HEAD` requests, and in the returned entries in \n`list-files` request. \n\n\nAdditionally, a request to a file path that already has a previous ongoing \nrequest still processing for the same file path will return with a \n`409 Conflict` error. This can be handled with a retry. \n\n---\n\n##### `DELETE ${hubUrl}/delete/${address}/${path}`\n\nThis performs a deletion of a file in the gaia hub at `${path}`. \n\nOn success, it returns a `202` status. Returns a `404` if the path does \nnot exist. Returns `400` if the path is invalid. \n\nThe `DELETE` must contain an authentication header with a bearer token.\nThe bearer token's content and generation is described in\nthe [access control](#address-based-access-control) section of this\ndocument.\n\n---\n\n##### `GET ${hubUrl}/hub_info/`\n\nReturns a JSON object:\n\n```javascript\n{\n \"challenge_text\": \"text-which-must-be-signed-to-validate-requests\",\n \"read_url_prefix\": \"${read-url-prefix}\"\n \"latest_auth_version\": \"v1\"\n}\n```\n\nThe latest auth version allows the client to figure out which auth versions the\ngaia hub supports.\n\n---\n\n##### `POST ${hubUrl}/revoke-all/${address}`\n\nThe post body must be a JSON object with the following field:\n```json\n{ \"oldestValidTimestamp\": \"${timestamp}\" }\n```\nWhere the `timestamp` is an epoch time in seconds. The timestamp is written\nto a bucket-specific file (`/${address}-auth`). This becomes the oldest valid \n`iat` timestamp for authentication tokens that write to the `/${address}/` bucket.\n\nOn success, it returns a `202` status, and a JSON object:\n\n```json\n{ \"status\": \"success\" }\n```\n\nThe `POST` must contain an authentication header with a bearer token.\nThe bearer token's content and generation is described in\nthe [access control](#address-based-access-control) section of this\ndocument.\n\n---\n\n##### `POST ${hubUrl}/list-files/${address}`\n\nThe post body can contain a `page` field with the pagination identifier from a previous request:\n```json\n{ \"page\": \"${lastListFilesResult.page}\" }\n```\nIf the post body contains a `stat: true` field then the returned JSON includes file metadata:\n```jsonc\n{\n  \"entries\": [\n    { \"name\": \"string\", \"lastModifiedDate\": \"number\", \"contentLength\": \"number\", \"etag\": \"string\" },\n    { \"name\": \"string\", \"lastModifiedDate\": \"number\", \"contentLength\": \"number\", \"etag\": \"string\" },\n    // ...\n  ],\n  \"page\": \"string\" // possible pagination marker\n}\n```\n\nIf the post body does not contain a `stat: true` field then the returned JSON entries will only be\nfile name strings:\n```jsonc\n{\n  \"entries\": [\n    \"fileNameExample1\",\n    \"fileNameExample2\",\n    // ...\n  ],\n  \"page\": \"string\" // possible pagination marker\n}\n```\n\nThe `POST` must contain an authentication header with a bearer token.\nThe bearer token's content and generation is described in\nthe [access control](#address-based-access-control) section of this\ndocument.\n\n-----\n\n# Future Design Goals\n\n## Dependency on DNS\n\nThe gaia specification requires that a gaia hub return a URL that a user's client\nwill be able to fetch. In practice, most gaia hubs will use URLs with DNS entries\nfor hostnames (though URLs with IP addresses would work as well). However, even\nthough the spec uses URLs, that doesn't necessarily make an opinionated claim on\nunderlying mechanisms for that URL. If a browser supported new URL schemes which\nenabled lookups without traditional DNS (for example, with the Blockstack Name\nSystem instead), then gaia hubs could return URLs implementing that scheme. As\nthe Blockstack ecosystem develops and supports these kinds of features, we expect\nusers would deploy gaia hubs that would take advantage.\n\n## Extensibly limiting membership sets\n\nSome service providers may wish to provide hub services to a limited set of\ndifferent users, with a provider-specific method of authenticating that a user\nor address is within that set. In order to provide that functionality, our hub\nimplementation would need to be extensible enough to allow plugging in different\nauthentication models.\n\n## A `.storage` Namespace\n\n\u003c!--- Blockstack Core no longer does this, but Gaia nodes do.  I updated this\nsection to reflect that (jcn) --\u003e\n\nGaia nodes can request data from other Gaia nodes, and can store data\nto other Gaia nodes.  In effect, Gaia nodes can be \"chained together\"\nin arbitrarily complex ways.  This creates an opportunity to create\na decentralized storage marketplace.\n\n### Example\n\nFor example, Alice can make her Gaia node public and program it to\nstore data to her Amazon S3 bucket and her Dropbox account.  Bob can then POST data to Alice's \nnode, causing her node to replicate data to both providers.  Later, Charlie can\nread Bob's data from Alice's node, causing Alice's node to fetch and serve back\nthe data from her cloud storage.  Neither Bob nor Charlie have to set up accounts on\nAmazon S3 and Dropbox this way, since Alice's node serves as an intermediary\nbetween them and the storage providers.\n\nSince Alice is on the read/write path between Bob and Charlie and cloud storage,\nshe has the opportunity to make optimizations.  First, she can program her\nGaia node to synchronously write data to\nlocal disk and asynchronously back it up to S3 and Dropbox.  This would speed up\nBob's writes, but at the cost of durability (i.e. Alice's node could crash\nbefore replicating to the cloud).\n\nIn addition, Alice can program her Gaia node to service all reads from disk.  This\nwould speed up Charlie's reads, since he'll get the latest data without having\nto hit back-end cloud storage providers.\n\n### Service Description\n\nSince Alice is providing a service to Bob and Charlie, she will want\ncompensation.  This can be achieved by having both of them send her money via\nthe underlying blockchain.\n\nTo do so, she would register her node's IP address in a\n`.storage` namespace in Blockstack, and post her rates per gigabyte in her node's\nprofile and her payment address.  Once Bob and Charlie sent her payment, her\nnode would begin accepting reads and writes from them up to the capacity\npurchased.  They would continue sending payments as long as Alice provides them\nwith service.\n\nOther experienced Gaia node operators would register their nodes in `.storage`, and\ncompete for users by offerring better durability, availability, performance,\nextra storage features, and so on.\n\n# Notes on our deployed service\n\nOur deployed service places some modest limitations on file uploads and rate limits.\nCurrently, the service will only allow up to 20 write requests per second and a maximum\nfile size of 5MB. However, these limitations are only for our service, if you deploy\nyour own Gaia hub, these limitations are not necessary.\n\n# Project Comparison\n\nHere's how Gaia stacks up against other decentralized storage systems.  Features\nthat are common to all storage systems are omitted for brevity.\n\n| **Features**                | Gaia | [Sia](https://sia.tech/) | [Storj](https://storj.io/) | [IPFS](https://ipfs.io) | [DAT](https://datproject.org) | [SSB](https://www.scuttlebutt.nz/) |\n|-----------------------------|------|--------------------------|----------------------------|-------------------------|-------------------------------|------------------------------------|\n| User controls where data is hosted           | X |   |   |   |   |   |\n| Data can be viewed in a normal Web browser   | X |   |   | X |   |   |\n| Data is read/write                           | X |   |   |   | X | X |\n| Data can be deleted                          | X |   |   |   | X | X |\n| Data can be listed                           | X | X | X |   | X | X |\n| Deleted data space is reclaimed              | X | X | X | X |   |   | \n| Data lookups have predictable performance    | X |   | X |   |   |   |\n| Writes permission can be delegated           | X |   |   |   |   |   |\n| Listing permission can be delegated          | X |   |   |   |   |   |\n| Supports multiple backends natively          | X |   | X |   |   |   |\n| Data is globally addressable                 | X | X | X | X | X |   | \n| Needs a cryptocurrency to work               |   | X | X |   |   |   |\n| Data is content-addressed                    |   | X | X | X | X | X |\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstacks-network%2Fgaia","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstacks-network%2Fgaia","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstacks-network%2Fgaia/lists"}