Search selfstore
v1.8.21

Resilience (a backup copy)

One home is a single point of failure: a token gets revoked, a bucket gets deleted, a provider goes down. A backup copy (a replica) is a second home that holds the same encrypted backup, rewritten after every save or converge that moved data. It is additive and opt-in - it attaches after your primary home and never changes the primary flow.

In the backups widget

If you mount <selfstore-backups>, the whole journey is one property. Build a replicaFlow with the destinations you want to offer (the same targets spec as the connect flow) and assign it:

import { replicaFlow } from 'selfstore/flows';

el.replica = replicaFlow(store, {
  drive: gisDriveAuth({ clientId }), // a second file on the same account
  file: true,
  webdav: true, // 'true' renders a config form in the panel
  s3: true,
});

A “Backup copy” entry appears in the active backup’s menu; picking a destination attaches the copy and its status card renders under the active card, with its own Remove. The flow remembers the chosen destination (under a scoped replica: prefix, isolated from the primary’s session) and re-attaches it silently at every boot - the widget calls restore() for you. Without the property, the panel is unchanged.

The copy’s file is named after the primary with a .replica suffix (“App.zip” becomes “App.replica.zip”), so a copy on the same Drive account never collides with the primary or shows up as a backup of its own.

From the API

No widget needed - the simple store carries the same feature as two calls. Build a target for the second home the same way any target is built, then:

import { s3Target } from 'selfstore/advanced';

// Primary home: Google Drive (connected on the simple store as usual).
await store.connectDrive(gisDriveAuth({ clientId }));

// Resilience copy: an S3 bucket, written alongside every save.
const replica = await s3Target.connect({
  kv: store.flowHost.kv,
  config: { endpoint, region, bucket, key, accessKeyId, secretAccessKey },
});
const id = store.addReplica(replica);
// ...later: store.removeReplica(id);

store.advanced.state.replicas lists what is attached and each one’s last result. At this level the attachment is memory-only: your app re-attaches at boot (the credential is restored from the cache KV; the attach is one call). replicaFlow is exactly that wiring, packaged - use it headless if you want the persistence without the widget.

What it is, and is not

  • Same bytes, same key. A replica receives the exact archive your primary home does, under the store’s own key - no second password, no extra Argon2id pass. It is a copy for availability, not a re-encryption.
  • It never gates the store. A replica that is offline or rejects a write records its error on state.replicas and is retried; it cannot block a save or hold your primary home hostage.
  • It is resilience, not sharing. Because it carries the same key, a replica is another copy you can restore from - not a copy addressed to someone else. For sharing between people, see peers and groups.

The honest note

A replica multiplies the number of operators who hold your data. When the backup is encrypted, they each hold only ciphertext, so N homes disclose no more than one - the encryption is what makes fanning out safe. With no password set, a replica copies plaintext to one more operator, which is exactly when requireEncryption earns its keep. This is threat T12 in the threat model.