Simple CouchDB ORM for Rails — ActiveModel-compliant, driver-agnostic.
Zero driver dependencies. Your app brings its own CouchDB client (couchrest, couchbase, etc.).
SimplyCouch is based on simply_stored (140 ★), created by Mathias Meyer and Jonathan Weiss at Peritor Consulting in Berlin (~2010). simply_stored was a convenience layer on top of CouchPotato.
This fork evolved over a decade with substantial additions — pagination, ancestry trees, embedded documents, include relations, multi-database support, and a migration toward ActiveModel. In 2026, the CouchPotato dependency was fully removed (~930 lines of view system + persistence ported directly into the gem), couchrest was removed from the gemspec, and the gem was renamed to SimplyCouch as its own project.
Where are the original authors? Jonathan Weiss (jweiss) co-founded Scalarium and is active in the Berlin tech scene. Mathias Meyer (roidrage) moved to Amazon Web Services Germany. Peritor's webistrano (868 ★) was widely used in the Capistrano era.
gem 'simply_couch'Or from git:
gem 'simply_couch', git: 'https://github.com/bterkuile/simply_couch.git'You must also add a CouchDB client to your Gemfile (the gem doesn't force one):
gem 'couchrest' # or couchbase, or any CouchDB HTTP clientclass User
include SimplyCouch::Model
property :name
property :email
property :active, type: Boolean
property :last_login, type: Time
property :tags, type: Array
has_many :posts
belongs_to :company
view :by_name, key: :name
view :active_by_created, key: :created_at, conditions: 'doc.active == true'
end
class Post
include SimplyCouch::Model
property :title
property :body
belongs_to :user
end
# CRUD
user = User.create(name: 'Alice', email: 'alice@example.com', active: true)
user.update(name: 'Alice B.')
user.destroy
# Queries
User.find_by_name('Alice')
User.find_all_by_active(true)
User.active_by_created(descending: true)
User.all(page: 1, per_page: 40)
# Associations
user.posts # => [Post, ...]
user.posts(limit: 5, order: :desc)
user.post_count # => 42property :name
property :age, type: Integer
property :price, type: Float
property :active, type: Boolean # stored as true/false
property :last_login, type: Time
property :tags, type: Array
property :metadata, type: Hashbelongs_to, has_many, has_many_embedded, has_and_belongs_to_many, has_one, embedded_in.
class Post
include SimplyCouch::Model
has_many :comments, dependent: :destroy
has_many :authors, through: :comments, source: :user
belongs_to :category
end
class User
include SimplyCouch::Model
has_and_belongs_to_many :networks, storing_keys: true
endStandard ActiveModel validations plus a containment validator for array properties:
class Page
include SimplyCouch::Model
property :categories
validates_containment_of :categories, in: %w[news blog docs]
endbefore_save, after_save, before_create, after_create, before_destroy, after_destroy — all standard ActiveModel callbacks.
CouchDB views are auto-generated from your model's property declarations. JavaScript only (Erlang dropped in 2026 port).
view :by_status, key: :status
view :published, key: :created_at, conditions: 'doc.status == "published"'
view :by_tags, key: :tags # array properties work tooCustom views and raw map/reduce are supported via view spec classes.
Post.all(page: 2, per_page: 25)
User.active_by_created(page: 1, per_page: 50, descending: true)class Document
include SimplyCouch::Model
enable_soft_delete # defaults to :deleted_at
end
doc = Document.create(title: 'draft')
doc.destroy
Document.all # => [] (soft-deleted filtered out)
Document.all(with_deleted: true) # => [doc] (recoverable)class Page
include SimplyCouch::Model
property :title
has_ancestry
end
# Build trees
parent = Page.create(title: 'Products')
child = Page.create(title: 'Widgets', parent: parent)
# Query trees
Page.roots # pages with no parent
Page.full_tree # entire tree loaded in one query
parent.children # direct children
parent.descendants # all descendants (flattened)
child.ancestors # path to root
# Scoped trees (different trees per property)
has_ancestry by_property: :localeUser.find_by_name('Alice')
User.find_all_by_active(true)
User.count_by_active(true)Eager-load associations to avoid N+1 queries on CouchDB:
Post.all(include: :user) # loads users with posts
Post.all(include: [:user, :comments]) # multiple associationsAuto-merge on CouchDB conflicts by default (can be disabled):
User.auto_conflict_resolution_on_save = false # disableclass ArchivedPost
include SimplyCouch::Model
use_database 'http://couchdb:5984/archive'
endPrevent full view reindexing when adding a new view:
class Post
include SimplyCouch::Model
split_design_documents_per_view # each view → own _design doc
view :by_user_id, key: :user_id # → _design/Post_view_by_user_id
view :by_status, key: :status # → _design/Post_view_by_status
endWithout splitting, changing any view reindexes all views. On large databases, this can take hours. With splitting, only the new/changed view reindexes.
SimplyCouch supports two attachment strategies — use the one that fits your use case:
class Invoice
include SimplyCouch::Model
include SimplyCouch::Model::Attachments
end
invoice.put_attachment('receipt.pdf', file, content_type: 'application/pdf')
invoice.fetch_attachment('receipt.pdf')
invoice.delete_attachment('receipt.pdf')
invoice.attachment_names # => ['receipt.pdf', 'logo.png']Attachments are stored inline in the CouchDB document — atomic, no extra storage, replicable.
Coming soon — has_one_attached / has_many_attached backed by CouchDB attachments.
See docs/attachments.md for a detailed comparison of approaches.
SimplyCouch can store file attachments in AWS S3 (or any S3-compatible service like MinIO).
Uses the officially maintained aws-sdk-s3 gem.
Add to your Gemfile:
gem 'aws-sdk-s3'Configure global defaults (per environment):
# config/initializers/simply_couch.rb
SimplyCouch.s3_defaults = {
bucket: 'myapp-development',
access_key: ENV['S3_ACCESS_KEY'],
secret_access_key: ENV['S3_SECRET_KEY'],
location: :eu # :us or :eu, defaults to :us
}
# Or load from a YAML file with ERB:
SimplyCouch.load_s3_config(Rails.root.join('config', 's3.yml'))
# Or from Rails encrypted credentials:
SimplyCouch.s3_defaults = Rails.application.credentials.s3A typical config/s3.yml:
development:
bucket: myapp-dev
access_key: dev-key
secret_access_key: dev-secret
production:
bucket: myapp-prod
access_key: <%= ENV['S3_ACCESS_KEY'] %>
secret_access_key: <%= ENV['S3_SECRET_KEY'] %>
location: euclass Report
include SimplyCouch::Model
has_s3_attachment :pdf # uses defaults from SimplyCouch.s3_defaults
has_s3_attachment :thumbnail, bucket: 'thumbnails' # override bucket
end
report = Report.new
report.pdf = File.read('monthly.pdf') # assign content
report.pdf = ['line1', 'line2'] # arrays serialized to JSON
report.save # uploads to S3
report.pdf_size # => 24501 (bytes)
report.pdf_url # presigned URL
# Read back
report.pdf # fetches from S3 on first access
# Delete
report.destroy # also deletes from S3 if configured| Option | Default | Description |
|---|---|---|
bucket |
(required) | S3 bucket name |
access_key |
(required) | AWS access key ID |
secret_access_key |
(required) | AWS secret access key |
location |
:us |
:us (us-east-1) or :eu (eu-west-1) |
permissions |
'private' |
S3 ACL: 'private', 'public-read', etc. |
after_delete |
:nothing |
:delete to remove from S3 on destroy |
logger |
nil |
Custom logger for S3 operations |
Historical note: The original simply_stored shipped with
right_aws(unmaintained for years). In June 2026 this was replaced withaws-sdk-s3— same API surface, maintained gem, supports S3-compatible services like MinIO.
| Gem | Version | Notes |
|---|---|---|
| activemodel | >= 6.0 | Validations, callbacks, dirty tracking |
| activesupport | >= 6.0 | Inflections, callbacks, concern |
No CouchDB driver dependency. Add couchrest or couchbase to your app's Gemfile.
Uses RSpec with an in-memory CouchDB via RockingChair.
bundle exec rspecBSD 2-Clause — see LICENSE.txt
- Original simply_stored: Mathias Meyer & Jonathan Weiss at Peritor Consulting
- Fork & simply_couch: Benjamin ter Kuile
- CouchPotato removal + 2026 port: BenClaw & Benjamin ter Kuile