{"id":32167994,"url":"https://github.com/skunkwerks/sofa","last_synced_at":"2026-02-21T03:31:04.761Z","repository":{"id":52254829,"uuid":"356275123","full_name":"skunkwerks/sofa","owner":"skunkwerks","description":"Yet Another Elixir CouchDB Client.","archived":false,"fork":false,"pushed_at":"2025-03-18T23:38:39.000Z","size":73,"stargazers_count":1,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-21T15:42:41.532Z","etag":null,"topics":["couchdb","couchdb-api","couchdb-client","elixir"],"latest_commit_sha":null,"homepage":"https://hexdocs.pm/sofa","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-2-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/skunkwerks.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,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-04-09T13:09:52.000Z","updated_at":"2025-07-24T01:03:31.000Z","dependencies_parsed_at":"2025-03-18T23:15:34.737Z","dependency_job_id":null,"html_url":"https://github.com/skunkwerks/sofa","commit_stats":null,"previous_names":["skunkwerks/sofa"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/skunkwerks/sofa","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skunkwerks%2Fsofa","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skunkwerks%2Fsofa/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skunkwerks%2Fsofa/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skunkwerks%2Fsofa/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/skunkwerks","download_url":"https://codeload.github.com/skunkwerks/sofa/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skunkwerks%2Fsofa/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29672704,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-21T03:11:15.450Z","status":"ssl_error","status_checked_at":"2026-02-21T03:10:34.920Z","response_time":107,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":["couchdb","couchdb-api","couchdb-client","elixir"],"created_at":"2025-10-21T15:42:10.774Z","updated_at":"2026-02-21T03:31:04.751Z","avatar_url":"https://github.com/skunkwerks.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Sofa\n\nA straightforwards, idiomatic [CouchDB] client.\n\nThe intention is to provide an idiomatic Elixir client, that can play\nnicely with Ecto, Maps, and in particular, Structs and Protocols. You\nshould be able to store a Struct in CouchDB, and have it come back to\nyou as a Struct again, assuming you're not doing anything too messy,\nsuch as nested structs, or trying to store pids, refs, and other\ndistinctly non-JSON things.\n\n## Installation\n\nIt is recommended to use a `Tesla.Adapter`. While in principle these are\nall equivalent, in practice, their patterns for handling query\nparameters, headers, empty HTTP bodies, IPv6, and generally dealing with\n`nil`, `true`, `false` and so forth mean that they are not created\nequal. This library should work, in most cases transparently, and if\nnot, we welcome tests and converters to address any shortcomings.\n\nSofa makes no guarantees about specific HTTP modules, but should run\nwith:\n\n- default Erlang `httpc` \"no dependencies!\"\n- Mint and Finch\n\nThe package can be installed by adding `sofa` to your list of\ndependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:sofa, \"~\u003e 0.1.0\"}\n  ]\nend\n```\n\n```elixir\n# config/config.exs\nimport Config\n\nif config_env() == :test do\n  config :tesla, adapter: Tesla.Mock\nelse\n  config :tesla, adapter: Tesla.Adapter.Mint\nend\n```\n\n## Docs\n\n- [hexdocs] as usual has all the goodies\n- the [CouchDB API] should map very closely to Sofa\n\nSofa really only has 2 important abstractions that live above the\nCouchDB API:\n\n- `%Sofa{}` aka `Sofa.t()` which is a struct that wraps your HTTP API\n    connection, along with any custom headers \u0026 settings you may\n    require, and the returned data from the CouchDB server you connect\n    to, including feature flags and vendor settings. As a convenience,\n    it also doubles as your \"database\" struct, as that's really only a\n    single additional field to be inserted into the CouchDB URL\n- `%Sofa.Doc{}` aka `Sofa.Doc.t()` which is the main struct you'll work\n    with. We've tried to keep it as close to the [CouchDB API] as possible,\n    so aside from `id`, `rev`, and the `attachments` stubs, all the\n    JSON is contained in a `body` and Sofa keeps out of your way.\n\n## Road Map\n\nWhile not yet implemented, Sofa wants to support \"native\" Elixir struct\nusage, where you implement the Protocol to convert your custom Struct\nto/from Sofa, and Sofa will use the `type` key that is commonly used in\nCouchDB to detect \u0026 marshall your Struct directly to/from CouchDB's JSON\nAPI transparently.\n\n- [x] server:   `Sofa.*`\n- [x] raw HTTP: `Sofa.Raw.*`\n- [x] database: `Sofa.DB.*`\n- [x] document: `Sofa.Doc.*`\n- [x] user:     `Sofa.User.*`\n- [ ] attachments\n- [ ] transparent Struct API\n- [ ] view:     `Sofa.View.*`\n- [ ] changes:  `Sofa.Changes.*`\n- [ ] timeouts for requests and inactivity\n- [ ] bearer token authorisation\n- [ ] runtime tracing filterable by method \u0026 URL\n- [ ] embeddable within CouchDB BEAM runtime\n- [ ] native CouchDB erlang term support\n\n## Usage\n\n### Connecting to CouchDB\n\n`Sofa.init/1` and `Sofa.client/1` are effectively static structures, so\nyou can build them at compile time, or store them efficiently in ETS\ntables, or `persistent_term` for faster access.\n\n`Sofa.connect!/1` needs access to the CouchDB server, to verify that\nyour credentials are sufficient, and to retrieve feature flags and\nvendor settings.\n\n\u003e Exactly how you use this, is dependent on your `Tesla.Adapter` and\n\u003e supervision trees. Make sure that you're not opening a new TCP\n\u003e connection for every call to the database, and then leave them\n\u003e dangling until your app or the server runs of of connections!\n\nThe `Sofa.DB.open!/2` call also does similar checks, ensuring you have\nat least permissions to access the database, in some form. There is\nnothing that changes over time within this struct, so feel free to cache\nit \"for a while\" in your processes if that helps.\n\n```elixir\n# connect to CouchDB and ensure our credentials are valid\niex\u003e sofa = Sofa.init(\"http://admin:passwd@localhost:5984/\")\n        |\u003e Sofa.client()\n        |\u003e Sofa.connect!()\n    #Sofa\u003c\n    client: %Tesla.Client{\n        adapter: nil,\n        fun: nil,\n        post: [],\n        pre: [{Tesla.Middleware.BaseUrl, ...}, {...}, ...]\n    },\n    features: [\"access-ready\", \"partitioned\", \"pluggable-storage-engines\",\n    \"reshard\", \"scheduler\"],\n    timeout: nil,\n    uri: %URI{\n        authority: \"admin:passwd@localhost:5984\",\n        fragment: nil,\n        host: \"localhost\",\n        ...\n    },\n    uuid: \"092b8cafefcaeef659beef7b60a5a9\",\n    vendor: %{\"name\" =\u003e \"FreeBSD\", ...},\n    version: \"3.2.0\",\n    ...\n# re-use the same struct, and confirm we can access a specific database\niex\u003e db = Sofa.DB.open!(\"mydb\")\n    #Sofa\u003c\n    client: %Tesla.Client{ ... },\n    database: \"mydb\",\n    ...\n    version: \"3.2.0\"\n    \u003e\n```\n\n### Doc Usage\n\nThere shouldn't be any surprises here - an Elixir `Map %{}` becomes the\n`body` of the `%Sofa.Doc{}` struct, and the usual CouchDB internal\nfields are available as additional atom fields off the struct:\n\n```elixir\niex\u003e  doc = %{\"_id\" =\u003e \"smol\", \"cute\" =\u003e true} |\u003e Sofa.Doc.from_map()\n    %Sofa.Doc{\n    attachments: nil,\n    body: %{\n        \"cute\" =\u003e true\n    },\n    id: \"smol\",\n    rev: nil,\n    type: nil\n    }\niex\u003e doc |\u003e Sofa.Doc.to_map()\n    %{\n        \"_id\" =\u003e \"smol\",\n        \"cute\" =\u003e true\n    }\n# fetch and retrieve documents works like you'd expect\niex\u003e Sofa.Doc.exists?(db,\"missing\")\n    false\n```\n\n### Raw Mode\n\nSometimes you just want to re-upholster the Couch yourself. That's fine,\nraw mode is here to help you:\n\n```elixir\n# raw mode gives you direct access to CouchDB API, with JSONification\niex\u003e db = Sofa.init(\"http://admin:passwd@localhost:5984/\")\n        |\u003e Sofa.client()\n        |\u003e Sofa.connect!()\n        |\u003e Sofa.raw(\"/_membership\")\n{:ok,\n #Sofa\u003c\n   client: %Tesla.Client{...},\n   database: nil,\n   features: [\"access-ready\",... \"reshard\", \"scheduler\"],\n   timeout: nil,\n   uri: %URI{...},\n   uuid: \"092b8cafefcaeef659beef7b60a5a9\",\n   vendor: %{\"name\" =\u003e \"FreeBSD\", ...},\n   version: \"3.2.0\",\n   ...\n \u003e,\n %Sofa.Response{\n   body: %{\n     \"all_nodes\" =\u003e [\"couchdb@127.0.0.1\"],\n     \"cluster_nodes\" =\u003e [\"couchdb@127.0.0.1\"]\n   },\n   headers: %{\n     cache_control: \"must-revalidate\",\n     content_length: 74,\n     content_type: \"application/json\",\n     date: \"Wed, 28 Apr 2021 14:11:10 GMT\",\n     server: \"CouchDB/3.2.0 (Erlang OTP/22)\"\n   },\n   method: :get,\n   query: [],\n   status: 200,\n   url: \"http://localhost:5984/_membership\"\n }}\n```\n\n## Contributing\n\nIf raw mode can't do it, send a PR, and we'll `make it so`. If you find\nyourself reaching for raw mode often, consider a PR that extends Sofa\nitself?\n\nSofa should pass credo, and also respect dialyzer, via `make lint`.\n\n## Thanks\n\nTo the CouchDB team, a part of my life for more than a decade. Relax.\n\n[hex]: https://hex.pm/packages/sofa\n[CouchDB]: https://couchdb.apache.org/\n[hexdocs]: https://hexdocs.pm/sofa\n[CouchDB API]: https://docs.couchdb.org/\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fskunkwerks%2Fsofa","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fskunkwerks%2Fsofa","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fskunkwerks%2Fsofa/lists"}