Steel Node SDK: Cloud browsers in 3 lines of code. Works with Puppeteer, Playwright. No Docker hell. TypeScript-first. Free 100hrs/mo.
Steel Node SDK: Because Your Puppeteer Scripts Deserve Better Than localhost Your Puppeteer script works flawlessly on your MacBook. Push to prod? Instant Cloudflare ban. Install headless Chrome on your server? Welcome to dependency hell. Steel gives you cloud browsers that just work; in Node, Deno, Bun, or wherever JavaScript runs these days.
When localhost:9222 Isn't Enough Anymore
Picture this: It's 3am. Your e-commerce scraper is down. Again. The Docker container crashed because Chrome ate all the RAM. Your proxy provider ghosted you. The site you're scraping now requires you to solve a picture puzzle of traffic lights.
You open GitHub and see 47 open issues on puppeteer-extra-plugin-stealth. The last commit was 8 months ago.
There has to be a better way.
Enter Steel: Cloud browsers with a proper API. No containers. No plugins. Just browsers that don't get detected.
Zero to Browser in 30 Seconds
npm install steel-sdkNo post-install scripts downloading 300MB of Chromium. No "please install these fonts for emoji support." Just a lightweight SDK with minimal dependencies.
Here's your first cloud browser:
import Steel from 'steel-sdk';
async function main() {
try {
const steel = new Steel(); // reads STEEL_API_KEY from env
const session = await steel.sessions.create();
console.log(`Session ID: ${session.id}`);
console.log(`Watch live: ${session.sessionViewerUrl}`);
console.log(`WebSocket: ${session.websocketUrl}`);
// That's it. You have a browser. In the cloud. Ready to connect.
// Always clean up
await steel.sessions.release(session.id);
} catch (error) {
console.error('Error:', error.message);
}
}
main();The TypeScript Experience We Deserve
Remember wrestling with Puppeteer's types? Page | null | undefined | ElementHandle<Element | null> | void?
Steel's SDK is TypeScript-first with types that actually help:
import Steel from 'steel-sdk';
import type { Session, SessionCreateParams } from 'steel-sdk';
// Every option, fully typed
const config: SessionCreateParams = {
dimensions: { // ✓ viewport config
width: 1920,
height: 1080
},
timeout: 600000, // ✓ session duration (10 min)
sessionContext: { // ✓ preload cookies & storage
cookies: [/* ... */],
localStorage: {/* ... */}
},
// Pro features (when you upgrade):
useProxy: true, // residential proxies
solveCaptcha: true, // beats reCAPTCHA
region: 'us-east' // geographic targeting
};
// Autocomplete everywhere
const session = await steel.sessions.create(config);
session.// ← Your IDE knows everythingNo more @ts-ignore. No more "I hope this property exists." Just types that work.
Puppeteer, But Make It Cloud Native
Your existing Puppeteer code? It just works:
import puppeteer from 'puppeteer-core'; // note: puppeteer-core, not puppeteer
import Steel from 'steel-sdk';
async function scrapeWithStyle() {
const steel = new Steel();
// Get a cloud browser with all protections
const session = await steel.sessions.create({
useProxy: true,
solveCaptcha: true
});
// Connect Puppeteer like nothing changed
const browser = await puppeteer.connect({
browserWSEndpoint: session.websocketUrl
});
// Your code runs exactly the same
const page = await browser.newPage();
await page.goto('https://bot-protected-site.com');
// Except now it doesn't get blocked 🎉
const data = await page.evaluate(() => {
return document.querySelector('.price')?.textContent;
});
// Always clean up
await steel.sessions.release(session.id);
return data;
}Same API. Better infrastructure. Zero learning curve.
Works With Your Entire Stack
Playwright? Obviously.
import { chromium } from 'playwright';
import Steel from 'steel-sdk';
async function usePlaywright() {
const steel = new Steel();
const session = await steel.sessions.create();
try {
const browser = await chromium.connectOverCDP(session.websocketUrl);
const context = await browser.newContext();
const page = await context.newPage();
// Now use Playwright as normal
await page.goto('https://example.com');
const title = await page.title();
console.log(`Title: ${title}`);
await browser.close();
} finally {
await steel.sessions.release(session.id);
}
}Running on Vercel Edge? Cloudflare Workers? No problem.
// edge-function.ts
import Steel from 'steel-sdk';
export default async function handler(req: Request) {
const steel = new Steel({
steelAPIKey: process.env.STEEL_API_KEY
});
const session = await steel.sessions.create();
// Steel handles the WebSocket → HTTP bridge automatically
return Response.json({
sessionId: session.id,
viewerUrl: session.sessionViewerUrl
});
}Bun? Deno? We don't discriminate.
// Works in Bun
import Steel from 'steel-sdk';
// Works in Deno
import Steel from 'npm:steel-sdk';
// Same API, same features, everywhereSession State That Persists (Like a Real Browser)
Remember implementing your own cookie jar? Managing localStorage across requests? Rebuilding browser state from scratch?
Steel sessions are stateful by default:
// Create a session with existing context
const session = await steel.sessions.create({
sessionContext: {
cookies: savedCookies,
localStorage: {
'example.com': {
'auth_token': 'abc123',
'user_prefs': JSON.stringify({ theme: 'dark' })
}
}
}
});
// ... do your thing ...
// Save the context for next time
const context = await steel.sessions.context(session.id);
await saveToDatabase({
cookies: context.cookies,
localStorage: context.localStorage,
sessionStorage: context.sessionStorage
});Sessions last up to 24 hours. Perfect for long-running automations, multi-step workflows, or just keeping that login alive.
When Your Automation Needs Files
Upload CSVs, download reports, handle PDFs, without the fs.writeFileSync dance:
// Upload a file to the session
const csv = await steel.sessions.files.upload(session.id, {
file: Buffer.from('name,email\njohn,john@example.com'),
path: 'data/users.csv'
});
// Your browser can now access it at file:///data/users.csv
await page.goto('file:///data/users.csv');
// Download files the browser created
const screenshot = await page.screenshot();
await steel.sessions.files.upload(session.id, {
file: screenshot,
path: 'screenshots/homepage.png'
});
// Get everything as a zip
const archive = await steel.sessions.files.downloadArchive(session.id);No temp directories. No cleanup scripts. Just files that work.
Error Handling for the Real World
Because UnhandledPromiseRejectionWarning: Error: Protocol error (Target.createTarget): Target closed isn't helpful:
import Steel from 'steel-sdk';
class ResilientScraper {
private steel: Steel;
constructor() {
this.steel = new Steel();
}
async scrapeWithRetry(url: string, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const session = await this.createSessionWithBackoff(attempt);
try {
const result = await this.performScrape(session, url);
return result;
} catch (error) {
// Actual useful error types
if (error instanceof Steel.RateLimitError) {
console.log(`Rate limited. Waiting ${error.headers['retry-after']}s`);
await this.wait(parseInt(error.headers['retry-after']) * 1000);
} else if (error instanceof Steel.APIConnectionTimeoutError) {
console.log(`Timeout on attempt ${attempt}, retrying...`);
} else if (error instanceof Steel.BadRequestError) {
// Don't retry invalid requests
throw new Error(`Invalid request: ${error.message}`);
}
} finally {
// Always cleanup
await this.steel.sessions.release(session.id).catch(() => {});
}
}
throw new Error(`Failed after ${maxAttempts} attempts`);
}
private async createSessionWithBackoff(attempt: number) {
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000);
if (attempt > 1) await this.wait(delay);
return this.steel.sessions.create({
useProxy: true,
solveCaptcha: true
});
}
private wait = (ms: number) => new Promise(r => setTimeout(r, ms));
}Real error types. Real retry strategies. Real production code.
The Features That Make You Go "Finally!"
Geographic Precision
const session = await steel.sessions.create({
useProxy: {
geolocation: {
country: 'US',
state: 'NY',
city: 'NEW_YORK'
}
}
});
// Your browser is now a New Yorker 🗽Human-Like Behavior
const session = await steel.sessions.create({
stealthConfig: {
humanizeInteractions: true // Mouse movements that don't scream "BOT"
}
});Auto-CAPTCHA Solving
// Encounters reCAPTCHA? No problem.
const session = await steel.sessions.create({
solveCaptcha: true
});
// Check CAPTCHA status
const status = await steel.sessions.captchas.status(session.id);Session Inspection
// Watch your automation live (seriously, try this)
console.log(`Debug live: ${session.sessionViewerUrl}`);
// Get all session events for playback
const events = await steel.sessions.events(session.id);Pricing That Doesn't Require a VC Round
Hobby (Free): 100 browser hours/month
- Perfect for side projects
- Basic anti-detection
- Community Discord access
Pro: Pay for what you use
- Residential proxies
- CAPTCHA solving
- Geographic targeting
- Priority support
Enterprise: Let's talk
- Custom proxy pools
- Dedicated infrastructure
- SLAs that mean something
- A Slack channel where we actually respond
Migration Guide: From Localhost to Cloud
Got an existing Puppeteer project? Here's your 5-minute migration:
Before (Your Current Setup)
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
// ... your automation ...
await browser.close();After (With Steel)
import puppeteer from 'puppeteer-core';
import Steel from 'steel-sdk';
const steel = new Steel();
const session = await steel.sessions.create();
const browser = await puppeteer.connect({
browserWSEndpoint: session.websocketUrl
});
const page = await browser.newPage();
// ... your automation (unchanged!) ...
await steel.sessions.release(session.id);That's it. Your code stays the same. Your success rate goes through the roof.
Join our Discord where people actually help each other instead of just posting memes (okay, we post memes too).
Let's Ship Something
# Install
npm install steel-sdk puppeteer-core dotenv
# Set your API key (get one at app.steel.dev)
echo "STEEL_API_KEY=your_api_key_here" > .env
# Run this quickstart
npx tsx quickstart.ts// quickstart.ts
import 'dotenv/config';
import Steel from 'steel-sdk';
import puppeteer from 'puppeteer-core';
const steel = new Steel();
async function main() {
let session;
try {
// Create a session
session = await steel.sessions.create();
console.log(`
🚀 Session ready!
📺 Watch live: ${session.sessionViewerUrl}
🔗 Session ID: ${session.id}
`);
// Connect and scrape
const browser = await puppeteer.connect({
browserWSEndpoint: session.websocketUrl
});
const page = await browser.newPage();
await page.goto('https://example.com');
const title = await page.title();
console.log(`\n✅ Scraped title: ${title}`);
await browser.close();
} catch (error) {
console.error('Error:', error);
process.exit(1);
} finally {
// Always cleanup
if (session) {
await steel.sessions.release(session.id);
console.log('\n🧹 Session released. Happy scraping!');
}
}
}
main();Resources for the Curious
- GitHub - Star it, fork it, break it, fix it
- API Docs - Every endpoint, every option
- Steel Cookbook - Copy-paste-ship recipes
- System Status - We're transparent about uptime
The Part Where We Get Real
Browser automation is hard. It shouldn't be. You came here to build something cool, not to become a Docker expert or a proxy specialist.
Steel handles the infrastructure so you can focus on the fun part, building things that matter.
No more "works on my machine." No more random bans. No more 3am debugging sessions.
Just npm install steel-sdk and start shipping.
Ready? Grab your API key and let's build something the internet hasn't seen before.
P.S. - If you build something cool, tell us in Discord. We love seeing what you ship with Steel. Plus, we might feature it (with your permission) and send you swag.