[Examples & benchmarks](https://cdpfleet.com/docs/cases) / #39 · 2026-10-12 · Example · Medium · for [ai agents](https://cdpfleet.com/ai-agents), [web scraping](https://cdpfleet.com/web-scraping)

# Saved the login, still logged out: storageState and IndexedDB

Firebase Auth, and apps built on SDKs that persist to IndexedDB, keep the login there, which storageState leaves out by default. One option brings it along; Map and Blob values still don't survive.

[Chromium](https://cdpfleet.com/docs/browsers/chromium)

Run on production on 2026-10-12: ✓ Node.js ✓ Python ✓ Java ✓ C# ✓ Go

## The problem

You log in once, save the state with `storageState()`, restore it in the next session — and the app greets you with a sign-in form. The cookies and localStorage came back, but the login wasn't in them: Firebase Auth, and apps built on SDKs that persist to IndexedDB, keep the signed-in user and its refresh token in **IndexedDB** (most other auth SDKs default to localStorage, sessionStorage or memory). By default Playwright doesn't save IndexedDB at all. [Case 11](https://cdpfleet.com/docs/cases/persist-login) carried cookies and localStorage between browsers; this one carries the rest.

## What we used, and why

| What | Why |
| --- | --- |
| A Firebase-shaped login | The record Firebase Auth's web SDK writes after sign-in: database `firebaseLocalStorageDb`, store `firebaseLocalStorage`, key `firebase:authUser:<apiKey>:[DEFAULT]`, the user and their tokens as the value. Plus a cookie and a localStorage value, the way most sites mix them. |
| `storageState()` | The default export: cookies and each origin's localStorage. |
| `storageState({ indexedDB: true })` | Also exports every IndexedDB database of every origin the context visited (Playwright 1.51+). Python `indexed_db=True`, Java `setIndexedDB(true)`, C# `IndexedDB = true`, Go `IndexedDB: playwright.Bool(true)`. |
| `newContext({ storageState })` | Restores the file before the first request, IndexedDB included when the file has it. No option needed on this side. In Go: `context.SetStorageState(path)`. |
| A store of non-JSON values | `Date`, `Uint8Array`, `BigInt`, `Map` and `Blob` in IndexedDB, to see which ones survive the trip through JSON. |
| Two sessions | The first browser is gone before the second starts; only the files carry the login. |

## How it works

1.  Session 1: set a session cookie on httpbin.org, store a theme in localStorage and write the Firebase-shaped user and the typed values to IndexedDB.
2.  Save the state twice: once with the defaults, once with IndexedDB.
3.  Close the browser. Session 2, a new browser: restore each file into its own context.
4.  In each, check the cookie the site receives, the localStorage value and whether the app would find its signed-in user — then which IndexedDB value types came back as what.

## The code

The same program in five languages (also [on GitHub](https://github.com/cdpfleet/cdpfleet/tree/main/cases/39-indexeddb-login-state), with the raw output). Set these environment variables first:

-   `CDPFLEET_API_KEY` — your API key (dashboard → API keys)
-   `PROXY_URL` — your proxy, e.g. `http://user:pass@proxy.example.com:8000`

 

```node
// npm install playwright@1.60.0
// env: CDPFLEET_API_KEY, PROXY_URL
import { chromium } from 'playwright';
import { readFileSync, statSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';

const KEY = process.env.CDPFLEET_API_KEY;
const ORIGIN = 'https://httpbin.org';
// Both files hold a live login: keep them somewhere safe.
const PLAIN = path.join(tmpdir(), 'cdpfleet-state.json');
const WITH_IDB = path.join(tmpdir(), 'cdpfleet-state-idb.json');

async function launch() {
  const res = await fetch('https://starter.cdpfleet.com/chromium/session', {
    method: 'POST',
    headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
    body: JSON.stringify({ proxy: process.env.PROXY_URL, headless: true }),
  });
  if (!res.ok) throw new Error(`launch: ${res.status} ${await res.text()}`);
  return res.json();
}

// What a Firebase Auth web app keeps after sign-in: one record in IndexedDB, nothing in
// cookies or localStorage. Plus a store of values that aren't plain JSON.
const WRITE = async () => {
  const open = (name, setup) => new Promise((ok, no) => {
    const r = indexedDB.open(name, 1);
    r.onupgradeneeded = () => setup(r.result);
    r.onsuccess = () => ok(r.result);
    r.onerror = () => no(r.error);
  });
  const put = (db, store, value, key) => new Promise((ok, no) => {
    const tx = db.transaction(store, 'readwrite');
    tx.objectStore(store).put(value, key);
    tx.oncomplete = ok;
    tx.onerror = () => no(tx.error);
  });
  const fb = await open('firebaseLocalStorageDb', (db) => db.createObjectStore('firebaseLocalStorage', { keyPath: 'fbase_key' }));
  await put(fb, 'firebaseLocalStorage', {
    fbase_key: 'firebase:authUser:AIzaDemoKey:[DEFAULT]',
    value: { uid: 'u_8f3a', email: 'alice@example.com', stsTokenManager: { refreshToken: 'demo-refresh-token', accessToken: 'demo-access-token', expirationTime: Date.now() + 3600000 } },
  });
  const types = await open('typesDb', (db) => db.createObjectStore('values'));
  const values = { date: new Date(0), bytes: new Uint8Array([1, 2, 3]), bigint: 10n, map: new Map([['k', 1]]), blob: new Blob(['hi']) };
  for (const [k, v] of Object.entries(values)) await put(types, 'values', v, k);
  localStorage.setItem('theme', 'dark');
};

// What the app finds on its next visit.
const READ = async () => {
  const dbs = (await indexedDB.databases()).map((d) => d.name);
  const get = (name, store, key) => new Promise((ok) => {
    if (!dbs.includes(name)) return ok(undefined);
    const r = indexedDB.open(name);
    r.onsuccess = () => {
      const q = r.result.transaction(store).objectStore(store).get(key);
      q.onsuccess = () => ok(q.result);
      q.onerror = () => ok(undefined);
    };
    r.onerror = () => ok(undefined);
  });
  const user = await get('firebaseLocalStorageDb', 'firebaseLocalStorage', 'firebase:authUser:AIzaDemoKey:[DEFAULT]');
  const types = {};
  for (const k of ['date', 'bytes', 'bigint', 'map', 'blob']) {
    const v = await get('typesDb', 'values', k);
    const t = v === undefined ? 'missing' : typeof v === 'bigint' ? 'BigInt' : Object.prototype.toString.call(v).slice(8, -1);
    types[k] = t === 'Object' && Object.keys(v).length === 0 ? 'Object {} (emptied)' : t;
  }
  return { local_storage_theme: localStorage.getItem('theme'), firebase_user: user ? user.value.email : null, value_types: types };
};

const cookieSent = async (page) => (await (await page.goto(`${ORIGIN}/cookies`, { timeout: 60000 })).json()).cookies.session === 'abc123';
const idbOrigins = (file) => JSON.parse(readFileSync(file, 'utf8')).origins.filter((o) => o.indexedDB?.length);

// Session 1: "sign in", then save the state twice — the default way and with IndexedDB.
const s1 = await launch();
const b1 = await chromium.connect(s1.wsUrl, { headers: { 'x-api-key': KEY } });
try {
  const ctx = await b1.newContext();
  const page = await ctx.newPage();
  await page.goto(`${ORIGIN}/cookies/set?session=abc123`, { timeout: 60000 });
  await page.evaluate(WRITE);
  await ctx.storageState({ path: PLAIN });
  await ctx.storageState({ path: WITH_IDB, indexedDB: true });
} finally {
  await b1.close();
}

// Session 2: a new browser. Restore each file into its own context and look around.
const s2 = await launch();
const b2 = await chromium.connect(s2.wsUrl, { headers: { 'x-api-key': KEY } });
try {
  const restored = [];
  let valueTypes;
  for (const [label, file] of [['cookies + localStorage', PLAIN], ['with IndexedDB', WITH_IDB]]) {
    const ctx = await b2.newContext({ storageState: file });
    const page = await ctx.newPage();
    const cookie = await cookieSent(page);
    const seen = await page.evaluate(READ);
    restored.push({ state: label, cookie_sent: cookie, local_storage_theme: seen.local_storage_theme, firebase_user: seen.firebase_user, signed_in: seen.firebase_user !== null });
    if (file === WITH_IDB) valueTypes = seen.value_types;
    await ctx.close();
  }
  console.log(JSON.stringify({
    origin: ORIGIN,
    sessions: [s1.sessionId, s2.sessionId],
    state_files: [
      { state: 'cookies + localStorage', bytes: statSync(PLAIN).size, indexeddb_databases: idbOrigins(PLAIN).flatMap((o) => o.indexedDB.map((d) => d.name)) },
      { state: 'with IndexedDB', bytes: statSync(WITH_IDB).size, indexeddb_databases: idbOrigins(WITH_IDB).flatMap((o) => o.indexedDB.map((d) => d.name)) },
    ],
    restored,
    value_types_after_restore: valueTypes,
  }, null, 2));
} finally {
  await b2.close();
}
```

```python
# pip install playwright==1.60.0 requests
# env: CDPFLEET_API_KEY, PROXY_URL
import json
import os
import tempfile

import requests
from playwright.sync_api import sync_playwright

KEY = os.environ["CDPFLEET_API_KEY"]
ORIGIN = "https://httpbin.org"
# Both files hold a live login: keep them somewhere safe.
PLAIN = os.path.join(tempfile.gettempdir(), "cdpfleet-state.json")
WITH_IDB = os.path.join(tempfile.gettempdir(), "cdpfleet-state-idb.json")

def launch():
    r = requests.post(
        "https://starter.cdpfleet.com/chromium/session",
        headers={"x-api-key": KEY},
        json={"proxy": os.environ["PROXY_URL"], "headless": True},
        timeout=90,
    )
    if not r.ok:
        raise RuntimeError(f"launch: {r.status_code} {r.text}")
    return r.json()

# What a Firebase Auth web app keeps after sign-in: one record in IndexedDB, nothing in
# cookies or localStorage. Plus a store of values that aren't plain JSON.
WRITE = """async () => {
  const open = (name, setup) => new Promise((ok, no) => {
    const r = indexedDB.open(name, 1);
    r.onupgradeneeded = () => setup(r.result);
    r.onsuccess = () => ok(r.result);
    r.onerror = () => no(r.error);
  });
  const put = (db, store, value, key) => new Promise((ok, no) => {
    const tx = db.transaction(store, 'readwrite');
    tx.objectStore(store).put(value, key);
    tx.oncomplete = ok;
    tx.onerror = () => no(tx.error);
  });
  const fb = await open('firebaseLocalStorageDb', (db) => db.createObjectStore('firebaseLocalStorage', { keyPath: 'fbase_key' }));
  await put(fb, 'firebaseLocalStorage', {
    fbase_key: 'firebase:authUser:AIzaDemoKey:[DEFAULT]',
    value: { uid: 'u_8f3a', email: 'alice@example.com', stsTokenManager: { refreshToken: 'demo-refresh-token', accessToken: 'demo-access-token', expirationTime: Date.now() + 3600000 } },
  });
  const types = await open('typesDb', (db) => db.createObjectStore('values'));
  const values = { date: new Date(0), bytes: new Uint8Array([1, 2, 3]), bigint: 10n, map: new Map([['k', 1]]), blob: new Blob(['hi']) };
  for (const [k, v] of Object.entries(values)) await put(types, 'values', v, k);
  localStorage.setItem('theme', 'dark');
}"""

# What the app finds on its next visit.
READ = """async () => {
  const dbs = (await indexedDB.databases()).map((d) => d.name);
  const get = (name, store, key) => new Promise((ok) => {
    if (!dbs.includes(name)) return ok(undefined);
    const r = indexedDB.open(name);
    r.onsuccess = () => {
      const q = r.result.transaction(store).objectStore(store).get(key);
      q.onsuccess = () => ok(q.result);
      q.onerror = () => ok(undefined);
    };
    r.onerror = () => ok(undefined);
  });
  const user = await get('firebaseLocalStorageDb', 'firebaseLocalStorage', 'firebase:authUser:AIzaDemoKey:[DEFAULT]');
  const types = {};
  for (const k of ['date', 'bytes', 'bigint', 'map', 'blob']) {
    const v = await get('typesDb', 'values', k);
    const t = v === undefined ? 'missing' : typeof v === 'bigint' ? 'BigInt' : Object.prototype.toString.call(v).slice(8, -1);
    types[k] = t === 'Object' && Object.keys(v).length === 0 ? 'Object {} (emptied)' : t;
  }
  return { local_storage_theme: localStorage.getItem('theme'), firebase_user: user ? user.value.email : null, value_types: types };
}"""

def cookie_sent(page):
    return page.goto(f"{ORIGIN}/cookies", timeout=60000).json()["cookies"].get("session") == "abc123"

def idb_databases(file):
    with open(file) as f:
        origins = json.load(f)["origins"]
    return [d["name"] for o in origins for d in o.get("indexedDB") or []]

with sync_playwright() as p:
    # Session 1: "sign in", then save the state twice: the default way and with IndexedDB.
    s1 = launch()
    b1 = p.chromium.connect(s1["wsUrl"], headers={"x-api-key": KEY})
    try:
        ctx = b1.new_context()
        page = ctx.new_page()
        page.goto(f"{ORIGIN}/cookies/set?session=abc123", timeout=60000)
        page.evaluate(WRITE)
        ctx.storage_state(path=PLAIN)
        ctx.storage_state(path=WITH_IDB, indexed_db=True)
    finally:
        b1.close()

    # Session 2: a new browser. Restore each file into its own context and look around.
    s2 = launch()
    b2 = p.chromium.connect(s2["wsUrl"], headers={"x-api-key": KEY})
    try:
        restored, value_types = [], None
        for label, file in [("cookies + localStorage", PLAIN), ("with IndexedDB", WITH_IDB)]:
            ctx = b2.new_context(storage_state=file)
            page = ctx.new_page()
            cookie = cookie_sent(page)
            seen = page.evaluate(READ)
            restored.append({
                "state": label,
                "cookie_sent": cookie,
                "local_storage_theme": seen["local_storage_theme"],
                "firebase_user": seen["firebase_user"],
                "signed_in": seen["firebase_user"] is not None,
            })
            if file == WITH_IDB:
                value_types = seen["value_types"]
            ctx.close()
        print(json.dumps({
            "origin": ORIGIN,
            "sessions": [s1["sessionId"], s2["sessionId"]],
            "state_files": [
                {"state": "cookies + localStorage", "bytes": os.path.getsize(PLAIN), "indexeddb_databases": idb_databases(PLAIN)},
                {"state": "with IndexedDB", "bytes": os.path.getsize(WITH_IDB), "indexeddb_databases": idb_databases(WITH_IDB)},
            ],
            "restored": restored,
            "value_types_after_restore": value_types,
        }, indent=2))
    finally:
        b2.close()
```

```java
// Maven: com.microsoft.playwright:playwright:1.60.0, com.google.code.gson:gson:2.11.0
// Run with PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1. env: CDPFLEET_API_KEY, PROXY_URL
import com.google.gson.*;
import com.microsoft.playwright.*;
import java.net.URI;
import java.net.http.*;
import java.nio.file.*;
import java.util.*;

public class Main {
  static final String KEY = System.getenv("CDPFLEET_API_KEY");
  static final Gson GSON = new GsonBuilder().serializeNulls().setPrettyPrinting().create();
  static final String ORIGIN = "https://httpbin.org";
  // Both files hold a live login: keep them somewhere safe.
  static final Path PLAIN = Path.of(System.getProperty("java.io.tmpdir"), "cdpfleet-state.json");
  static final Path WITH_IDB = Path.of(System.getProperty("java.io.tmpdir"), "cdpfleet-state-idb.json");

  // What a Firebase Auth web app keeps after sign-in: one record in IndexedDB, nothing in
  // cookies or localStorage. Plus a store of values that aren't plain JSON.
  static final String WRITE = """
      async () => {
        const open = (name, setup) => new Promise((ok, no) => {
          const r = indexedDB.open(name, 1);
          r.onupgradeneeded = () => setup(r.result);
          r.onsuccess = () => ok(r.result);
          r.onerror = () => no(r.error);
        });
        const put = (db, store, value, key) => new Promise((ok, no) => {
          const tx = db.transaction(store, 'readwrite');
          tx.objectStore(store).put(value, key);
          tx.oncomplete = ok;
          tx.onerror = () => no(tx.error);
        });
        const fb = await open('firebaseLocalStorageDb', (db) => db.createObjectStore('firebaseLocalStorage', { keyPath: 'fbase_key' }));
        await put(fb, 'firebaseLocalStorage', {
          fbase_key: 'firebase:authUser:AIzaDemoKey:[DEFAULT]',
          value: { uid: 'u_8f3a', email: 'alice@example.com', stsTokenManager: { refreshToken: 'demo-refresh-token', accessToken: 'demo-access-token', expirationTime: Date.now() + 3600000 } },
        });
        const types = await open('typesDb', (db) => db.createObjectStore('values'));
        const values = { date: new Date(0), bytes: new Uint8Array([1, 2, 3]), bigint: 10n, map: new Map([['k', 1]]), blob: new Blob(['hi']) };
        for (const [k, v] of Object.entries(values)) await put(types, 'values', v, k);
        localStorage.setItem('theme', 'dark');
      }""";

  // What the app finds on its next visit.
  static final String READ = """
      async () => {
        const dbs = (await indexedDB.databases()).map((d) => d.name);
        const get = (name, store, key) => new Promise((ok) => {
          if (!dbs.includes(name)) return ok(undefined);
          const r = indexedDB.open(name);
          r.onsuccess = () => {
            const q = r.result.transaction(store).objectStore(store).get(key);
            q.onsuccess = () => ok(q.result);
            q.onerror = () => ok(undefined);
          };
          r.onerror = () => ok(undefined);
        });
        const user = await get('firebaseLocalStorageDb', 'firebaseLocalStorage', 'firebase:authUser:AIzaDemoKey:[DEFAULT]');
        const types = {};
        for (const k of ['date', 'bytes', 'bigint', 'map', 'blob']) {
          const v = await get('typesDb', 'values', k);
          const t = v === undefined ? 'missing' : typeof v === 'bigint' ? 'BigInt' : Object.prototype.toString.call(v).slice(8, -1);
          types[k] = t === 'Object' && Object.keys(v).length === 0 ? 'Object {} (emptied)' : t;
        }
        return { local_storage_theme: localStorage.getItem('theme'), firebase_user: user ? user.value.email : null, value_types: types };
      }""";

  static JsonObject launch() throws Exception {
    String body = "{\"proxy\": " + GSON.toJson(System.getenv("PROXY_URL")) + ", \"headless\": true}";
    HttpResponse<String> res = HttpClient.newHttpClient().send(HttpRequest.newBuilder(URI.create("https://starter.cdpfleet.com/chromium/session"))
        .header("x-api-key", KEY).header("content-type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(body)).build(), HttpResponse.BodyHandlers.ofString());
    if (res.statusCode() != 200) throw new RuntimeException("launch: " + res.statusCode() + " " + res.body());
    return JsonParser.parseString(res.body()).getAsJsonObject();
  }

  static boolean cookieSent(Page page) {
    page.navigate(ORIGIN + "/cookies", new Page.NavigateOptions().setTimeout(60000));
    JsonObject cookies = JsonParser.parseString(page.textContent("body")).getAsJsonObject().getAsJsonObject("cookies");
    return cookies.has("session") && cookies.get("session").getAsString().equals("abc123");
  }

  static JsonArray idbDatabases(Path file) throws Exception {
    JsonArray names = new JsonArray();
    for (JsonElement o : JsonParser.parseString(Files.readString(file)).getAsJsonObject().getAsJsonArray("origins")) {
      JsonElement idb = o.getAsJsonObject().get("indexedDB");
      if (idb != null && idb.isJsonArray()) for (JsonElement d : idb.getAsJsonArray()) names.add(d.getAsJsonObject().get("name"));
    }
    return names;
  }

  static JsonObject stateFile(String label, Path file) throws Exception {
    JsonObject f = new JsonObject();
    f.addProperty("state", label);
    f.addProperty("bytes", Files.size(file));
    f.add("indexeddb_databases", idbDatabases(file));
    return f;
  }

  public static void main(String[] args) throws Exception {
    try (Playwright playwright = Playwright.create()) {
      BrowserType.ConnectOptions auth = new BrowserType.ConnectOptions().setHeaders(Map.of("x-api-key", KEY));

      // Session 1: "sign in", then save the state twice: the default way and with IndexedDB.
      JsonObject s1 = launch();
      Browser b1 = playwright.chromium().connect(s1.get("wsUrl").getAsString(), auth);
      try {
        BrowserContext ctx = b1.newContext();
        Page page = ctx.newPage();
        page.navigate(ORIGIN + "/cookies/set?session=abc123", new Page.NavigateOptions().setTimeout(60000));
        page.evaluate("(" + WRITE + ")()");
        ctx.storageState(new BrowserContext.StorageStateOptions().setPath(PLAIN));
        ctx.storageState(new BrowserContext.StorageStateOptions().setPath(WITH_IDB).setIndexedDB(true));
      } finally {
        b1.close();
      }

      // Session 2: a new browser. Restore each file into its own context and look around.
      JsonObject s2 = launch();
      Browser b2 = playwright.chromium().connect(s2.get("wsUrl").getAsString(), auth);
      try {
        JsonArray restored = new JsonArray();
        JsonElement valueTypes = JsonNull.INSTANCE;
        Map<String, Path> files = new LinkedHashMap<>();
        files.put("cookies + localStorage", PLAIN);
        files.put("with IndexedDB", WITH_IDB);
        for (Map.Entry<String, Path> f : files.entrySet()) {
          BrowserContext ctx = b2.newContext(new Browser.NewContextOptions().setStorageStatePath(f.getValue()));
          Page page = ctx.newPage();
          boolean cookie = cookieSent(page);
          JsonObject seen = GSON.toJsonTree(page.evaluate("(" + READ + ")()")).getAsJsonObject();
          JsonObject r = new JsonObject();
          r.addProperty("state", f.getKey());
          r.addProperty("cookie_sent", cookie);
          r.add("local_storage_theme", seen.get("local_storage_theme"));
          r.add("firebase_user", seen.get("firebase_user"));
          r.addProperty("signed_in", !seen.get("firebase_user").isJsonNull());
          restored.add(r);
          if (f.getValue().equals(WITH_IDB)) valueTypes = seen.get("value_types");
          ctx.close();
        }
        JsonObject out = new JsonObject();
        out.addProperty("origin", ORIGIN);
        JsonArray sessions = new JsonArray();
        sessions.add(s1.get("sessionId"));
        sessions.add(s2.get("sessionId"));
        out.add("sessions", sessions);
        JsonArray stateFiles = new JsonArray();
        stateFiles.add(stateFile("cookies + localStorage", PLAIN));
        stateFiles.add(stateFile("with IndexedDB", WITH_IDB));
        out.add("state_files", stateFiles);
        out.add("restored", restored);
        out.add("value_types_after_restore", valueTypes);
        System.out.println(GSON.toJson(out));
      } finally {
        b2.close();
      }
    }
  }
}
```

```csharp
// dotnet add package Microsoft.Playwright --version 1.60.0
// env: CDPFLEET_API_KEY, PROXY_URL
using System.Net.Http.Json;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Playwright;

const string Origin = "https://httpbin.org";
// Both files hold a live login: keep them somewhere safe.
var plain = Path.Combine(Path.GetTempPath(), "cdpfleet-state.json");
var withIdb = Path.Combine(Path.GetTempPath(), "cdpfleet-state-idb.json");

// What a Firebase Auth web app keeps after sign-in: one record in IndexedDB, nothing in
// cookies or localStorage. Plus a store of values that aren't plain JSON.
const string Write = """
    async () => {
      const open = (name, setup) => new Promise((ok, no) => {
        const r = indexedDB.open(name, 1);
        r.onupgradeneeded = () => setup(r.result);
        r.onsuccess = () => ok(r.result);
        r.onerror = () => no(r.error);
      });
      const put = (db, store, value, key) => new Promise((ok, no) => {
        const tx = db.transaction(store, 'readwrite');
        tx.objectStore(store).put(value, key);
        tx.oncomplete = ok;
        tx.onerror = () => no(tx.error);
      });
      const fb = await open('firebaseLocalStorageDb', (db) => db.createObjectStore('firebaseLocalStorage', { keyPath: 'fbase_key' }));
      await put(fb, 'firebaseLocalStorage', {
        fbase_key: 'firebase:authUser:AIzaDemoKey:[DEFAULT]',
        value: { uid: 'u_8f3a', email: 'alice@example.com', stsTokenManager: { refreshToken: 'demo-refresh-token', accessToken: 'demo-access-token', expirationTime: Date.now() + 3600000 } },
      });
      const types = await open('typesDb', (db) => db.createObjectStore('values'));
      const values = { date: new Date(0), bytes: new Uint8Array([1, 2, 3]), bigint: 10n, map: new Map([['k', 1]]), blob: new Blob(['hi']) };
      for (const [k, v] of Object.entries(values)) await put(types, 'values', v, k);
      localStorage.setItem('theme', 'dark');
    }
    """;

// What the app finds on its next visit.
const string Read = """
    async () => {
      const dbs = (await indexedDB.databases()).map((d) => d.name);
      const get = (name, store, key) => new Promise((ok) => {
        if (!dbs.includes(name)) return ok(undefined);
        const r = indexedDB.open(name);
        r.onsuccess = () => {
          const q = r.result.transaction(store).objectStore(store).get(key);
          q.onsuccess = () => ok(q.result);
          q.onerror = () => ok(undefined);
        };
        r.onerror = () => ok(undefined);
      });
      const user = await get('firebaseLocalStorageDb', 'firebaseLocalStorage', 'firebase:authUser:AIzaDemoKey:[DEFAULT]');
      const types = {};
      for (const k of ['date', 'bytes', 'bigint', 'map', 'blob']) {
        const v = await get('typesDb', 'values', k);
        const t = v === undefined ? 'missing' : typeof v === 'bigint' ? 'BigInt' : Object.prototype.toString.call(v).slice(8, -1);
        types[k] = t === 'Object' && Object.keys(v).length === 0 ? 'Object {} (emptied)' : t;
      }
      return { local_storage_theme: localStorage.getItem('theme'), firebase_user: user ? user.value.email : null, value_types: types };
    }
    """;

var key = Environment.GetEnvironmentVariable("CDPFLEET_API_KEY")!;
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("x-api-key", key);
async Task<JsonElement> Launch()
{
    var res = await http.PostAsJsonAsync("https://starter.cdpfleet.com/chromium/session",
        new { proxy = Environment.GetEnvironmentVariable("PROXY_URL"), headless = true });
    if (!res.IsSuccessStatusCode) throw new Exception($"launch: {(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}");
    return await res.Content.ReadFromJsonAsync<JsonElement>();
}

async Task<bool> CookieSent(IPage page)
{
    await page.GotoAsync($"{Origin}/cookies", new() { Timeout = 60000 });
    var cookies = JsonNode.Parse(await page.TextContentAsync("body") ?? "{}")!["cookies"]!.AsObject();
    return cookies["session"]?.GetValue<string>() == "abc123";
}

JsonArray IdbDatabases(string file)
{
    var names = new JsonArray();
    foreach (var o in JsonNode.Parse(File.ReadAllText(file))!["origins"]!.AsArray())
        if (o!["indexedDB"] is JsonArray dbs)
            foreach (var d in dbs) names.Add(d!["name"]!.GetValue<string>());
    return names;
}

using var playwright = await Playwright.CreateAsync();
var auth = new BrowserTypeConnectOptions { Headers = new Dictionary<string, string> { ["x-api-key"] = key } };

// Session 1: "sign in", then save the state twice: the default way and with IndexedDB.
var s1 = await Launch();
var b1 = await playwright.Chromium.ConnectAsync(s1.GetProperty("wsUrl").GetString()!, auth);
try
{
    var ctx = await b1.NewContextAsync();
    var page = await ctx.NewPageAsync();
    await page.GotoAsync($"{Origin}/cookies/set?session=abc123", new() { Timeout = 60000 });
    await page.EvaluateAsync(Write);
    await ctx.StorageStateAsync(new() { Path = plain });
    await ctx.StorageStateAsync(new() { Path = withIdb, IndexedDB = true });
}
finally
{
    await b1.CloseAsync();
}

// Session 2: a new browser. Restore each file into its own context and look around.
var s2 = await Launch();
var b2 = await playwright.Chromium.ConnectAsync(s2.GetProperty("wsUrl").GetString()!, auth);
try
{
    var restored = new JsonArray();
    JsonNode? valueTypes = null;
    foreach (var (label, file) in new[] { ("cookies + localStorage", plain), ("with IndexedDB", withIdb) })
    {
        var ctx = await b2.NewContextAsync(new() { StorageStatePath = file });
        var page = await ctx.NewPageAsync();
        var cookie = await CookieSent(page);
        var seen = JsonNode.Parse((await page.EvaluateAsync<JsonElement>(Read)).GetRawText())!;
        var user = seen["firebase_user"]?.GetValue<string>();
        restored.Add(new JsonObject
        {
            ["state"] = label,
            ["cookie_sent"] = cookie,
            ["local_storage_theme"] = seen["local_storage_theme"]?.GetValue<string>(),
            ["firebase_user"] = user,
            ["signed_in"] = user != null,
        });
        if (file == withIdb) valueTypes = seen["value_types"]?.DeepClone();
        await ctx.CloseAsync();
    }
    var output = new JsonObject
    {
        ["origin"] = Origin,
        ["sessions"] = new JsonArray(s1.GetProperty("sessionId").GetString(), s2.GetProperty("sessionId").GetString()),
        ["state_files"] = new JsonArray(
            new JsonObject { ["state"] = "cookies + localStorage", ["bytes"] = new FileInfo(plain).Length, ["indexeddb_databases"] = IdbDatabases(plain) },
            new JsonObject { ["state"] = "with IndexedDB", ["bytes"] = new FileInfo(withIdb).Length, ["indexeddb_databases"] = IdbDatabases(withIdb) }),
        ["restored"] = restored,
        ["value_types_after_restore"] = valueTypes,
    };
    Console.WriteLine(output.ToJsonString(new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }));
}
finally
{
    await b2.CloseAsync();
}
```

```go
// go get github.com/playwright-community/playwright-go@v0.6000.0
// Driver: build playwright-core 1.60.0 from npm and set PLAYWRIGHT_DRIVER_PATH (see /docs/quickstart).
// env: CDPFLEET_API_KEY, PROXY_URL
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"

	"github.com/playwright-community/playwright-go"
)

const origin = "https://httpbin.org"

var (
	key = os.Getenv("CDPFLEET_API_KEY")
	// Both files hold a live login: keep them somewhere safe.
	plain   = filepath.Join(os.TempDir(), "cdpfleet-state.json")
	withIdb = filepath.Join(os.TempDir(), "cdpfleet-state-idb.json")
)

// What a Firebase Auth web app keeps after sign-in: one record in IndexedDB, nothing in
// cookies or localStorage. Plus a store of values that aren't plain JSON.
const write = `async () => {
  const open = (name, setup) => new Promise((ok, no) => {
    const r = indexedDB.open(name, 1);
    r.onupgradeneeded = () => setup(r.result);
    r.onsuccess = () => ok(r.result);
    r.onerror = () => no(r.error);
  });
  const put = (db, store, value, key) => new Promise((ok, no) => {
    const tx = db.transaction(store, 'readwrite');
    tx.objectStore(store).put(value, key);
    tx.oncomplete = ok;
    tx.onerror = () => no(tx.error);
  });
  const fb = await open('firebaseLocalStorageDb', (db) => db.createObjectStore('firebaseLocalStorage', { keyPath: 'fbase_key' }));
  await put(fb, 'firebaseLocalStorage', {
    fbase_key: 'firebase:authUser:AIzaDemoKey:[DEFAULT]',
    value: { uid: 'u_8f3a', email: 'alice@example.com', stsTokenManager: { refreshToken: 'demo-refresh-token', accessToken: 'demo-access-token', expirationTime: Date.now() + 3600000 } },
  });
  const types = await open('typesDb', (db) => db.createObjectStore('values'));
  const values = { date: new Date(0), bytes: new Uint8Array([1, 2, 3]), bigint: 10n, map: new Map([['k', 1]]), blob: new Blob(['hi']) };
  for (const [k, v] of Object.entries(values)) await put(types, 'values', v, k);
  localStorage.setItem('theme', 'dark');
}`

// What the app finds on its next visit.
const read = `async () => {
  const dbs = (await indexedDB.databases()).map((d) => d.name);
  const get = (name, store, key) => new Promise((ok) => {
    if (!dbs.includes(name)) return ok(undefined);
    const r = indexedDB.open(name);
    r.onsuccess = () => {
      const q = r.result.transaction(store).objectStore(store).get(key);
      q.onsuccess = () => ok(q.result);
      q.onerror = () => ok(undefined);
    };
    r.onerror = () => ok(undefined);
  });
  const user = await get('firebaseLocalStorageDb', 'firebaseLocalStorage', 'firebase:authUser:AIzaDemoKey:[DEFAULT]');
  const types = {};
  for (const k of ['date', 'bytes', 'bigint', 'map', 'blob']) {
    const v = await get('typesDb', 'values', k);
    const t = v === undefined ? 'missing' : typeof v === 'bigint' ? 'BigInt' : Object.prototype.toString.call(v).slice(8, -1);
    types[k] = t === 'Object' && Object.keys(v).length === 0 ? 'Object {} (emptied)' : t;
  }
  return { local_storage_theme: localStorage.getItem('theme'), firebase_user: user ? user.value.email : null, value_types: types };
}`

func launch() (map[string]any, error) {
	body, _ := json.Marshal(map[string]any{"proxy": os.Getenv("PROXY_URL"), "headless": true})
	req, _ := http.NewRequest("POST", "https://starter.cdpfleet.com/chromium/session", bytes.NewReader(body))
	req.Header.Set("x-api-key", key)
	req.Header.Set("content-type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		msg, _ := io.ReadAll(res.Body)
		return nil, fmt.Errorf("launch: %s %s", res.Status, msg)
	}
	var session map[string]any
	return session, json.NewDecoder(res.Body).Decode(&session)
}

type stateFile struct {
	Origins []struct {
		Origin    string           `json:"origin"`
		IndexedDB []map[string]any `json:"indexedDB"`
	} `json:"origins"`
}

func readState(file string) stateFile {
	var s stateFile
	data, err := os.ReadFile(file)
	if err != nil {
		log.Fatal(err)
	}
	if err := json.Unmarshal(data, &s); err != nil {
		log.Fatal(err)
	}
	return s
}

func idbDatabases(file string) []string {
	names := []string{}
	for _, o := range readState(file).Origins {
		for _, d := range o.IndexedDB {
			names = append(names, d["name"].(string))
		}
	}
	return names
}

func cookieSent(page playwright.Page) bool {
	res, err := page.Goto(origin+"/cookies", playwright.PageGotoOptions{Timeout: playwright.Float(60000)})
	if err != nil {
		log.Fatal(err)
	}
	var body struct {
		Cookies map[string]string `json:"cookies"`
	}
	if err := res.JSON(&body); err != nil {
		log.Fatal(err)
	}
	return body.Cookies["session"] == "abc123"
}

type restoredState struct {
	State             string `json:"state"`
	CookieSent        bool   `json:"cookie_sent"`
	LocalStorageTheme any    `json:"local_storage_theme"`
	FirebaseUser      any    `json:"firebase_user"`
	SignedIn          bool   `json:"signed_in"`
}

type fileInfo struct {
	State              string   `json:"state"`
	Bytes              int64    `json:"bytes"`
	IndexedDBDatabases []string `json:"indexeddb_databases"`
}

func size(file string) int64 {
	st, err := os.Stat(file)
	if err != nil {
		log.Fatal(err)
	}
	return st.Size()
}

// Session 1: set a cookie, write the Firebase-shaped login, save both state files.
func signIn(pw *playwright.Playwright, auth playwright.BrowserTypeConnectOptions) map[string]any {
	s1, err := launch()
	if err != nil {
		log.Fatal(err)
	}
	b1, err := pw.Chromium.Connect(s1["wsUrl"].(string), auth)
	if err != nil {
		log.Fatal(err)
	}
	// log.Panic below (not Fatal) so this deferred Close still ends the fleet session.
	defer b1.Close()
	ctx, err := b1.NewContext()
	if err != nil {
		log.Panic(err)
	}
	page, err := ctx.NewPage()
	if err != nil {
		log.Panic(err)
	}
	if _, err := page.Goto(origin+"/cookies/set?session=abc123", playwright.PageGotoOptions{Timeout: playwright.Float(60000)}); err != nil {
		log.Panic(err)
	}
	if _, err := page.Evaluate(write); err != nil {
		log.Panic(err)
	}
	if _, err := ctx.StorageState(playwright.BrowserContextStorageStateOptions{Path: playwright.String(plain)}); err != nil {
		log.Panic(err)
	}
	if _, err := ctx.StorageState(playwright.BrowserContextStorageStateOptions{Path: playwright.String(withIdb), IndexedDB: playwright.Bool(true)}); err != nil {
		log.Panic(err)
	}
	return s1
}

func main() {
	pw, err := playwright.Run(&playwright.RunOptions{SkipInstallBrowsers: true})
	if err != nil {
		log.Fatal(err)
	}
	defer pw.Stop()
	auth := playwright.BrowserTypeConnectOptions{Headers: map[string]string{"x-api-key": key}}

	// Session 1: "sign in", then save the state twice: the default way and with IndexedDB.
	s1 := signIn(pw, auth)

	// Session 2: a new browser. Restore each file into its own context and look around.
	s2, err := launch()
	if err != nil {
		log.Fatal(err)
	}
	b2, err := pw.Chromium.Connect(s2["wsUrl"].(string), auth)
	if err != nil {
		log.Fatal(err)
	}
	defer b2.Close()
	restored := []restoredState{}
	var valueTypes any
	for _, f := range []struct{ label, file string }{{"cookies + localStorage", plain}, {"with IndexedDB", withIdb}} {
		// Not NewContext's StorageStatePath: playwright-go parses that file into a typed
		// struct with no IndexedDB field and silently drops it. SetStorageState passes the
		// file to Playwright untouched.
		ctx, err := b2.NewContext()
		if err != nil {
			log.Fatal(err)
		}
		if err := ctx.SetStorageState(f.file); err != nil {
			log.Fatal(err)
		}
		page, err := ctx.NewPage()
		if err != nil {
			log.Fatal(err)
		}
		cookie := cookieSent(page)
		v, err := page.Evaluate(read)
		if err != nil {
			log.Fatal(err)
		}
		seen := v.(map[string]any)
		restored = append(restored, restoredState{
			State: f.label, CookieSent: cookie, LocalStorageTheme: seen["local_storage_theme"],
			FirebaseUser: seen["firebase_user"], SignedIn: seen["firebase_user"] != nil,
		})
		if f.file == withIdb {
			valueTypes = seen["value_types"]
		}
		ctx.Close()
	}
	out, _ := json.MarshalIndent(map[string]any{
		"origin":   origin,
		"sessions": []any{s1["sessionId"], s2["sessionId"]},
		"state_files": []fileInfo{
			{"cookies + localStorage", size(plain), idbDatabases(plain)},
			{"with IndexedDB", size(withIdb), idbDatabases(withIdb)},
		},
		"restored":                  restored,
		"value_types_after_restore": valueTypes,
	}, "", "  ")
	fmt.Println(string(out))
}
```

## What we got

| State file | File size (bytes) | Cookie sent | localStorage | Firebase user in IndexedDB | App signed in |
| --- | --- | --- | --- | --- | --- |
| cookies + localStorage | 403 | yes | dark | — | no |
| with IndexedDB | 2551 | yes | dark | alice@example.com | yes |

From the Node.js run on 2026-10-12. IP addresses are replaced with placeholders (203.0.113.x); equal addresses stay equal. The other languages produced the same findings.

Raw output (Node.js)

```
{
  "origin": "https://httpbin.org",
  "sessions": [
    "0098446e-ada1-4746-a61b-94c8d93cce96",
    "92a2fc86-8733-4f28-be01-5e2efa9e8548"
  ],
  "state_files": [
    {
      "state": "cookies + localStorage",
      "bytes": 403,
      "indexeddb_databases": []
    },
    {
      "state": "with IndexedDB",
      "bytes": 2551,
      "indexeddb_databases": [
        "firebaseLocalStorageDb",
        "typesDb"
      ]
    }
  ],
  "restored": [
    {
      "state": "cookies + localStorage",
      "cookie_sent": true,
      "local_storage_theme": "dark",
      "firebase_user": null,
      "signed_in": false
    },
    {
      "state": "with IndexedDB",
      "cookie_sent": true,
      "local_storage_theme": "dark",
      "firebase_user": "alice@example.com",
      "signed_in": true
    }
  ],
  "value_types_after_restore": {
    "date": "Date",
    "bytes": "Uint8Array",
    "bigint": "BigInt",
    "map": "Object {} (emptied)",
    "blob": "Object {} (emptied)"
  }
}
```

## Takeaways

-   **The default file brought back the cookie and localStorage but not the user** — a Firebase app would show its sign-in form. With `indexedDB: true` the user and its tokens came back too, so the app would be signed in.
-   **Only saving needs the option.** `newContext({ storageState })` restores whatever IndexedDB is in the file. The file grew from a few hundred bytes to about 1–2.5 KB here (Node writes it indented) — and it now holds the refresh token in plain text, so guard it like a password.
-   **Not every value survives:** `Date`, `Uint8Array` and `BigInt` came back as themselves; a `Map` and a `Blob` came back as empty objects `{}`. Check what the app you depend on stores before trusting a restored login.
-   **In Go, restore with `SetStorageState`:** playwright-go 0.6000's `StorageStatePath` on `NewContext` drops IndexedDB (it parses the file into a typed struct with no field for it — tried here, the app came back signed out). `context.SetStorageState(path)` passes the file to Playwright untouched, IndexedDB included. The other four clients restore it from `newContext` as is.

---
Source: https://cdpfleet.com/docs/cases/indexeddb-login-state · Markdown: https://cdpfleet.com/docs/cases/indexeddb-login-state.md · All docs for AI tools: https://cdpfleet.com/llms.txt
