Skip to main content

What replicates

By default, a stack replicates its data and its data model, and keeps its own bookkeeping on the device. You can narrow that by class, or with a predicate of your own.

What DocStack keeps local for you

Every stack writes documents that describe this device's copy of the database rather than its contents. Replicating them is never right — two devices each write their own, so they collide on identical ids with unrelated revisions.

Kept localWhy
~systemCarries this database's schemaVersion, read on every mount. A peer's copy would claim patches this device has not applied.
~crypto-engine-configPins a database to its encryption setting. A peer's copy says nothing about this one.
_design/*Mango indexes, built on demand per device.
~lock-*Guards an in-flight class-model propagation on this device.
~UserSessionSessions belong to the device that logged in.
patch ledgerEach device applies its own patches; patches ship with your code.

This list is DocStack's to maintain rather than yours to guess, and it grows as DocStack does. Sessions and the patch ledger can be opted back in:

stack.sync({
remote,
internalDocs: { replicateSessions: true },
});

Classes, domains, users, groups and the rest of the data model do replicate: they are authored data model, and a peer needs them to make sense of anything else. The complete list of what stays local, and the options that change it, is in What stays on the device.

Filtering by class

// Everything except one class
stack.sync({ remote, classes: { exclude: ['Draft'] } });

// Only these classes
stack.sync({ remote, classes: { include: ['Task', 'Project'] } });

Two things this does that a hand-written predicate would not:

An allow-list keeps the data model. include: ['Task'] taken literally would produce a remote holding Task documents and no Task class model — a database the next device could not open. Class models, domains, users, groups, auth modules and job definitions (DATA_MODEL_CLASSES) ride along automatically. includeDataModel: false turns that off, for a remote that is not meant to be a readable replica.

Relations are judged by their endpoints. A relation document carries ~domain plus sourceClass/targetClass, not ~class, so a predicate reading only ~class lets every relation through — including ones pointing at documents you just filtered out, which arrive on the peer as dangling references. A relation replicates only when both of its endpoints do.

One asymmetry worth knowing: exclude: ['Draft'] drops documents of Draft, but the Draft class model still crosses, because it is ~class: "class". That is deliberate — it keeps the remote readable. To keep a model local too, name its id:

stack.sync({
remote,
classes: { exclude: ['Draft'] },
internalDocs: { extraDocIds: ['Draft'] },
});

(A class created through Class.create has its name as its id.)

Filtering with a predicate

For anything class rules do not express:

stack.sync({
remote,
filter: (doc) => doc.archived !== true,
});

It is ANDed with everything above, and must be pure — it runs once per change.

Prefer classes where it fits. A bare function has no configuration DocStack can see, which matters for the next section.

Changing a filter re-replicates

PouchDB identifies a replication — and therefore its resume point — partly by the filter. DocStack derives that identity from your filter's configuration, so:

  • Change which classes replicate, and replication re-scans from the beginning and backfills the documents the new filter admits.
  • Keep the configuration, and it resumes where it left off. Listing the same classes in a different order does not count as a change.

The backfill is the behaviour you want — a widened filter that silently skipped history would leave documents permanently missing — but it is a full pass over the changes feed, so it is not free.

A bare filter function is identified by its own source text, so two closures over different data with identical source read as the same filter and will resume from each other's position. Use classes if the rule is really a configuration.

Two things filters do not do

They are not access control. A filter shapes what a device bothers to carry. Partitioning belongs to the topology — a database per workspace, a Drive folder per account. Do not use a filter to keep data away from someone who can reach the remote.

They do not clean up. Documents that already replicated under a looser filter stay on the remote. Tightening a filter stops new ones crossing; removing the old ones takes an actual delete.

Options that look like they should work, but do not

PouchDB's doc_ids and selector are handed to the source adapter, and the Google Drive adapter implements neither. Passed to a Drive sync they sit in the options looking like configuration while everything replicates anyway. Use filter or classes — those are applied by the replication layer itself, whatever the transport.