# simply_couch → Couchbase: Session Report **For:** Benjamin ter Kuile (bterkuile) **Prepared by:** Greg (via Claude Code / Opus 4.8) **Date:** 2026-06-20 This document summarizes a working session that started from one request — "compartmentalize simply_couch's data backend so CouchDB could be swapped for Couchbase" — and grew, through your input in the thread, into three concrete artifacts plus an architectural recommendation. Everything below was built and verified against a live database (CouchDB 3.5.2 and Couchbase Community 7.x in local Docker); nothing here is hypothetical. --- ## TL;DR Three artifacts, in increasing ambition: 1. **PR #10 on `bterkuile/simply_couch`** — a behaviour-preserving refactor that turns the leaky single-driver seam into a real, documented adapter contract. Full suite green (453 examples). CouchDB still the default. 2. **`simply_couch-couchbase`** — a Couchbase *adapter plugin* for the refactored simply_couch (kept the CouchDB-shaped API). SDK-based, N1QL view translation. 3. **`simply_based`** — the *revolutionary* path you asked for: a standalone Couchbase-native ORM. `include SimplyBased::Model`, no CouchDB concepts left. Runs the simply_couch test suite: **403/403 applicable examples green**. And a candid recommendation at the end: whether a Couchbase-native ORM is actually the right call versus a standard Postgres + Redis Rails stack. --- ## The core strategy point: Couchbase ≠ CouchDB Despite the shared name and lineage (Damien Katz), the two are not API-compatible, and a literal `simply_couchbase_rest` drop-in for `couchrest` cannot exist: | Concern | CouchDB (couchrest) | Couchbase | |---|---|---| | Data path | HTTP REST per-document | binary KV via SDK (REST is management-only) | | Query | JS map/reduce **design-doc views** | **N1QL / SQL++** + GSI (CB views deprecated in 7.0) | | Concurrency | MVCC `_rev`, conflict docs | CAS (uint64), no user-facing revisions | | Bulk | `_bulk_docs` | batched KV / subdoc ops | | Attachments | native blob attachments | none — store externally (S3/local) | | "CouchDB interface" | — | only the deprecated Views engine (port 8092 CAPI), or Sync Gateway's replication protocol — neither is a general server API for an ORM | So Couchbase is reached the way it's meant to be: the SDK for KV, **N1QL** for queries. Not a REST shim. The pinch point is the view system — but simply_couch builds views from **structured Ruby spec objects**, not ad-hoc JS, so the finder and association queries translate deterministically to N1QL. That's what made the whole thing tractable. --- ## Artifact 1 — PR #10: the adapter-contract refactor **Link:** https://github.com/bterkuile/simply_couch/pull/10 (open; you were notified on open). **What it does (behaviour-preserving — zero functional change for couchrest users):** - Adds `SimplyCouch::Adapter` — the backend-neutral contract the model layer depends on (`save_document`, `load_document`, `destroy_document`, `bulk_*`, `view`, `first`, `create_database!`, `drop_database!`), plus an exception contract (drivers translate their errors to `SimplyCouch::Conflict` / `NotFound`). - Moves the existing CouchDB code out of `Model::DatabaseInstance` into `SimplyCouch::Adapters::CouchRest` **verbatim** (a `DatabaseInstance` alias is kept). This is a move, not a rewrite. - **Lazy driver loading:** `require 'simply_couch'` now loads with no driver present; couchrest loads only when its adapter is first used. - Adapter registry: `SimplyCouch.register_adapter(name, class, require_path:)` + `SimplyCouch.adapter = :name`. - Stops the model/association/view layers reaching through `database.couchrest_database` (5 sites → the neutral `database.bulk_load`). - `SimplyCouch::DocumentCodec` — rebuilds a model from a plain hash via the `ruby_class` tag, so non-couchrest backends can hydrate documents. - Exposes the view-spec `klass`/`options` as the backend-neutral query IR. - Hardens the load guard so a plugin gem can open `module SimplyCouch` for its own namespace before the library loads. - Adds shared `'a simply_couch adapter'` RSpec examples (reusable parity proof). - Version 0.1.2 → 0.2.0; README/CHANGELOG updated. **Verification:** 453 examples, 0 failures vs a live CouchDB 3.5.2. No model-facing signature changed. (Two pre-existing flaky DB-reset races predate the PR.) --- ## Artifact 2 — `simply_couch-couchbase` (adapter plugin) A separate gem that plugs the **refactored** simply_couch into Couchbase while keeping the CouchDB-shaped API. `SimplyCouch::Adapters::Couchbase` on the official `couchbase` SDK: KV for documents, CAS↔`_rev`, and an `N1qlTranslator` that renders the structured `ModelViewSpec` / `DeletedModelViewSpec` (all_documents / find_by / count) to N1QL. Custom/raw JS association views raise `SimplyCouch::UnsupportedView` (documented boundary). **Verification:** 20 examples green = 11 N1QL-translator unit tests (no server) + 9 live-Couchbase contract examples (the *same* shared examples couchrest passes). This is the lower-ambition path: it proves the adapter seam works and gives a working Couchbase backend, but associations were left as an explicit boundary. --- ## Artifact 3 — `simply_based` (the Couchbase-native ORM) The path you chose in the thread: not an adapter, a **new gem** that *is* the ORM, Couchbase-native throughout. **The one-line swap:** ```ruby - include SimplyCouch::Model + include SimplyBased::Model ``` **Why so little changes:** simply_couch's model layer (properties, type-casting, validations, callbacks, dirty tracking, the association DSL, soft-delete, pagination) is already storage-neutral. SimplyBased keeps all of it and replaces exactly one layer — `SimplyBased::Store` — which speaks Couchbase directly. Every association lookup funnels through a handful of Store methods that emit N1QL. **How the CouchDB views became N1QL:** - `belongs_to` / `has_many` / `has_one` → scalar foreign-key equality: `WHERE ruby_class = $rc AND user_id = $id`, ordered by `created_at`, GSI auto- created on first use. - `has_and_belongs_to_many` → on the storing side, a **KV multi-get** of the id array (no query at all); on the other side, the two hand-written map/reduce functions become one query: ```sql SELECT c.*, META(c).id, META(c).cas FROM `bucket`.`_default`.`_default` AS c WHERE c.ruby_class = $rc AND ANY x IN c.`network_ids` SATISFIES x = $v END ``` backed by an array index. - **Ancestry / trees** → you were right that it's a nested-set/materialized-path intention. `path_ids` is the full path root→self, so no recursive build: - children = `path_ids[ARRAY_LENGTH-2] = $id` - subtree = `$id IN path_ids` (minus self) - roots = `ARRAY_LENGTH(path_ids) = 1` - `_rev` is mapped onto **CAS**: a record's `_rev` is the stringified CAS of its last write; updates use it for optimistic locking (stale write → `SimplyBased::Conflict`). - Soft-delete-aware `find_by`/`find_all_by`: the `[key, deleted_at] = [val, nil]` convention compares element-wise so a `nil` component matches `MISSING`/`NULL` (non-deleted docs carry no `deleted_at`). - Timestamps are microsecond-precision so same-second creations paginate in creation order deterministically. **Replaces Redis too** (same cluster, no second datastore) — 5/5 green: - `SimplyBased::Counter` — Couchbase binary increment/decrement (true atomic counters; a drop-in for Redis INCR/DECR). - `SimplyBased::Cache` — an `ActiveSupport::Cache::Store` over KV + native TTL; usable as `Rails.cache` and therefore the ActiveJob / fragment / low-level cache. - `SimplyBased::Lock` — distributed lock via insert-if-absent + TTL auto-expiry + CAS-checked release (only the holder can unlock). ### Test coverage SimplyBased runs the **actual simply_couch test suite** against a live Couchbase. - **403 / 403 applicable examples green on a clean run.** One ancestry reparent edge in the namespaced (`by_property`) cascade is intermittently flaky (~50%) — it does several dependent saves and occasionally races; tracked, not yet nailed. - **Green:** CRUD; finders (`find`, `all`, `first`, `find_by_*`, `count_by_*`); `belongs_to` / `has_many` / `has_one` / `has_and_belongs_to_many`; validations; callbacks; dirty tracking; soft-delete (incl. soft-delete-aware finders); pagination; bulk; eager-loading; ancestry/trees; the Redis primitives. - **Excluded by design:** - `:couchdb_only` — native inline attachments, JS-view token-safety, design-document maintenance, raw custom JS views. No Couchbase equivalent (use `has_s3_attached` / `has_local_attached`; N1QL uses bound parameters). - `embedded_in` reverse-lookups — out of scope per your call. ### Known gaps / risks (read before relying on it) - It is **0.1.0**, written in one session. Treat its youth as the real risk. - **No multi-document transactions** exposed (Couchbase has them in the SDK; the ORM doesn't surface them yet). - You **design GSIs deliberately** — a query with no supporting index does a full primary scan. There is no migration framework for indexes yet. - `embedded_in` is unimplemented; one flaky namespaced-ancestry test. ### One recommended pattern shift v0 uses a single collection with a `ruby_class` discriminator (closest to CouchDB; runs the whole suite as-is). For production, map each model to its own Couchbase **Collection** — tighter GSIs, cleaner queries, idiomatic. A `collection :name` per-model DSL is the planned opt-in. ### Integration sketch ```ruby # Gemfile gem 'simply_based', git: 'https://github.com/gbentz2/simply_based.git' # config/initializers/simply_based.rb SimplyBased.database_url = "couchbase://Administrator:password@localhost/app?scope=_default&collection=_default" # config/environments/*.rb config.cache_store = SimplyBased::Cache.new(namespace: "app") ``` ```ruby class User include SimplyBased::Model property :name has_many :posts validates :name, presence: true end ``` --- ## Should this ORM exist? — SimplyBased vs. Postgres + Redis You raised the real question: another path is to drop CouchDB and refactor to a standard Rails stack (Postgres + Redis). For an app with **many reads and writes per second**, does a Couchbase-native ORM have real benefits? An honest take. **Maturity first.** Postgres + ActiveRecord + Redis is the most battle-tested stack in Rails. SimplyBased is 0.1.0 with known gaps. On risk and ecosystem alone, the standard stack wins decisively. That sits at the top of the ledger. | Dimension (sustained high throughput) | SimplyBased / Couchbase | Postgres + Redis | |---|---|---| | Raw write scale | memory-first, async persist, sub-ms KV, **shards horizontally** | single primary takes all writes; scales up, not out (shard/partition = heavy) | | By-id reads | sub-ms from RAM | fast, network + planner hop; replicas for read scale | | Complex/ad-hoc queries | N1QL, but **you design every GSI**; missing index = full scan | world-class planner, joins, ad-hoc SQL | | Transactions / integrity | single-doc atomic + CAS only (multi-doc not exposed) | **ACID, FKs, constraints, multi-row txns** | | Operational surface | **one datastore** — docs + cache + counters + locks, no Redis | two systems, two backups, PG↔Redis cache-invalidation drift | | Cost / footprint | RAM-hungry, multi-service, doesn't scale down; some EE-licensed | both lean, cheap, scale down; fully open | | Schema | schemaless documents, denormalized aggregates | migrations — but **`jsonb` gives documents *with* ACID** | **Verdict.** Couchbase's write advantage is real but only **above a high ceiling** — a tuned Postgres does tens of thousands of writes/sec; you only *need* Couchbase's horizontal write scale-out when you'd otherwise be sharding Postgres (the genuinely painful threshold). Below it, Postgres + Redis wins on transactions, cost, ecosystem and hiring. So it's not "which is faster" — it's **data shape and scale horizon**: - **SimplyBased/Couchbase wins** when the domain is document-shaped and denormalized, cross-entity transactions are rare, you truly expect to outgrow a single Postgres primary, and the **one-datastore consolidation** (no Redis, no cache-invalidation drift) is worth running a heavier engine. - **Postgres + Redis wins** for relational/transactional domains (most business apps), the mature default Rails path, cost-sensitivity, or when `jsonb` already covers your document needs. **Migration-cost asymmetry — and it favors SimplyBased for *this* project.** You are already on CouchDB with simply_couch models: - *SimplyBased path:* `include` swap, design GSIs, adopt the collection pattern, copy data CouchDB→Couchbase. Document model + association API survive intact. Low app-code churn. - *Postgres path:* re-model documents into tables (or jsonb), rewrite every model to ActiveRecord, rebuild associations as FKs, re-express views as SQL, rewire jobs/cache. You land on the best-supported stack — but it's a large rewrite. **The option neither of us named first, and often the strongest:** **Postgres + `jsonb` + Redis.** Document flexibility *and* ACID, on the mature Rails rails, with Redis still doing cache/counters/locks. For a transactional high-throughput app this is the "boring tech wins" answer. **Bottom line.** SimplyBased earns its place when the document model is the point, write scale-out is a real future need, and consolidating onto one engine is worth the heavier footprint and the youth of the library. For getting *this* project off CouchDB with minimal model-code churn, it's the lowest-friction path and it's genuinely working (403/403 applicable). But the benchmark gap only pays off past a scale most apps never reach — so weigh the 0.1.0 maturity and the multi-doc-transaction gap as the real decision factors, not the throughput numbers. --- ## Repos & access | Artifact | Location | State | |---|---|---| | Adapter refactor | PR https://github.com/bterkuile/simply_couch/pull/10 | open | | Couchbase adapter gem | https://github.com/gbentz2/simply_couch-couchbase | public | | SimplyBased ORM | https://github.com/gbentz2/simply_based | **private** | To browse `simply_based`, the cleanest options are: Greg flips it public, adds your `bterkuile` GitHub account as a collaborator, or mints a read-only fine-grained deploy token scoped to that one repo. (A link with Greg's personal token embedded works for `git clone` but hands over his whole account — not recommended for anything durable.) ## Reproduce the verification - Ruby 3.1+ (built on 4.0.5). `bundle install`. - CouchDB: `docker run -d -p 5984:5984 -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=secret couchdb:3` - Couchbase: `docker run -d -p 8091-8096:8091-8096 -p 11210:11210 couchbase:community`, then init a single-node cluster + a **couchstore** bucket (Community edition rejects the default magma backend) named `simply_based_test`. - `COUCHBASE_URL="couchbase://Administrator:password@localhost/simply_based_test" bundle exec rspec`