Skip to main content
The Node package (@heretic-tech/apostate on npm) finds or downloads the browser, looks up the proxy’s exit, and starts the browser through Patchright, a Playwright build. It returns the driver’s own objects, so the rest of your script is ordinary Playwright or Puppeteer code. The package is an ES module, loaded with import, and needs Node 22 or later. Installation covers npm install @heretic-tech/apostate and the browser download.

Launch a browser

fingerprint is the seed. The same seed and persona give the same machine on every launch. fingerprintPlatform is the persona, the operating system the browser presents. Personas and Seeds and identity explain both. Under Patchright or Playwright, launch() returns Playwright’s Browser. The browser runs on a temporary normal profile, which is deleted when the browser closes. newPage() opens pages in that profile. The first call returns the blank tab the browser started with.
newContext() opens an off-the-record context, as it does in Playwright, and sites can tell an off-the-record context from a normal profile. Open pages with newPage().
newPage() takes no options, because every page shares the one profile.

Entry points

The first launchPersistentContext() with a directory stores a seed in DIR/apostate/identity, and every later launch with that directory presents the same machine. launch() refuses userDataDir.

Options

Proxies, Locale and timezone, Custom profiles and Linux servers cover the options in use. Node API lists every export and option, and Switches lists what args accepts.

Driver options

Three options go through to the driver:
  • env goes over the browser’s environment, which the package gives LANGUAGE, LC_ALL, LC_MESSAGES, LANG and TZ (Locale and timezone). Your entries win.
  • ignoreDefaultArgs removes switches the driver adds by default. The package adds --disable-component-update to the list, so the driver does not pass that switch (Widevine).
  • defaultViewport goes to Puppeteer. The package passes null unless you set it.
The package passes no other driver option of yours. Under Playwright drivers it turns the viewport off, so the page gets the persona’s real window size. To set a viewport on one page, call page.setViewportSize(). From 0.4.4 it also turns off colour-scheme emulation, so a page reads the persona’s light or dark theme. With 0.4.3, call await page.emulateMedia({ colorScheme: null }) on each page (Known gaps). Screen and window has what a viewport of your own changes.

Diagnostics

The object launch() or launchPersistentContext() returns carries three extra properties. A context from launchContext() carries its browser as context.apostateBrowser, which has them. For the launch at the top of this page, browser.apostateDiagnostics is:
geoip is resolved when the lookup answered, unresolved when it failed, disabled with geoip: false, and explicit when no lookup was needed because you passed locale and timezone and no proxy. proxy is the proxy URL without its credential. warnings lists the launch’s warnings:
  • A launch with no seed warns that its machine changes on every launch.
  • A profile you wrote warns that nothing checks its coherence.
  • A failed GeoIP lookup warns that the locale or timezone was not set. GeoIP warnings are also printed with console.warn, prefixed [Apostate].
To see the whole machine a launch presents, run --fingerprint-explain (Verify):

Drivers

Patchright is installed with the package and is the default. The package also drives playwright, playwright-core, puppeteer and puppeteer-core. It uses the first one installed, in that order, or the one you name with driver. None of them needs to download a browser. driverInfo() reports what is installed and which driver a launch would use:
Detection has what a page can see of each driver.

Puppeteer

Puppeteer’s newPage() already opens pages in the browser’s normal profile. Two things Puppeteer does are visible to a page, measured with puppeteer-core 25.12.0:
  • A stack trace from code you pass to page.evaluate() names your script’s absolute path, percent-encoded after pptr:evaluate;file. Under Patchright the same trace names no file.
  • page.exposeFunction("hello", ...) also installs a global named puppeteer_hello.
Both come from the driver, and the browser cannot remove them. Use Patchright when a page may look for them.

launchProcess

launchProcess() starts the browser with no driver attached. It takes the same options as launch(), applies the persona, proxy and GeoIP the same way, starts Xvfb for a headed launch on a Linux host with no display, and returns an ApostateProcess. Connect any client to it, for example over the DevTools protocol:
userDataDir works here as an option. stdio and cwd go to Node’s spawn(), and stdio defaults to "ignore". From 0.4.4, launchProcess() passes --no-first-run and --no-default-browser-check. With 0.4.3, add both to args, or a headed first launch of a new user data directory can stop at a first-run dialog and never open its DevTools port. Keep --remote-debugging-port on 127.0.0.1, its default. Any program that reaches the port can drive the browser. The browser closes DevTools requests that web pages send (Detection). Raw binary covers other ways to connect.

Host mode

fingerprint: "host" presents the host’s own values and composes nothing. "off", "false", "0", "disable" and "disabled" mean the same. Use it to tell whether a problem comes from the persona or from the host and network. Host mode refuses fingerprintPlatform, and the package sets no locale variables. Headless host mode reports an 800x600 screen (Screen and window).

Close the browser

await browser.close() closes the browser, deletes its temporary profile, and stops any Xvfb display the package started. Closing a context from launchContext() closes its browser. The returned objects also work with await using, tested on Node 26:

Errors

Package errors are subclasses of ApostateError and carry a code and a details object. A malformed option value, such as a proxy URL that does not parse, throws a TypeError instead. From 0.4.4, a timezone that is not an IANA zone throws a RangeError.
A failed GeoIP lookup does not throw. It adds a warning and the launch continues. Errors lists the common messages and what to do about each.