Search selfstore
v1.8.37

Google Drive home

The Drive home stores the backup in the user’s own Drive, not yours. You never operate storage. Google never sees cleartext once the backup has a password: the file is then encrypted before it leaves the device, so the cloud holds opaque bytes. Without one the backup is a readable ZIP, because encryption is off by default (requireEncryption defaults to false), so the one-call example sets both.

One call

import { selfstore, gisDriveAuth } from 'selfstore';

// Never write a readable backup, whatever a later code path forgets.
const store = await selfstore('my-app', { requireEncryption: true });

const outcome = await store.connectDrive(gisDriveAuth({ clientId: GOOGLE_CLIENT_ID }), {
  password: backupPassword, // encrypts a new backup from its first write
});
// 'started' | 'merged' | 'manual' | 'cancelled'  (see quick start)

The password goes into the connect call, not into a store.protect() before it: protect() needs a connected destination and throws NOT_CONNECTED without one. Called after connecting, it does encrypt, but the first backup has already been written in clear by then. With requireEncryption: true, a connect without a password throws ENCRYPTION_REQUIRED instead of writing anything.

One prerequisite: an OAuth client id from a Google Cloud project (APIs and Services, then Credentials, then “OAuth client ID”, type Web application, your origin listed). selfstore requests only the drive.file scope, which grants access solely to files this app created, the narrowest scope Drive offers.

The options it takes

interface GisDriveAuthOptions {
  clientId: string;
  scope?: string;
  hint?: string | (() => string | null | undefined);
  persist?: GisTokenPersistence; // 'memory' (default) | 'session'
}

hint pre-selects an account in the Google chooser - pass a function when your app only learns which account later. persist: 'session' keeps the token for the tab so a reload does not re-prompt; 'memory' drops it, which is the safer default and the one a shared machine wants.

driveTarget.account() answers who holds the backup, as a DriveAccount { email, name }. Worth showing next to “saved to Google Drive”: a brand name is not an address, and several accounts look alike.

The same password also opens a Drive file that is already encrypted, so the merge can read it. Without it the call throws PASSWORD_REQUIRED before changing anything, so you can prompt and retry.

Load Google Identity before the click

import { preloadGoogleIdentity } from 'selfstore';

preloadGoogleIdentity(); // when the screen holding the Drive button opens

The consent popup only opens inside the user activation of a click. Loaded cold on that click, Google Identity Services can take longer than the activation lasts: the browser blocks the popup, and Google answers as if the user had refused. preloadGoogleIdentity() (since 1.8.24, also in selfstore/advanced) starts the load early, and the click reuses it. It contacts Google, so selfstore never calls it for you. Safe to call any number of times, and a no-op outside a browser.

The honest trade-off

Stated plainly, because it shapes your UX:

  • Client-only (gisDriveAuth): zero backend, periodic re-consent. Google Identity Services issues roughly hour-long tokens to pure browser apps, and third-party-cookie rules prevent silent renewal forever. Expect an occasional one-click re-consent. Fine for a backup that syncs a few times a day.
  • Permanent connection: a small token broker. For “connected for good”, exchange the OAuth code server-side for a refresh token and mint short-lived access tokens on demand. That is the one place a tiny backend buys real UX, and it still never sees data. DriveAuth is three methods, so a broker slots in:
import type { DriveAuth } from 'selfstore';

const brokerAuth: DriveAuth = {
  async token({ signal } = {}) { /* fetch a fresh access token; pass signal to your fetch */ },
  async reconnect() { /* run consent again; true if access re-established */ },
  async forget() { /* drop the session, locally and broker-side */ },
};
await store.connectDrive(brokerAuth);

Either way, a genuine loss of access (a real 401) surfaces as store.status.action === 'reconnect'; a cold start or a blip is retried silently and never bothers the user.

token() also receives a signal (since 1.1.0) that fires the moment the user disconnects the backup. A broker running its own retry loop should pass it to its fetches and stop retrying at once, so a “disconnect” tap lands immediately instead of waiting for a slow request to time out. Ignoring the signal keeps working exactly as before - the loop just runs to its own deadline first.

On the advanced store

Driving createLocalStore yourself, the home is driveTarget from the advanced subpath, which also ships shared-file primitives (preview, adopt, findOrCreateOwnFile) for pointing several devices at one file:

import { driveTarget, gisDriveAuth } from 'selfstore/advanced';

const drive = () => driveTarget.connect({ auth, kv: cache.kv, fileName: 'my-app.zip' });
await store.attachTarget(await drive(), { strategy: 'merge', password });

Sharing with other people

Handing a copy to somebody else is a different file, never the backup itself: createCompanion mints one next to it, share publishes it on a link, unshare takes that back, owner says whose Drive a copy lives on, and secondary is the read-write target a mirror publishes into. The full reference is in the advanced API; the topology those calls serve is peers.

One thing they deliberately do not do: read another account’s shared file. The drive.file scope only sees files your app created or the user picked, so reaching somebody else’s copy needs either the Google Picker (a gesture per file) or a relay you host - and a relay is a server, which is not a decision a zero-server library gets to make for you.