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.replicasand 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.