Playwriter is built on a clean, modular architecture that separates concerns and enables flexible deployment scenarios.
High-Level Design
┌────────────────────────────────────────────────────────────────┐
│ Your Application │
└─────────────────────────────┬──────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────┐
│ Playwriter │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Public API (Playwriter module) │ │
│ │ with_browser/2 fetch_html/2 screenshot/2 etc. │ │
│ └─────────────────────────────┬───────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────▼───────────────────────────┐ │
│ │ Browser Session (GenServer) │ │
│ │ Manages browser lifecycle and pages │ │
│ └─────────────────────────────┬───────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────▼───────────────────────────┐ │
│ │ Transport Layer │ │
│ │ ┌──────────────────┐ ┌────────────────────────────┐ │ │
│ │ │ Local Transport │ │ Remote Transport │ │ │
│ │ │ (playwright_ex) │ │ (WebSocket to Windows) │ │ │
│ │ └──────────────────┘ └────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘Core Components
1. Public API (Playwriter)
The main module provides a simple, composable interface:
with_browser/2- Execute a function with a managed browser sessionfetch_html/2- Convenience wrapper for fetching page contentscreenshot/2- Convenience wrapper for taking screenshots- Context operations -
goto/3,content/1,click/3,fill/4
2. Browser Session (Playwriter.Browser.Session)
A GenServer that manages the complete browser lifecycle:
- Starts and configures transports
- Launches browsers and creates contexts
- Manages pages and their frames
- Handles cleanup on termination
# Session state structure
%Playwriter.Browser.Session{
transport: pid(), # Transport process
transport_module: module(), # Local or Remote
browser_guid: String.t(), # Playwright browser ID
contexts: %{}, # Active browser contexts
pages: %{} # Active pages
}3. Transport Layer
The transport layer abstracts communication with Playwright:
Local Transport (Playwriter.Transport.Local)
- Wraps
playwright_exfor direct browser control - Uses Erlang Ports to communicate with Node.js
- Best for headless automation and local development
Remote Transport (Playwriter.Transport.Remote)
- Connects via WebSocket to a Playwright server
- Enables WSL-to-Windows browser visibility
- Supports distributed browser automation
Both transports implement Playwriter.Transport.Behaviour:
@callback start_link(keyword()) :: {:ok, pid()} | {:error, term()}
@callback launch_browser(transport(), browser_type(), keyword()) :: {:ok, guid()}
@callback new_context(transport(), guid(), keyword()) :: {:ok, guid()}
@callback new_page(transport(), guid()) :: {:ok, map()}
@callback goto(transport(), guid(), String.t(), keyword()) :: {:ok, map()}
@callback content(transport(), guid()) :: {:ok, String.t()}
# ... and more4. Server Discovery (Playwriter.Server.Discovery)
Automatically finds Playwright servers in WSL environments:
- Scans common ports (3337, 3336, 3335, etc.)
- Tries multiple host addresses (localhost, WSL gateway IP, etc.)
- Returns the first working endpoint
Data Flow
Local Mode
with_browser([])
│
▼
Session.start_link()
│
├──▶ Transport.Local.start_link()
│ │
│ └──▶ PlaywrightEx.Supervisor.start_link()
│ │
│ └──▶ Node.js Playwright Driver
│
├──▶ launch_browser(:chromium)
│ │
│ └──▶ PlaywrightEx.launch_browser()
│
└──▶ new_page()
│
└──▶ Browser visible (if headless: false)Remote Mode (WSL to Windows)
with_browser([mode: :remote])
│
▼
Session.start_link()
│
├──▶ Discovery.discover()
│ │
│ └──▶ Find ws://localhost:3337/
│
├──▶ Transport.Remote.start_link()
│ │
│ └──▶ WebSocket connect to Windows
│
└──▶ new_page()
│
└──▶ Browser visible on Windows desktop!Error Handling
Playwriter uses OTP patterns for robust error handling:
- Session supervision - Sessions are independent GenServers
- Transport isolation - Transport failures don't crash sessions
- Resource cleanup -
terminate/2ensures browsers are closed - Graceful degradation - Remote failures fall back cleanly
Extension Points
Custom Transports
Implement Playwriter.Transport.Behaviour for custom scenarios:
defmodule MyApp.CustomTransport do
@behaviour Playwriter.Transport.Behaviour
@impl true
def start_link(opts) do
# Your implementation
end
# ... implement other callbacks
endSession Callbacks
The Session GenServer can be extended:
# Get session state for debugging
:sys.get_state(session_pid)Performance Considerations
- Reuse sessions - Creating browsers is expensive
- Use headless mode - Faster than headed browsers
- Local transport - Lower latency than remote
- Connection pooling - Consider for high-throughput scenarios