Obscura Architecture — 8 Crates, Single V8 Isolate, and CDP Dispatch
Deep dive into Obscura's architecture: 8 crate layer design, single V8 Isolate, DOM tree implementation, CDP dispatch, and lifecycle management.
16Yun Engineering TeamJul 5, 20262 min read
From Command to Bytes
When Puppeteer calls page.goto('https://example.com'), what happens inside Obscura? This article traces the full path through 8 crates.
The 8 Crates
obscura-cli CLI entry: fetch / serve / scrape / mcp
obscura-cdp CDP WebSocket: routing, 12 domain handlers
obscura-browser Page type: navigation, lifecycle, context
obscura-js V8 runtime: deno_core, bootstrap.js, Rust ops
obscura-dom DOM tree: html5ever parsing, selectors, serialization
obscura-net HTTP: reqwest, wreq stealth client, cookie jar, tracker blocklist
obscura-mcp MCP protocol server: 35 tools
obscura Embeddable library API: Browser / Page / Element / CookieStore
```bash
## Request Flow
A `Page.navigate` from a CDP client:
```bash
CDP Client (Puppeteer)
│ WebSocket frame
▼
obscura-cdp/server.rs accept, route by sessionId
│
▼
obscura-cdp/dispatch.rs method router, acquires v8_lock
│
▼
obscura-cdp/domains/page.rs Page.navigate handler
│
▼
obscura-browser/page.rs navigate_with_wait
│
├──► obscura-net/client.rs HTTP fetch
│
├──► obscura-dom/tree.rs parse HTML into DOM tree
│
└──► obscura-js/runtime.rs run inline scripts
│
└──► bootstrap.js + ops.rs DOM bindings
```bash
The dispatcher emits CDP events (`Network.requestWillBeSent`, `Page.frameNavigated`, `Page.lifecycleEvent`) back through the same WebSocket.
## Single V8 Isolate
All pages share one V8 Isolate. This is Obscura's most important architectural decision.
```rust
let _guard = obscura_js::v8_lock::global().lock().await;
page.evaluate(expr).await;
```bash
The V8 Isolate is single-threaded by design. All JavaScript operations must acquire this global lock.
The dispatcher routes long-running operations through `process_with_interception`, which spawns them onto a tokio `LocalSet` so the dispatcher keeps handling other CDP messages.
## DOM Tree
Obscura uses `html5ever` for HTML parsing with a custom `tree_sink`.
```rust
// obscura-dom/src/tree.rs
struct Node {
id: NodeId,
parent: Option<NodeId>,
children: Vec<NodeId>,
node_type: NodeType, // Element / Text / Comment / Document
}
struct Tree {
nodes: Vec<Node>,
root: NodeId,
}
```bash
Key features:
- **Cycle guards**: `append_child` and `insert_before` reject inserting an ancestor. A cyclic reparent once made `descendants()` loop forever.
- **Selector engine**: Uses the `selectors` crate (Servo's CSS selector implementation).
- **descendants() cap**: Hard limit on tree traversal depth.
## CDP Dispatch
Session IDs are `"{targetId}-session"`. The dispatcher routes by `sessionId` to the correct `Page`.
`process_with_interception` extracts long-running operations and spawns them into independent tasks, freeing the dispatcher for other messages.
## Lifecycle State Machine
Managed by `obscura-browser/lifecycle.rs`:
```bash
init → commit → domcontentloaded → load → networkidle2 → networkidle0
```bash
## Storage Persistence
```bash
<storage-dir>/
├── cookies.json
└── localStorage/
├── <origin-1>.json
└── <origin-2>.json
```bash
## Stealth Integration
`--stealth` is a global CLI flag. In `scrape`, it is forwarded to workers via `OBSCURA_STEALTH`.
## Why 8 Crates
- **Single responsibility**: Each crate covers one capability domain
- **Compile caching**: Changing one crate does not rebuild V8
- **Test boundaries**: DOM tests do not need networking, CDP tests do not need V8
- **Optional features**: Stealth only affects `obscura-net`
## Summary
Obscura's architecture is a series of performance-constrained decisions: a single V8 Isolate avoids thread synchronization overhead, Rust directly calls V8's C API without a bridge layer, and the DOM tree lives in Rust without serialization overhead.Need an enterprise proxy plan?
We can tailor architecture to your target domains, concurrency, and reliability goals.