SimplyCouch
Simple CouchDB ORM for Rails — ActiveModel-compliant, driver-agnostic.
Zero driver dependencies. Your app brings its own CouchDB client (couchrest, couchbase, etc.).
History
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.
Installation
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 clientQuick Start
class 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 # => 42Features
Properties & Type Casting
property :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: HashAssociations
belongs_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
endValidations
Standard 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]
endCallbacks
before_save, after_save, before_create, after_create, before_destroy, after_destroy — all standard ActiveModel callbacks.
Views
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.
Pagination
Post.all(page: 2, per_page: 25)
User.active_by_created(page: 1, per_page: 50, descending: true)Soft Delete
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)Ancestry (Tree Structures)
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: :localeDynamic Finders
User.find_by_name('Alice')
User.find_all_by_active(true)
User.count_by_active(true)Include Relations
Eager-load associations to avoid N+1 queries on CouchDB:
Post.all(include: :user) # loads users with posts
Post.all(include: [:user, :comments]) # multiple associationsConflict Resolution
Auto-merge on CouchDB conflicts by default (can be disabled):
User.auto_conflict_resolution_on_save = false # disableMulti-Database Support
class ArchivedPost
include SimplyCouch::Model
use_database 'http://couchdb:5984/archive'
endDesign Document Splitting
Prevent 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.
Attachments
SimplyCouch supports two attachment strategies — use the one that fits your use case:
1. CouchDB Native Inline Attachments
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.
2. ActiveStorage Compatibility
Coming soon — has_one_attached / has_many_attached backed by CouchDB attachments.
See docs/attachments.md for a detailed comparison of approaches.
S3 Attachments
SimplyCouch can store file attachments in AWS S3 (or any S3-compatible service like MinIO).
Uses the officially maintained aws-sdk-s3 gem.
Setup
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: euUsage
class 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 configuredConfiguration options
| 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.
Dependencies
| 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.
Testing
Uses RSpec with an in-memory CouchDB via RockingChair.
bundle exec rspecLicense
BSD 2-Clause — see LICENSE.txt
Credits
- Original simply_stored: Mathias Meyer & Jonathan Weiss at Peritor Consulting
- Fork & simply_couch: Benjamin ter Kuile
- CouchPotato removal + 2026 port: BenClaw & Benjamin ter Kuile