Provisioning and transactions
Create resources explicitly, plan index drift, and use transaction or CAS safety.
CouchSet never creates scopes, collections, or indexes just because you imported a definition or bound a model. Run DDL with admin credentials when you mean to. Transactions and CAS cover multi-document and single-document safety separately from provisioning.
Explicit provisioning
await db.ensureCollections();
await db.ensureIndexes({ waitForIndexes: true });These need Manage Scopes and query-index privileges.
| Helper | Behavior |
|---|---|
ensureCollections | Creates missing named scopes, then collections. Concurrent already-exists is OK. Never updates, renames, or drops. Does not create _default. Named collections in _default are supported. Conflicting settings for the same target fail. |
ensureIndexes | CREATE INDEX IF NOT EXISTS only — never compares, changes, drops, or rebuilds an existing index |
Singleton / legacy models also support indexStatements(), ensureIndexes(), and top-level static ensureIndexes where applicable.
Dynamic registration
const reports = await db.registerModel(reportDefinition, {
provision: { collections: true, indexes: true, waitForIndexes: true },
});Binding db.model(definition) alone still performs no I/O. Only an explicit provision option (or the ensure/plan helpers above) touches the cluster.
Index drift
const plan = await db.planIndexes(); // create | matching | replace
await db.applyIndexPlan(plan);
await db.applyIndexPlan(plan, { dropReplaced: true });When a live definition drifts, the plan creates a versioned physical replacement and waits for it to become online before an old index can be dropped. That avoids an unindexed window. Dropping the old index is affirmative (dropReplaced: true).
Search indexes use the parallel planSearchIndexes / applySearchIndexPlan path (see Search, vector, and geospatial).
CLI counterparts (collections plan|apply, indexes plan|apply, search-indexes plan|apply) live under Operations and CLI — still review-then-apply, no silent drops.
Transactions
await db.transaction(async (tx) => {
const sessions = tx.model(sessionDefinition);
const session = await sessions.getById(id);
if (session) await sessions.remove(session);
});Transaction-bound models expose attempt-scoped get / insert / replace / remove. Inserts need an explicit ID.
Couchbase may retry the callback. Do not send email, fire webhooks, or perform other irreversible external effects inside it — do that after the transaction resolves, or use an outbox / idempotency key. Hooks and validation can also repeat. Commit-ambiguous errors surface to the caller; CouchSet does not claim success or re-run external work.
CAS
const current = await model.getWithCas(id);
await model.replaceIfCas(id, nextValue, current.cas);
const result = await model.consumeOnce(id, current.cas);
// { status: 'consumed' } | { status: 'missing' } | { status: 'conflict' }consumeOnce issues exactly one conditional remove. It does not re-read and retry with a fresh CAS.
Hydrated save({ cas }) and related replace options are covered in Relationships and hydration.
Test fixtures
const { model, cleanup } = await createCouchsetTestFixture(db, definition);
try {
// isolated named scope/collection via registerModel + provision
} finally {
await cleanup(); // drops fixture collection (and generated scope when applicable)
}Fixture cleanup is the only helper that drops a resource. Always run cleanup() in teardown.
Pitfalls
- Expecting
db.model(...)or import to provision collections/indexes. - Using
ensureIndexesto “fix” drifted definitions — useplanIndexes/applyIndexPlan. - Side effects inside
db.transactioncallbacks (retries). - Assuming
consumeOnceretries on conflict. - Leaving test fixtures without
cleanup().
See also
- Connections and clients — binding vs provisioning
- Models and definitions — indexes on definitions
- Search, vector, and geospatial — Search index plans
- Eventing — separate control-plane reconcile
- Operations and CLI — plan/apply from the CLI
- API reference — ensure/plan/transaction entrypoints