Search selfstore
v1.8.21

Google Drive home

The Drive home stores the encrypted backup in the user’s own Drive, not yours. You never operate storage, and Google never sees cleartext: the file is encrypted before it leaves the device, so the cloud holds opaque bytes.

One call

import { gisDriveAuth } from 'selfstore';

const outcome = await store.connectDrive(gisDriveAuth({ clientId: GOOGLE_CLIENT_ID }));
// 'started' | 'merged' | 'manual' | 'cancelled'  (see quick start)

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.

If the Drive file is encrypted, pass the password so the merge can read it: store.connectDrive(auth, { password }). Without it the call throws PASSWORD_REQUIRED before changing anything, so you can prompt and retry.

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.