Playwright login-state guide

Connect Playwright to a profile that already remembers login.

storageState files go stale the day a session rotates. A persistent profile is live: connect Playwright over CDP and the login is already there.

Guide updated 2026-09-01. Follow the steps in order.

Direct answer

Keep the login in an AliasMode profile, start it through the Local API, and connect with chromium.connectOverCDP — no storageState export, no login flows in your tests.

What you will accomplish

Before you start

  • AliasMode installed on Windows with the Local API on 127.0.0.1:50400
  • A Node.js project with playwright installed
  • An AliasMode profile with the target site's login seeded manually
  • The profile ID from GET /api/v1/user/list

Step-by-step

  1. Seed the login once, by hand

    Open the profile in AliasMode, log into the site, and close the profile. The session now lives in the profile's user-data directory — the durable copy.

  2. Get the profile ID

    List profiles and record the ID of the profile you just seeded.

    curl 'http://127.0.0.1:50400/api/v1/user/list'
  3. Start the profile from the script

    Start the browser and capture the CDP websocket endpoint AliasMode returns.

    const origin = 'http://127.0.0.1:50400';
    const res = await fetch(`${origin}/api/v1/browser/start?user_id=${profileId}`);
    const payload = await res.json();
    if (payload.code !== 0) throw new Error(payload.msg);
    const cdp = payload.data.ws.puppeteer;
  4. Connect over CDP — do not launch

    Connect to the running browser instead of launching your own. The profile's cookies, storage, and fingerprint are already live.

    import { chromium } from 'playwright';
    const browser = await chromium.connectOverCDP(cdp);
    const context = browser.contexts()[0];
    const page = context.pages()[0] ?? await context.newPage();
    await page.goto('https://example.com/dashboard'); // already logged in
  5. Assert the session, not the login

    Treat the logged-in state as an assertion, not a setup step. If the session died, fail loudly and let a human re-seed it.

    const loggedIn = await page.locator('[data-testid="account-menu"]').isVisible();
    if (!loggedIn) throw new Error('Session expired — re-seed the AliasMode profile');
  6. Stop the profile in finally

    Always release the session so the profile stays consistent for the next run.

    try {
      // ... test or scrape work ...
    } finally {
      await browser.close();
      await fetch(`${origin}/api/v1/browser/stop?user_id=${profileId}`);
    }
  7. Compare with storageState honestly

    storageState is fine for short-lived fixtures. When a site rotates sessions weekly, the profile wins: the cookie jar stays warm without a refresh pipeline.

  8. CI caveat

    In CI, keep AliasMode and the profiles on a long-lived Windows runner or self-hosted agent; ephemeral containers cannot hold persistent sessions.

The AliasMode workflow

  1. Profiles replace storageState

    The profile's user-data directory is the durable session store; your repo holds no credentials and no cookie files.

  2. Local API as the lifecycle hook

    start and stop bracket every run; the API contract is AdsPower-shaped, so existing tooling maps over.

  3. Fingerprint stability

    The deterministic seed means the session always presents the same device, week after week.

  4. Group by project

    Keep test identities in their own group so CI and local runs share one source of truth.

Verify it worked

  • The first page.goto lands on the logged-in dashboard with zero login code in the script.
  • Killing the script mid-run still stops the profile via the finally block.
  • Consecutive runs reuse the same session without new cookies warnings.
  • GET /api/v1/browser/stop returns success and no chromium process lingers.

Cautions

  • Only automate sites in ways their terms allow; a logged-in scripted session is still automation.
  • Never print cookies or tokens from the CDP connection into logs.
  • Two runs connecting to one profile at once will interleave state — serialize per profile.

The login-state run loop

  1. Resolve

    Profile ID from GET /api/v1/user/list, or an env var in CI.

  2. Start

    GET /api/v1/browser/start?user_id=... returns data.ws.puppeteer.

  3. Connect

    chromium.connectOverCDP(cdp) — the session is already live.

  4. Release

    browser.close(), then GET /api/v1/browser/stop?user_id=... in finally.

Login-state strategies compared

Ways to get Playwright logged in
StrategyDurabilityBest for
Login flow in testNone — re-runs the risk every timeTesting the login itself
storageState JSONExpires with the sessionShort-lived fixtures
AliasMode profile over CDPPersistent, self-healing sessionsLong-running scrapers and monitors

Playwright FAQ

Do I need Puppeteer instead for CDP?

No. Playwright's chromium.connectOverCDP supports CDP endpoints directly; contexts() and pages() work as usual.

What if the site rotates cookies mid-run?

Fine — the profile's cookie jar updates live in the user-data directory, so the next run inherits the rotation.

Can I run several tests against one profile in parallel?

Use separate browser contexts per test if they must be isolated, but serialize the profile itself: one connection at a time.

Sources and verification

  • Playwright · Browser.connectOverCDP API reference (checked September 2026)
  • Playwright · Authentication and storageState guide (checked September 2026)
  • Chrome DevTools Protocol · DevTools Protocol documentation (checked September 2026)

Public product details can change after the check date. Facts are re-checked on a monthly cycle.