Skip to main content
Browserbase
Browserbase gives an AI agent a real cloud browser: create a session, connect over CDP, and drive it — usually with Stagehand, Browserbase’s automation framework whose act / extract / observe verbs let you script the page in natural language instead of brittle selectors. It does that job well, and the API is mature. But it is also a separate vendor account:
  • The session lives behind a BROWSERBASE_API_KEY + project ID, its own console, and its own billing — disconnected from wherever the agent’s email, cards, secrets, and KYC live.
  • The interesting workflows — sign this agent up for a SaaS, then log it back in later — are yours to build: you generate the password, you decide where to store it, and you own the risk of it leaking. Browserbase persists cookies via Contexts; it does not vault the credential itself.
  • “Who let this agent create an account, and what else can it touch?” is answered in Browserbase for browser sessions, and in unrelated systems for everything else. The session has no shared accountability with the rest of the agent’s footprint.
Naive’s browser primitive gives the agent the same capability — a cloud browser it drives with navigate / act / extract / observe — but the session is rooted in one governed identity:
  • The tenant user that owns the session is the same user that owns its email inboxes, its cards, its vault secrets, and its KYC.
  • Autonomous signup generates a strong password, fills the form, and vaults { email, password } server-side under that identity — the password never reaches your code or the model. A later login reads it back the same way.
  • Whether an agent may browse at all — which domains, whether it can submit forms, whether creating an account freezes for human approval — is decided by that user’s Account Kit and the session’s allowlist at execution time.
This guide maps Browserbase + Stagehand to Naive’s browser primitive, shows the smallest working swap, and is explicit about what does not map yet.
Browserbase and Stagehand are trademarks of their respective owners, used here for identification only. No endorsement or affiliation is implied.
Tested against: the Browserbase Node SDK @browserbasehq/sdk v2.x (bb.sessions.create, bb.contexts.create, base https://api.browserbase.com) and Stagehand @browserbasehq/stagehand v3 (stagehand.act / extract / observe on the instance; docs snapshot June 2026), and the Naive Node SDK @usenaive-sdk/server against the Naive API (base https://api.usenaive.ai/v1, docs snapshot June 2026).Version notes:
  • Stagehand v3 moved act / extract / observe from the page object to the stagehand instance and unified model config into a single model option. v2 called page.act({ action }). This guide uses the v3 shape.
  • Browserbase separates concerns: the SDK opens the session (sessions.createconnectUrl), you connect with Playwright/Puppeteer over CDP, and Stagehand layers the NL verbs on top. Naive folds open + drive + autonomous signup/login into one per-user primitive with no CDP/Playwright wiring.
  • Browserbase identifies a session by its id and persists login state via the Contexts API. Naive returns a session_id, enforces a domain allowlist (default-deny), and persists logins as human-granted saved contexts — an agent can use one but generally cannot create the grant itself.
  • Naive sessions have a hard TTL (default 15 min, max 30) and a per-session credit ceiling of 40. Browserbase supports much longer sessions (up to 6h) and keepAlive — see what doesn’t map yet.

Concept map

Browserbase / StagehandNaiveNotes
new Browserbase({ apiKey }) + new Stagehand({ env, apiKey, model })new Naive({ apiKey })Server-side credential in both cases
BROWSERBASE_API_KEY + project ID — one Browserbase account, isolated from your other vendorstenant user via naive.forUser(id) — scopes every primitiveThe core consolidation win
Project as the unit of grouping (browser sessions only)Tenant users (naive.forUser(id))Naive’s unit of isolation spans every primitive, not just the browser
bb.sessions.create({ projectId, browserSettings })connectUrlclient.browser.createSession({ allowed_domains, allow_writes, timeout })session_idNaive sessions are domain-allowlisted (default-deny) and per-user
chromium.connectOverCDP(session.connectUrl) + Playwright page(none)No CDP/Playwright wiring — you call verbs directly on the session_id
await stagehand.init() (launches the model + browser)(folded into createSession)No separate model key — Naive runs the automation model server-side
page.goto(url)client.browser.navigate(sessionId, url)Allowlist + SSRF denylist enforced on every navigation
stagehand.act("click …")client.browser.act(sessionId, "click …")Natural-language action; submits/destructive actions need allow_writes
stagehand.extract(instruction, zodSchema)client.browser.extract(sessionId, instruction)Read-only; no schema arg — see gaps
stagehand.observe("find …")client.browser.observe(sessionId, "find …")List candidate elements/actions (read-only)
page.screenshot()client.browser.screenshot(sessionId)Short-lived signed URL
stagehand.close() / session timeoutclient.browser.closeSession(sessionId)Always close — idle sessions bill a vendor time floor at close
bb.sessions.list()client.browser.listSessions()Sessions for the identity
Build it yourself: act + your own password + your own secret storeclient.browser.signup({ service, url })Generates a strong password, fills the form, vaults the credential server-side
Re-use a Context to land logged-inclient.browser.login({ service, url })Reads the vaulted credential and re-authenticates — password never returned
act("…", { variables: { password } }) — secret substituted, but you supply + store it%password% substituted server-side — you never hold itSecret never crosses the agent/LLM boundary in either case; ownership differs
bb.contexts.create() + browserSettings.context: { id, persist }Saved logins: --human-logincontext savecontext grantPersistence is human-granted, default-deny (an agent can use, never grant)
Manual login via Session Live View, then persist contextDashboard Browser tab → Live View → Take controlOne-time human login for SSO / 2FA / CAPTCHA
bb.sessions.debug(id) → live/debugger URLDashboard Live View (watch + take control)Human watch/intervene
proxies: true / geolocation--proxy residential proxy (requires an allowlist)
API-key scopes / project separationAccount Kit browser enabled + per-kit requiresApproval for signupPermission is execution-time policy on the identity — see gains
browserSettings.solveCaptchasNo automatic CAPTCHA solving — a human takes control in Live View
stagehand.agent({...}).execute(...) (CUA multi-step loop)No built-in autonomous multi-step agent — see gaps
Bring-your-own model / per-call model choiceAutomation model is server-side; not selectable
Session recording / replay, downloads, file uploads, extensionsNot provided — see gaps
timeout up to 6h, keepAliveHard TTL 15 min (max 30), ceiling 40 credits/sessionRe-open sessions; persist with saved logins

Before / after: the core path

The path that matters for almost every browsing agent is open a session, go somewhere, do something, read something back. Here it is on both platforms.
import { Stagehand } from "@browserbasehq/stagehand";
import { z } from "zod";

// Browserbase account key + a separate LLM key for Stagehand
const stagehand = new Stagehand({
  env: "BROWSERBASE",
  apiKey: process.env.BROWSERBASE_API_KEY!,
  projectId: process.env.BROWSERBASE_PROJECT_ID!,
  model: "openai/gpt-4.1-mini", // your own model credential
});

await stagehand.init();                       // opens the cloud session
const page = stagehand.context.pages()[0];    // a Playwright page over CDP

await page.goto("https://news.ycombinator.com");
await stagehand.act("click the first story link");

const data = await stagehand.extract(
  "the article title and author",
  z.object({ title: z.string(), author: z.string() }),
);

await stagehand.close();
// → the session is an island behind your Browserbase project + a separate model key.
import { Naive } from "@usenaive-sdk/server";

const naive = new Naive({ apiKey: process.env.NAIVE_API_KEY! });
const client = naive.forUser("acme"); // same id space as your own users

// One call opens a domain-scoped, per-user session. No CDP, no model key.
const session = await client.browser.createSession({
  allowed_domains: ["news.ycombinator.com"],
});

await client.browser.navigate(session.session_id, "https://news.ycombinator.com");
await client.browser.act(session.session_id, "click the first story link");

const data = await client.browser.extract(
  session.session_id,
  "the article title and author",
);

await client.browser.closeSession(session.session_id);
// → the SAME `client` also owns this user's email, cards, vault, and KYC.
The drive verbs line up closely. The real differences to plan for:
  • One handle, no plumbing. Browserbase is create session → connect over CDP → wrap with Stagehand → bring your own model key. Naive’s createSession returns a session_id you drive directly — no Playwright, no CDP, no separate model credential.
  • Sessions are allowlisted by default. Every Naive session must pass allowed_domains (default-deny); navigation also runs through an SSRF denylist. Pass ['*'] to browse unrestricted (not recommended).
  • Writes are gated. A destructive/submit act is refused unless the session was opened with allow_writes: true. Reading and clicking links work without it.
  • extract is instruction-only. Naive returns extracted data from a natural-language instruction; it does not take a Zod/JSON schema. Validate the shape in your own code — see gaps.
  • The id is your identity, not a separate account. In Browserbase the API key scopes Browserbase. In Naive the same forUser(id) handle also owns the agent’s email, cards, vault, and KYC.

The workflow that actually changes: signup & login

This is where the migration earns its keep. On Browserbase you build account creation: generate a password, drive the form, and decide where to keep the secret. Stagehand can substitute the value so it never hits the model — but you still own it. Naive turns the whole flow into one call that vaults the credential under the user’s identity.
import crypto from "node:crypto";

const email = "acme-agent@yourdomain.com";
const password = crypto.randomBytes(18).toString("base64url"); // you generate it

await page.goto("https://app.example.com/signup");
// `variables` keeps the value out of the LLM prompt — good — but it is still yours:
await stagehand.act("fill the email field with %email%",       { variables: { email } });
await stagehand.act("fill the password field with %password%", { variables: { password } });
await stagehand.act("submit the signup form");

// Now YOU must store { email, password } somewhere and protect it,
// and YOU must wire any human approval before the account is created.
await myOwnSecretsManager.put("login:app.example.com", { email, password });
// Naive generates a strong password, fills the form, and stores { email, password }
// in THIS user's encrypted Vault under `login:app.example.com`. The password is not
// returned to your code or sent to the model.
const res = await client.browser.signup({
  service: "app.example.com",
  url: "https://app.example.com/signup",
});

// signup is sensitive, so it is approval-gated by default:
if (isPendingApproval(res)) {
  // returns HTTP 202 / status "pending_approval" — a human approves, then it runs.
}

// Later, from a brand-new session, re-authenticate without ever touching the password:
await client.browser.login({ service: "app.example.com", url: "https://app.example.com/login" });
  • Credential custody flips. Browserbase substitutes the secret at fill-time but leaves you holding it. Naive generates and vaults it under the tenant user — the agent and the model never see the password.
  • Verification emails land in the same identity. signup uses the user’s profile email as the account email. Point it at a provisioned inbox (client.profile.setEmail(...)) so confirmation links arrive in a mailbox the same user owns.
  • Account creation is approval-gated. Because signup creates a real account under the user’s identity, it freezes for human approval by default — toggle per Account Kit.

Minimal viable migration

The smallest swap that keeps a working browsing agent running is just open → drive → read → close.
1

Install the SDK and set your key

npm install @usenaive-sdk/server
Set NAIVE_API_KEY (a server-side key from the dashboard). You can drop BROWSERBASE_API_KEY, the project ID, and the separate model key for this path.
2

Swap session creation

Replace new Stagehand({...}) + stagehand.init() (or bb.sessions.create() + chromium.connectOverCDP(...)) with a single client.browser.createSession({ allowed_domains: [...] }). Keep the returned session_id. Add allow_writes: true only if the agent submits forms.
3

Swap the drive verbs

Map each call onto the session_id: page.gotoclient.browser.navigate(sessionId, url), stagehand.actclient.browser.act(sessionId, instruction), stagehand.extractclient.browser.extract(sessionId, instruction) (drop the schema), stagehand.observeclient.browser.observe(sessionId, instruction), page.screenshotclient.browser.screenshot(sessionId).
4

Swap teardown

Replace stagehand.close() with client.browser.closeSession(sessionId). Always close — an idle session bills a vendor time floor at close (1 credit ≤5 min, 2 ≤15 min, 3 ≤30 min).
5

Ship it

At this point you are off Browserbase for the core open/drive/read path. Everything below is upside, not a requirement.

Consolidate further once you’re on Naive

This is where the migration pays for itself. In Browserbase, the API key isolates browser sessions and nothing else, and any account the agent creates is a credential you babysit. On Naive, the unit of isolation is a tenant user, and the browser is one of many primitives that identity owns.
// One project (or key) per customer isolates browser sessions — and only those.
const stagehand = new Stagehand({
  env: "BROWSERBASE",
  apiKey: process.env.BROWSERBASE_API_KEY!,
  projectId: customerProjectId,
  model: "openai/gpt-4.1-mini",
});
await stagehand.init();
// ...drive the browser...

// The card, the vault secrets, the email inbox, and the KYC for this customer's
// agent live in entirely separate systems with their own tenancy — and the SaaS
// password the agent just created lives in whatever store you wired up.
// One tenant user per customer isolates *every* primitive.
const acme = await naive.users.create({
  external_id: dbCustomer.id,
  email: dbCustomer.email,
});

const client = naive.forUser(acme.id);

// Browse, then create + vault an account, all under one identity:
const session = await client.browser.createSession({ allowed_domains: ["app.example.com"] });
await client.browser.signup({ service: "app.example.com", url: "https://app.example.com/signup" });

// The SAME client owns this customer's email inbox, card, vault, and KYC —
// one identity, isolated end to end, and the SaaS credential lives in their vault.
await client.email.createInbox({ local_part: "agent" });
await client.cards.create({ spending_limit_cents: 25_000 });
const creds = await client.vault.reveal("login:app.example.com"); // server-side; not returned to the model

Gain #1 — one identity across primitives

  • With Browserbase, the agent’s sessions are an island behind an API key, and any account it creates is a secret you store and protect yourself. With Naive, naive.forUser(acme.id) is a single handle to browser and email and cards and vault and KYC.
  • The SaaS accounts the agent signs up for are not loose credentials — they are vaulted under the identity that owns everything else. Tear the customer down from one place; sibling tenants can never read each other’s sessions, logins, cards, or secrets.

Gain #2 — execution-time permission enforcement

  • Whether an agent may browse at all, which domains it may reach, whether it may submit forms, and whether creating an account freezes for human review are all policy on the identity — not checks you hand-write and not API-key scopes you manage in a second console.
// A starter tier: browsing allowed, but new-account signup must be approved by a human.
const starter = await naive.accountKits.create({
  name: "Starter",
  primitives_config: {
    browser: { enabled: true, requiresApproval: true }, // signup freezes for approval
    cards: { enabled: false },                           // no card for this tier
  },
});
await naive.accountKits.assignUser(starter.id, acme.id);
  • The agent’s code is identical for every tier — client.browser.act(...), client.browser.signup(...). Whether the call runs is decided at execution time:
    • A session must pass its allowed_domains allowlist (default-deny) and an SSRF denylist — no code change on your side.
    • A destructive act without allow_writes is refused.
    • With browser.requiresApproval: true, an autonomous signup returns pending_approval and runs only after a human approves it. Driving the page is a routine action and is not approval-gated.
    • Saved logins are human-granted. An agent can use a saved login but generally cannot create the grant or revoke a shared one — that lives in the dashboard.

Gain #3 — unified accountability

  • Every session, navigation, action, and autonomous signup for a customer lands in one per-user activity log — alongside their email, card, vault, and KYC events, not in a separate Browserbase console:
const { events } = await naive.forUser(acme.id).logs.query({ limit: 50 });
// "what did this agent do, for whom?" — browser, email, cards, vault, one timeline
  • That is the question that is hard to answer when browsing lives in Browserbase, the SaaS password lives in your secrets manager, cards live in Stripe, and email lives somewhere else. Under Naive it is a single query.

What does not map yet

A migration guide that hides gaps is worse than none. The core path (open → navigate → act → extract → close) and the signup/login flow map cleanly, but the following Browserbase / Stagehand capabilities have no direct equivalent on Naive’s browser primitive today. Check this list against your app before you commit.
Browserbase / Stagehand featureStatus on NaiveWorkaround
Raw CDP / Playwright / Puppeteer access (connectUrl)Not provided — driven via SDK verbs onlyUse navigate / act / extract / observe; no arbitrary Playwright calls
Stagehand agent() autonomous multi-step loop (Computer-Use Agent)Not providedOrchestrate the verbs yourself, or drive them from orchestration
Bring-your-own model / per-call model choiceNot exposed — the automation model runs server-sideNo model selection in the browser verbs
extract with a Zod/JSON schemaInstruction-only extractParse + validate the returned data in your own code
Auto CAPTCHA solving (solveCaptchas)Not automaticA human clicks Take control in the dashboard Live View
Session recording / replay, downloads, file uploads, browser extensionsNot providedScreenshot per step; no replay / upload / extension APIs
Long-lived sessions (timeout up to 6h, keepAlive)Hard TTL 15 min (max 30), 40-credit ceiling/sessionRe-open sessions; persist state via saved logins
Self-managed Contexts (create/persist arbitrary cookie state)Saved logins are human-granted, default-denyA human opens a --human-login session, then context save + context grant
Custom fingerprints / verified OS spoofing / viewport / regionsNot exposedResidential --proxy only; limited fingerprint control
userMetadata and other session configNot exposedTrack your own metadata against the session_id
If your agent needs raw Playwright/CDP control, a built-in autonomous (CUA) agent loop, schema-validated extraction, model choice per call, auto CAPTCHA solving, session replay/uploads/downloads, or multi-hour sessions, those are the gaps most likely to matter — Naive’s browser is a per-user, domain-allowlisted session driven by NL verbs, with autonomous signup/login backed by the Vault. The flip side is the gain: the session, its saved logins, and every credential it creates are governed by the same identity and Account Kit as the rest of the agent — not a standalone account whose secrets you babysit.

Where to go next

  • browser primitive — full session lifecycle, autonomous signup/login, saved logins, live view
  • browser SDK sub-client — typed method signatures
  • browser CLI — session create/navigate/act/extract, signup/login, context save/grant
  • Vault — where autonomous signup stores login:<service> credentials
  • Account Kits and Approvals — the policy model behind execution-time governance and signup approval
  • Email and Profile — the inbox that receives the agent’s account-verification mail
  • Tenant users — the identity that owns the browser, email, cards, and vault