# API: the advanced store Source: https://selfstore.dev/docs/api-advanced Complete reference for selfstore/advanced - the LocalStore interface, the BackupTarget contract you implement for a custom destination, the four built-in targets, and storage pressure advice. The pull-model store the simple one is built on, plus the destination primitives. The narrative version, and when to reach for it at all, is [advanced](https://selfstore.dev/docs/advanced). Two rules before anything else. `selfstore()` gives you `store.advanced`, so you almost never construct a `LocalStore` yourself. And the advanced store does **not** auto-save: you call `schedule()` and `flush()`, which is precisely the control you came for. ## LocalStore ### State and lifecycle | Member | Signature | | --- | --- | | `state` | `LocalStoreState` (readonly) | | `subscribe` | `(fn: () => void) => () => void` | | `init` | `() => Promise` | | `schedule` | `() => void` | | `flush` | `() => Promise` | | `dispose` | `() => void` | ### Syncing | Member | Signature | Notes | | --- | --- | --- | | `syncIfStale` | `(source: SyncSource) => Promise` | `'boot' \| 'focus' \| 'online' \| 'interval' \| 'manual' \| 'push' \| 'connect'` | | `syncNow` | `() => Promise` | Null when nothing changed | `syncNow` returning the journal entry is what lets an app report *what* a sync moved - see [`CollectionChanges`](https://selfstore.dev/docs/api-sync). ### Destinations | Member | Signature | | --- | --- | | `inspectTarget` | `(target) => Promise<{ hasBackup, date, encrypted }>` | | `attachTarget` | `(target, opts?) => Promise` | | `detachTarget` | `(opts?: { keepSession?: boolean }) => Promise` | | `setManualFile` | `() => Promise` | | `exportBlob` | `() => Promise` | | `markDownloaded` | `() => void` | | `forget` | `() => Promise` | ```ts attachTarget(target, { password?: string | null, group?: StoreGroupConfig, strategy?: 'merge' | 'replace-local' | 'replace-remote', keepSession?: boolean, wipe?: boolean }) ``` `inspectTarget` before `attachTarget` is the honest order: it tells you whether a backup is already there and whether it is encrypted, **without** touching local data, so you can ask the user before anything is merged or replaced. ### Copies, peers and mirrors | Member | Signature | What it is | | --- | --- | --- | | `attachReplica` / `detachReplica` | `(target, opts?) => string` / `(id) => void` | The same backup, written twice ([resilience](https://selfstore.dev/docs/resilience)) | | `attachPeer` / `detachPeer` | `(source: PeerSource, opts?) => string` / `(id) => void` | Someone else's backup, read-only ([peers](https://selfstore.dev/docs/peers)) | | `attachMirror` / `detachMirror` | `(target, opts: { password }) => string` / `(id) => void` | A copy under a different key | Three different words for three different things, and the distinction is worth holding: a **replica** is your data written again, a **peer** is data you read from someone else, a **mirror** is your data re-encrypted for another audience. ### Encryption | Member | Signature | | --- | --- | | `setEncryption` | `(password: string \| null) => Promise` | | `addEncryptionKey` | `(password: string, id?: string) => Promise` | | `removeEncryptionKey` | `(id: string) => Promise` | | `setExternalEncryption` | `(secret: Uint8Array, keyRef: string) => Promise` | | `addExternalKey` | `(secret: Uint8Array, keyRef: string, id?: string) => Promise` | | `unlockWithExternal` | `(secret: Uint8Array) => Promise` | | `listEncryptionKeys` | `() => { id: string; kind: 'password' \| 'external' }[]` | | `unlock` / `lock` | `(password) => Promise` / `() => void` | | `reconnect` | `() => Promise` | | `setGroup` | `(manifest: SignedManifest) => Promise` | The envelope holds several key slots. That is what makes a recovery code possible (`addEncryptionKey`) and what makes a passwordless group possible (`setGroup` plus one slot per member) - the same mechanism, two products. `addExternalKey` takes raw bytes rather than a password: a passkey PRF result or a hardware-held secret never has to become a string. ### Modes | Member | Signature | | --- | --- | | `setEphemeral` | `() => void` | | `leaveEphemeral` | `() => Promise` | ## BackupTarget: writing a destination Any object satisfying this contract is a destination. The store's merge semantics, encryption and status all work unchanged. ```ts interface BackupTarget { readonly kind: string; readonly label: string; save(blob: Blob): Promise; load(): Promise; stat?(): Promise; isReady(): Promise; reconnect(): Promise; disconnect(): Promise; abortInFlight?(): void; } ``` | Member | Contract | | --- | --- | | `kind` | Flows into `TargetKind` and the status. Must not be `'device'` or `'file-manual'` - those name the **absence** of a target and `attachTarget` refuses them. | | `label` | What the user sees. | | `save` | Returns a version token, or null. | | `load` | Null means no backup there yet - **not** an error. | | `stat` | Optional cheap freshness check. | | `isReady` | False when a gesture is needed; drives `needs-attention`. | | `reconnect` | Re-run the auth gesture. | | `abortInFlight` | Optional; cancel a hung upload. | **Getting the error semantics right matters more than the happy path.** A transient failure must not look like a gone destination, or the store will propose the wrong remedy. The rules, with the exact codes to throw, are on [error codes](https://selfstore.dev/docs/errors). ## The built-in targets | Export | Destination | | --- | --- | | `fileTarget` | A disk file, File System Access (Chromium) | | `driveTarget` | Google Drive | | `webdavTarget` | A WebDAV server | | `s3Target` | An S3-compatible bucket | Each exposes the same shape of helpers: `connect(...)` for the first-time gesture, `fromSession(...)` to rebuild from what was persisted, plus `isSupported()` and, for files, `isOpenSupported()` and `openExisting()`. Every one of them takes a `KV`, because that is where the session - a file handle, a token, a config - is persisted. Keeping it injected is what stops the targets from knowing anything about your storage. ```ts interface FileConnectOptions { kv: KV; fileName: string } interface DriveOptions { auth: DriveAuth; kv: KV; fileName: string } interface WebdavConnectOptions { kv: KV; config: WebdavConfig } interface S3ConnectOptions { kv: KV; config: S3Config } interface WebdavPeerOptions { url: string; username?: string; password?: string } ``` `WebdavPeerOptions` is the read-only variant: a peer's backup you fetch but never write. `BuiltinTargetKind` is `'file' | 'drive' | 'webdav'`, the kinds the library ships. The file handle is persisted **through the injected KV**, which is what keeps the target storage-agnostic. The browser re-confirms write permission once per session, which is the one-click reconnect the status asks for. ### Sharing over Drive: companion files `driveTarget` carries a second family of calls, and they operate on a different kind of file. Everything above works on the user's **own backup**. Sharing needs a **companion**: a file the app creates next to that backup, publishes on a link, and tears down on its own schedule - a member's published copy, an invitation others read before they join. ```ts function createCompanion(opts: { auth: DriveAuth; fileName: string }): Promise<{ fileId: string }>; function share(opts: { auth: DriveAuth; fileId: string }): Promise; function unshare(opts: { auth: DriveAuth; fileId: string }): Promise; function owner(opts: { auth: DriveAuth; fileId: string }): Promise; function secondary(opts: { auth: DriveAuth; kv: KV }, fileId: string): BackupTarget; ``` | Call | What it does | | --- | --- | | `createCompanion` | Creates an empty named file and answers its id. Unlike `createBackup` it does **not** refuse a duplicate name: several members of several groups may legitimately hold identically named copies, and the id is the handle everywhere. | | `share` | Makes the file readable by **anyone holding its link**. The capability-link model: the file carries ciphertext only, the key travels in the link's fragment, so the link *is* the capability and the recipient needs no Google account. | | `unshare` | Drops every link grant, leaving named ones alone. Call it **before** any plaintext rewrite of a file that was shared - a decrypted copy must never stay link-readable for even one save. | | `owner` | Which account owns the file, not which one this session belongs to (that is `account()`). It is how a copy living on somebody else's Drive gets a label: "Google Drive" is the same for everyone, only the owner's address says whose. | | `secondary` | A read-**write** target over one companion file, for [`attachMirror`](#copies-peers-and-mirrors) or a replica. No `fileName`, unlike the rest of `DriveOptions`: a target bound to an id never searches or creates by name. | Publish encrypted files, always. `share` hands the bytes to whoever has the URL, including whoever it gets forwarded to. ```ts const { fileId } = await driveTarget.createCompanion({ auth, fileName: 'my copy.zip' }); await driveTarget.share({ auth, fileId }); await store.attachMirror(driveTarget.secondary({ auth, kv }, fileId), { password: linkKey }); ``` Teardown, in this order: `unshare`, then `deleteBackup({ auth, fileId })`. #### `secondary` rather than `preview` `preview(opts, fileId)` also binds a target to a given file, and its `save()` works - which makes reaching for it tempting. Do not. Its name says read-only because its **`disconnect()` belongs to the primary connection**: it calls `auth.forget()` and can drop the remembered backup id. That is right when the user is leaving Drive, and catastrophic when they are only dropping a shared copy. `secondary` detaches nothing but itself, and carries `kind: 'drive-companion'` so a store that persists the kind never mistakes a shared copy for the destination. #### What is not here, and why **Reading another account's link-shared file.** The `drive.file` scope only sees files this app created or the user picked, so fetching a copy from somebody else's Drive takes either the Google Picker (a user gesture per file, plus a third-party script) or a relay you host. One of those is a server, and neither is a choice this library makes for you - `attachPeer` accepts whatever `load()` you can provide. Every call here follows the [error protocol](https://selfstore.dev/docs/errors) the rest of the target does: `AuthExpiredError` for a genuine loss of access and only after the stale-token retry, `TARGET_WRITE_FAILED` for a refusal, `TARGET_UNAVAILABLE` for a metadata read that did not land. `owner` is the one exception, and deliberately: a file that will not say answers nulls rather than throwing, because a missing label must never break the sync round that asked for it. ## storageAdvice ```ts function storageAdvice(): StorageAdvice; type StorageRisk = 'none' | 'ephemeral' | 'evicted-when-idle'; interface StorageAdvice { risk: StorageRisk; remedy?: 'install-to-dock' | 'install-to-home-screen'; installed: boolean; } ``` Whether the browser is likely to throw the working copy away, and what the user could do about it. `'ephemeral'` is a private window; `'evicted-when-idle'` is a browser that reclaims storage from sites the user does not return to. The advice **disappears on its own** once the remedy is applied, so it is safe to render unconditionally rather than tracking dismissal. This is not a substitute for a durable home - it is the argument for one. A working copy the browser can evict is exactly the state [``](https://selfstore.dev/docs/widget-gate) exists to get the user out of. ## Snapshots and reserved names | Export | Signature | Notes | | --- | --- | --- | | `exportSnapshot` | `(store) => ...` | The raw snapshot, no file format | | `importSnapshot` | `(store, snapshot) => ...` | | | `datedName` | `(name: string) => string` | The same name stamped to the minute | | `inspect` / `isEncrypted` | `(input) => ...` | Read a backup's header without decrypting | | `deriveStatus` | `(input: StatusInput) => StatusDescriptor` | The ranking function the store uses | | `RESERVED_COLLECTION_PREFIX` | `'__'` | Library bookkeeping collections | | `RESERVED_STORE_MODES` | constant | `'device'`, `'file-manual'` | | `isReservedStoreMode` | `(kind: string) => boolean` | | `datedName` is used only on the download path, and the reason looks backwards until you see it: through a handle the app rewrites the **same** file, so its name must stay stable; a download never replaces anything, so a stamped name keeps successive downloads distinguishable instead of `backup (3).zip`. `deriveStatus` is exported so you can test your own status rendering against the same ranking the store applies, rather than reimplementing the precedence and drifting from it. ## Types you will meet but rarely write These are exported because they appear in the shapes above, not because most apps construct them. Listed so a `.d.ts` never sends you hunting. | Type | What it is | | --- | --- | | `LocalStoreOptions` | What `createLocalStore` takes | | `LocalStoreState` | Everything `store.state` holds: mode, target, journal, peers, replicas | | `SnapshotFile` | One file entry inside a snapshot | | `EncodeOptions` | How a backup is written: app name, version, encryption, verification | | `KdfParams` | The Argon2id parameters recorded in the header, so a backup can always be re-derived | | `RecipientStanza` | One key slot in the envelope - a password, an external key or a group member | | `ReplicaState` | A copy's health: last write, last error | | `MirrorState` | A mirror's health | | `PeerState` | A followed peer's health and last read | | `CachedFile` | A file as the local cache holds it | | `LockableCache` | A cache that can be sealed; `isLockableCache(cache)` narrows to it | | `GroupMember` | One member entry in a signed manifest | | `DriveBackupInfo` | One row of Drive's `listBackups()`: `id`, `name`, `modifiedTime`, `size` | `ReplicaState`, `MirrorState` and `PeerState` are worth reading when you render health: each carries its own last error, and **a broken one never gates the store** - that is the point of the design, and it means you have to surface them yourself or nobody will see them. `DriveBackupInfo` deliberately omits whether a backup is encrypted. That lives inside the file, and a listing has to stay one cheap metadata call rather than N downloads - which is also why [`BackupRow.encrypted`](https://selfstore.dev/docs/api-backups) is three-state and learned afterwards. Map of this site for a model: https://selfstore.dev/llms.txt Every page in one file: https://selfstore.dev/llms-full.txt