# Feature Request — `@shade/storage-indexeddb`

**To**: Shade SDK team
**From**: Dispatch (browser-based Shade consumer)
**Target**: Shade SDK 4.3.x (or whichever release vehicle fits)
**Priority**: blocks all browser-based Shade apps from achieving session
persistence across tab refresh

---

## Summary

Ship an official IndexedDB-backed `StorageProvider` adapter as a new
workspace package `@shade/storage-indexeddb`, so browser-based Shade SDK
consumers can persist identity, prekeys, sessions, and peer-verification
state across tab refresh and browser restart — the same way `@shade/storage-sqlite`
does for Node and `@shade/storage-postgres` does for server deployments.

## Problem

Today the Shade SDK ships three storage paths:

| spec                              | adapter                  | environment      |
| --------------------------------- | ------------------------ | ---------------- |
| `"memory"`                        | `MemoryStorage` (in-SDK) | tests, ephemeral |
| `"sqlite:/path"`                  | `@shade/storage-sqlite`  | Node             |
| `{ type: 'postgres', url: '…' }`  | `@shade/storage-postgres`| Node servers     |

There is **no browser-storage option**. The only way to run Shade in a
browser today is `storage: "memory"`, which means:

- Identity keypair regenerates on every page load
- Sessions reset → re-enrollment after every refresh
- `getLocalRegistrationId()` returns a fresh value → `device:${id}`
  address changes → server-side device record orphaned every reload

This forces every browser-based Shade app to either (a) accept the broken
UX, or (b) build their own `StorageProvider` from scratch — duplicating
~25 methods × N consumers, with no shared conformance test surface.

The right place to solve this is at the SDK level, exactly mirroring how
SQLite and Postgres are handled.

## Proposed package

`packages/shade-storage-indexeddb/` — modeled directly after
`packages/shade-storage-sqlite/`. Same package shape, same test layout,
same `@shade/core`-only runtime dependency surface.

### Public API

```ts
// @shade/storage-indexeddb
export class IndexedDBStorage implements StorageProvider {
  /**
   * Open (or create) the IndexedDB database. Idempotent — repeated calls
   * with the same dbName return a connection sharing the same object stores.
   */
  static async create(opts?: { dbName?: string }): Promise<IndexedDBStorage>;

  /**
   * Cleanly close the underlying connection. Future calls will reopen.
   * Called by Shade.shutdown() when consumers register cleanup.
   */
  async close(): Promise<void>;

  // ─── all StorageProvider methods (identity, prekeys, sessions,
  //      retired identities, peer verifications, optional stream-state) ───
}
```

`dbName` defaults to something like `"shade"`. Consumers like Dispatch
will pass distinct names per app (`"dispatch-dashboard-shade"`,
`"dispatch-host-ui-shade"`) so DevTools' IndexedDB inspector groups them
sensibly, even though origin-isolation already makes the data isolated.

### SDK integration

`@shade/sdk` `resolveStorage()` gets a fourth branch:

```ts
if (typeof spec === 'object' && spec.type === 'indexeddb') {
  const moduleId = '@shade/storage-indexeddb';
  const mod = (await import(moduleId)) as {
    IndexedDBStorage: { create(opts: { dbName?: string }): Promise<StorageProvider> };
  };
  return mod.IndexedDBStorage.create({ dbName: spec.dbName });
}
```

Dynamic import keeps `@shade/storage-indexeddb` an optional dependency,
matching the Postgres pattern — Node-only consumers don't need to install
a browser-only adapter.

Consumer surface:

```ts
const shade = await createShade({
  prekeyServer: 'https://…/shade-prekey',
  storage: { type: 'indexeddb', dbName: 'my-app-shade' },
  address: 'device:user@example.com', // optional — falls back to device:${registrationId}
});
```

## Implementation guidance (non-prescriptive)

- **IDB wrapper**: suggest `idb` (Jake Archibald's thin wrapper, well-typed,
  zero deps). Avoid Dexie or idb-keyval — we want full schema control to
  match the SQL adapters' explicit schemas.
- **Object-store layout**: one store per StorageProvider category
  (`identity`, `signedPreKeys`, `oneTimePreKeys`, `sessions`,
  `trustedIdentities`, `retiredIdentities`, `peerVerifications`,
  `streamStates`). Keypaths match the natural keys (`keyId`, `address`,
  `streamId`).
- **Schema version**: integer, bumped on every shape change. Migrations
  in `db.upgrade(...)` callback. Document schema-history alongside
  the SQLite schema.
- **Concurrency**: IndexedDB transactions are auto-committing — the adapter
  must keep operations within a single transaction where SQL adapters do.
  Particular care for `bumpPeerIdentityVersion` (atomic read-modify-write).
- **Stream-state methods**: implement them. Browser apps will increasingly
  use `@shade/transfer` for large file resume, and parity with SQLite's
  capabilities matters.

## Test expectations

Mirror `packages/shade-storage-sqlite/tests/`:

- `indexeddb-storage.test.ts` — full StorageProvider surface (identity,
  sessions, trusted identities, retired identities)
- `indexeddb-prekey-store.test.ts` — signed + one-time prekey lifecycle
- `peer-verifications.test.ts` — verification CRUD + identity-version
  bumping invariants
- `indexeddb-stream-state.test.ts` (if stream-state is implemented)

Use **`fake-indexeddb`** for the Node test environment — it's the
established standard, supports the v3 spec, and lets us run IDB tests
in `bun test` / `vitest` / `jest` without a real browser.

If/when Shade gains a shared `StorageProvider` conformance test suite,
this adapter should consume it directly. Until then, follow the SQLite
adapter's per-method coverage style.

## Acceptance criteria

1. `@shade/storage-indexeddb` published at version 4.3.0 (or whichever
   matches the next Shade release)
2. `@shade/sdk` `resolveStorage()` resolves `{ type: 'indexeddb', dbName? }`
   via dynamic import
3. Full StorageProvider conformance in tests (identity, prekeys,
   sessions, retired identities, peer verifications, stream-state)
4. Documented in Shade docs alongside SQLite/Postgres adapters
5. README example showing browser-app integration
6. Bundle-size note: dynamic-imported IDB module shouldn't pull crypto
   dependencies — adapter should be ≤ ~10 KB minified+gzipped

## Out of scope (deferred)

- **Encryption-at-rest** for the IDB contents — separate work item; should
  match the deviceKey-AES-GCM pattern Shade already uses for `secretEnc`
  in `PersistedStreamState`. This adapter ships unencrypted-at-rest in v1
  (consistent with SQLite), with the encryption layer added uniformly to
  all adapters later.
- **Cross-tab BroadcastChannel sync** — IDB is shared across same-origin
  tabs already; concurrent writes work via IDB transactions. Real-time
  notification across tabs (e.g. "session was rotated in another tab") is
  a separate concern, not storage-adapter scope.
- **Quota handling** — IDB quota for a Shade keystore is far below realistic
  browser quotas. If it ever becomes relevant, add a `QuotaExceededError`
  observability hook then.

## Why this can't be done in consumer-land

Building this in Dispatch (or any other consumer) would mean:

- 25+ method `StorageProvider` re-implementation per consumer
- No shared conformance tests
- Schema drift across consumers — each would invent its own object-store
  shape, blocking any future cross-app data import/export
- Every Shade SDK update that adjusts `StorageProvider` would force every
  consumer to track and patch independently

`@shade/storage-sqlite` and `@shade/storage-postgres` are part of the SDK
for the same reason. IndexedDB belongs alongside them.

## What unblocks

Shipping this unblocks Dispatch's "Slice 2.5" — persistent enrollment
across browser refresh, which today is the largest QA-friction point in
the dev loop. Any future browser-Shade consumer (web dashboard,
contact-list app, browser-extension messenger) gets persistence for free.
