"Control Your Real Chrome Browser with Playwright"
→ "Let Agents Control Your Real Chrome Browser"
Shifts the framing from Playwright (implementation detail)
to the core value prop: AI agents controlling your browser.
Session: ses_1015d49fcffebpkTVLdZmlqk9s
The Cloud Browsers docs page was hidden behind `"hidden": true`.
Removing the flag makes it visible in the navigation sidebar
so users can discover cloud browser features.
Session: ses_1015d49fcffebpkTVLdZmlqk9s
- title: "Control Your Real Chrome Browser with Playwright" (shows as
"Control Your Real Chrome Browser with Playwright — Playwriter" in
browser tab and Google results)
- sidebarTitle: "Home" (clean sidebar label instead of repeating the title)
Session: ses_1019526e6ffenre3Zso9o0s2dB
Add `playwriter browser install` to download Chrome for Testing from
Google into ~/.playwriter/browsers/. Supports mac-arm64, mac-x64,
linux64, win64. Skips if already installed. Retries downloads 3x
with backoff and 5-minute timeout.
Add `playwriter session new --browser headless` to launch a headless
Chrome via chromium.launch() and create a direct CDP session. No
extension needed, no user browser touched. Multiple headless sessions
share the same Chrome process (each gets its own context/page).
Key implementation details:
- browser-config.ts checks ~/.playwriter/browsers/ first when
resolving browser binaries (highest priority after explicit path
and PLAYWRITER_BROWSER_PATH env var)
- Shared headless browser uses a static field on PlaywrightExecutor
with a launch promise to deduplicate concurrent first-session races.
Single disconnect handler registered at launch time clears both
statics so next session relaunches after a crash.
- Session creation forces immediate connection so missing Chrome
errors surface at `session new` time, not on first execute call.
- Reset only closes the session context in headless mode, not the
shared browser (other sessions depend on it).
- Session delete closes the headless context to prevent leaks.
Relay shutdown closes the shared browser.
- `browser list` shows headless as an option when a Chrome binary
is available, alongside extension and cloud browsers.
- Zip extraction uses execFileSync with args array (not shell
interpolation) for safety. Uses unzip on macOS/Linux and
PowerShell Expand-Archive on Windows.
Session: ses_101fbc895ffeysfyjR2JE1su3L
Document that D1 queries are slow (especially writes) and must be
minimized. Use db.batch() to combine multiple operations into single
round trips. When adding new fields, piggyback updates onto existing
writes instead of adding separate D1 calls.
Session: ses_107161964ffefCyMno6mi5LtMr
Removes the separate D1 write for the rate limit timestamp by
piggybacking it onto the placeholder→real-ID update that already
happens. Both statements run in one db.batch() call, so connect
has the same number of D1 round trips as before the rate limit
was added.
Session: ses_107161964ffefCyMno6mi5LtMr
The lastCloudCreateAt update and bu.createBrowser() were running in
Promise.all. If createBrowser succeeded but the D1 update failed,
Promise.all rejected and vmResult was lost, orphaning a paid VM.
Now the D1 update runs first (sequentially), then createBrowser.
If the DB write fails, no VM is created. If VM creation fails,
the catch block has no vm.id to worry about and just cleans up
the placeholder row.
Session: ses_107161964ffefCyMno6mi5LtMr
- use row.slotIndex from DB instead of sequential counter in /api/cloud/status
so slots keep their real index after gaps (e.g. stopping slot 2 of 3)
- dashboard renders max(totalSlots, maxSessionIndex) rows so sessions
above subscription quantity are still visible after downgrades
Session: ses_10bc2a749ffeInm3HXNY1r5v6D
Empty title skips the h1 render; browser tab falls back to "Playwriter".
No new props needed: AuthPage conditionally renders h1 only when title is truthy.
Session: ses_105c733a9ffekXw867fixfqgKX
**CLI: `playwriter cloud live [key]`**
- prints and opens playwriter.dev/live?wss= URL for an active cloud session
- accepts cloud-N key, cloudSessionId, or browserUseSessionId
- auto-selects when only one session is active
**Dashboard: browser slots table**
- new CloudSessionsPanel shows all subscription slots (not just active)
- active sessions: green dot, duration, expiry countdown, "Watch live" link
- empty slots: gray "Available" status with dashes
- header shows active/total counter (e.g. "1/4 active")
- auto-refreshes every 10s, clears transient errors on success
**Replace Browser Use live URLs**
- all liveUrl fields now point to our /live?wss= route instead of live.browser-use.com
- uses exact CDP endpoint URL (not reconstructed from UUID) to handle BU sharding
- cloud-api.ts, cli.ts, cloud-client.ts all updated
- new buildLiveUrl() helper in cloud-client.ts (CLI) and cloud-api.ts (website)
Session: ses_10bc2a749ffeInm3HXNY1r5v6D
The /login route passed visualTitle={<PlaywriterLogo>} to AuthPage,
but AuthPage already renders the logo at the top. This caused the logo
to appear twice on the login screen.
- Remove the visualTitle prop from the /login route, use "Sign in" text title
- Remove unused visualTitle and headTitle props from AuthPage component
- No other pages had this issue (device flow and dashboard use text titles)
Session: ses_105c733a9ffekXw867fixfqgKX
Instead of combining browserCost + proxyCost into a single cloudSpend
counter, track them separately with independent budgets. This lets
operators set different limits for proxy bandwidth vs VM runtime.
Schema (org table):
- Restore proxySpendCents / proxyBudgetCents (proxy bandwidth)
- Add browserSpendCents / browserBudgetCents (VM runtime)
- Rename proxySpendPeriodStart → spendPeriodStart (shared period marker)
- Keep lastCloudCreateAt for rate limiting
Schema (cloudSession table):
- Restore lastProxyCostCents (proxy cost baseline)
- Add lastBrowserCostCents (browser cost baseline)
cloud-api.ts connect handler:
- Check both budgets independently before allowing new sessions
- Reset both counters on billing period rollover
scheduled.ts cron:
- Compute proxy and browser deltas separately per session
- Atomic increment both spend counters independently
- Kill sessions if either budget is exceeded
- Conditional baseline update uses WHERE on both previous values
Migration adds new columns alongside existing ones (no renames needed
since proxy columns were never dropped from D1).
Session: ses_107161964ffefCyMno6mi5LtMr
**Problem:** Several cost vulnerabilities in cloud browser management:
1. Budget only tracked proxyCost, not browserCost ($0.02/hr VM runtime).
Users with proxyRegion=null bypassed the budget entirely.
2. No rate limiting on /connect: rapid connect/disconnect loops waste
VM costs (Browser Use charges minimum 1 minute per VM).
3. Timeout allowed up to 240 minutes (4 hours) with no server cap.
4. customProxy accepted from users without validation.
5. Sessions with cancelled subscriptions kept running until timeout.
6. Concurrent flood: VM created before slot claim meant N concurrent
requests could create N paid VMs when only 1 slot was available.
**Fixes:**
Schema changes (db/src/schema.ts + migration):
- Rename proxy-only columns to cloud-wide:
proxySpendCents → cloudSpendCents
proxyBudgetCents → cloudBudgetCents
proxySpendPeriodStart → cloudSpendPeriodStart
lastProxyCostCents → lastTotalCostCents
- Add org.lastCloudCreateAt for per-org creation rate limiting
- Migration copies existing data to new columns
cloud-api.ts:
- Restore slot-claim-before-VM-creation pattern to prevent
concurrent flood (slot claim is cheap D1 write; VM creation
is expensive BU API call that costs money)
- Track browserCost + proxyCost as total cost in budget checks
- Add 10-second per-org creation cooldown (MIN_CREATE_INTERVAL_MS)
to prevent rapid connect/disconnect loops
- Cap timeout at 60 minutes (MAX_TIMEOUT_MINUTES) instead of 240
- Remove customProxy from connect schema (silently ignored if sent)
- Parallelize rate limit timestamp update with VM creation
scheduled.ts:
- Track total cost (browserCost + proxyCost) instead of proxy only
- Kill sessions for orgs without active subscription (cancelled/expired)
- Rename all proxy* references to cloud* for consistency
Session: ses_107161964ffefCyMno6mi5LtMr
**Before:** connect did 2 D1 writes per session: insert a placeholder row
with a pending-xxx ID, create the BU VM, then update the placeholder
with the real session ID. Required stale placeholder cleanup, pending
row filtering in status/cron, and complex rollback paths.
**After:** connect creates the BU VM first, then does 1 D1 insert with
the real session ID. If the insert fails (unique constraint = slot taken,
or any DB error), stops the VM. Simpler code, fewer D1 writes.
Changes:
- Remove PENDING_PREFIX, PENDING_STALE_MS, isPendingRow(),
checkSlotOccupied(), claimCloudSessionSlot()
- Add insertCloudSession() that inserts with real BU session ID
- Wrap post-createBrowser code in try/catch to stop VM on any failure
(prevents VM leaks on DB errors)
- Status endpoint no longer filters out pending rows
- Cron no longer has WHERE NOT LIKE pending-% filter
- resolveActiveSession and cron error handler now treat HTTP 400
(malformed ID) as dead alongside 404, cleaning up any legacy
pending-* rows on first check after deploy
Trade-off: in a rare race (two concurrent connects for the last slot),
the loser creates a VM then immediately stops it (fractions of a cent).
Session: ses_107161964ffefCyMno6mi5LtMr
better-auth-drizzle-adapter@1.0.5 now implements consumeOne and
incrementOne, so the device authorization plugin endpoints work
natively. Removed:
- Custom handleDeviceToken function and middleware intercept
- Direct DB queries for device verify, approve, deny
- orm/schema imports that were only used for device flow workarounds
The device page now uses auth.api.deviceVerify with request.headers,
and approve/deny actions use auth.api.deviceApprove/deviceDeny.
The built-in /api/auth/device/token endpoint handles token exchange.
Session: ses_10aba81e2ffeUWR93aNz4avLK2
**CLI (`playwriter/src/cli.ts`):**
- Use `createAuthClient` with `deviceAuthorizationClient` plugin instead of
hardcoded endpoint URLs. Hardcoded URLs broke when better-auth changed paths
between versions. Now calls `client.device.code()` and `client.device.token()`.
- Added `better-auth` as a dependency of the playwriter package.
**Website server (`website/src/server.tsx`):**
- Added custom `/api/auth/device/token` handler in the auth middleware, intercepted
before better-auth processes it. The drizzle adapter does not implement `consumeOne`
which the device authorization plugin requires for the token exchange. Our handler
does: find approved device code → delete it → create session → return access_token.
- Device verify page queries the deviceCode table directly via Drizzle instead of
`auth.api.deviceVerify` (which depends on `incrementOne`, also missing from adapter).
- Session check moved before device code lookup so we redirect to login first.
**Website actions (`website/src/actions.tsx`):**
- Approve/deny actions update the DB directly via Drizzle instead of
`auth.api.deviceApprove`/`deviceDeny` (which depend on adapter methods not
available in better-auth-drizzle-adapter: `consumeOne`, `incrementOne`).
**Other:**
- Hidden Cloud Browsers tab in docs.json.
- Cloud browsers docs copy improvements (unrelated).
- Changeset for the playwriter package.
Session: ses_10aba81e2ffeUWR93aNz4avLK2
The PlaywriterLogo component is now rendered above the title in the
shared AuthPage layout, wrapped in a spiceflow Link to /dashboard.
This affects all message screens: CLI Approved, CLI Denied, Invalid
Device Code, Device Code Claimed, and login pages.
Session: ses_10ab972f7ffehKgUpczAXRiPCm
- Rename "concurrent sessions" to "browsers" in billing panel subtitle
- Show subscription card before Quick Start code snippets
- Remove "Full documentation" link from Quick Start panel
- Remove descriptive paragraphs from both CLI and MCP tabs
- Remove subtitle paragraph under Quick Start heading
- Remove "Organization: ..." line from dashboard header
Session: ses_10ac8dddaffe3Oth3P4r7dJukF
Use the same orange brand color across Holocron docs and custom website routes.
- Set Holocron light and dark primary colors to #FF9500 so docs do not derive a lighter dark-mode accent.
- Override the dashboard CSS primary token and ring token to the same orange.
- Leave unrelated docs navigation changes unstaged.
Session: ses_10af1cdd3ffeTJr7Qg6c3hHUA7
Add a /live page in the website for viewing and controlling a remote CDP browser session from the browser.
This includes:
- a reusable CDP screencast component that streams Page.screencastFrame frames to a canvas
- mouse, wheel, keyboard, navigation, reload, and viewport forwarding over CDP
- a small /live entry page that accepts either a Browser Use session id or a full WebSocket URL
- stable scrollbar gutter styling to avoid layout shifts around the full-screen viewer
Session: ses_10bc2a749ffeInm3HXNY1r5v6D
New QuickStartPanel component at top of dashboard showing how to use
cloud browsers. Two tabs: CLI and MCP.
CLI tab shows two usage cases in a single code block split by a comment:
- Interactive login flow: cloud login, session new, execute
- API key flow: export PLAYWRITER_API_KEY, session new --proxy, execute
MCP tab shows the MCP client JSON config with API key env var.
Uses holocron CodeBlock for Prism syntax highlighting with copy buttons.
Matches existing dashboard card styling (rounded-xl, border-border).
Session: ses_10bb4920bffeAdYhx4RlRNuzEp
Adds placement.mode = "smart" so the worker runs closer to the D1 database
rather than always at the nearest edge, reducing latency for DB-heavy requests.
- outline button variant: replace generic border+shadow with border-foreground/15 and hover border-foreground/25, explicit bg-white with dark:bg-background
- ghost variant: remove bg-accent hover (just text color change now)
- hero GitHub link: match the new outline border style
- sign out button: same border treatment, switch from double to single quotes for consistency
**BrowserUseApiError** (browser-use.ts):
- new typed error class that carries the HTTP status code from Browser Use API responses
- replaces generic Error so callers can distinguish 404 (VM gone) from transient failures (500, 429, network)
**resolveActiveSession** (cloud-api.ts):
- only treats confirmed 404 as dead; transient errors leave the row intact for the next cron/status retry
- previously any error (including 500s and network blips) would mark the session as dead, causing premature deletion
**recordFinalCostAndDelete** (cloud-api.ts):
- new helper that atomically increments org.proxySpendCents by the delta between lastProxyCostCents baseline and final BU proxyCost, then deletes the cloudSession row in one db.batch
- used in both the DELETE /stop endpoint and resolveActiveSession cleanup
- prevents losing proxy cost data when sessions are cleaned up
**DELETE /stop endpoint** (cloud-api.ts):
- now captures final BU session state (via stopBrowser or fallback getBrowser) before deleting
- passes the session to recordFinalCostAndDelete so the cost delta is recorded
**enforceProxyBudgets** (scheduled.ts):
- same 404-only-dead logic: only pushes to deadSessionIds when the BU API error is a confirmed 404
- transient failures no longer cause budget enforcement to silently delete sessions
The cron trigger (every minute) was configured in wrangler.jsonc and the
enforceProxyBudgets() logic was added in scheduled.ts, but the Worker
default export was missing the scheduled() method. Without this, Cloudflare
has no handler to invoke when the cron fires.
Adds the scheduled() method to the ExportedHandler default export that
delegates to enforceProxyBudgets() via ctx.waitUntil().
Session: ses_10c04f97dffeANAnNI8Zof0Csj
toLocaleDateString() produces different output on Cloudflare Workers (server)
vs browser (client) due to locale differences. Use a stable UTC YYYY-MM-DD
format instead.
Session: ses_10bef183cffeBNdpywc27HbcLi
**Security:** API key auth (`x-api-key` header) is now restricted to cloud
API routes only via `getSessionWithApiKey()`. Dashboard, device flow, and
billing routes use `getSession()` which does not accept API keys. This
prevents a leaked API key from accessing the full dashboard or billing actions.
**Performance:** added index on `apikey.key` column. The plugin validates
keys by their hash, so without this index every API request would do a
full table scan.
**Cleanup:**
- `createApiKey` now returns only `{ key }`, not the full plugin result
- Removed unused `orm` and `schema` imports from actions.tsx and db.ts
- Removed accidental `scheduled.ts` import from server.tsx (untracked file)
Session: ses_10bef183cffeBNdpywc27HbcLi
Add `@better-auth/api-key` plugin with `enableSessionForAPIKeys: true` so
API keys resolve to sessions transparently via the `x-api-key` header.
All existing `requireSession`/`requireOrgSession` calls work without changes.
**Server (website):**
- `apikey` table added to D1 schema with migration
- `apiKey()` plugin in `getAuth()` config, rate limiting disabled because
`better-auth-drizzle-adapter` lacks `incrementOne` support
- `getSession()` guard updated to check `x-api-key` header
- Server actions: `createApiKey` and `revokeApiKey` in actions.tsx
- `listUserApiKeys` helper in db.ts
- `ApiKeyPanel` client component on dashboard (create, copy, revoke)
- `apiKeyClient()` plugin added to auth-client.ts
**CLI (playwriter):**
- `PLAYWRITER_API_KEY` env var is now highest-priority auth method
- API keys sent via `x-api-key` header; session tokens via `Authorization: Bearer`
- Auth priority: PLAYWRITER_API_KEY > PLAYWRITER_CLOUD_TOKEN > ~/.playwriter/auth.json
- Error messages updated to mention API key as alternative to `cloud login`
**Docs:**
- skill.md cloud section updated with API key auth option
- cloud-browsers.mdx getting started section adds Option B for API keys
Session: ses_10bef183cffeBNdpywc27HbcLi
Minimize D1 round-trips by batching independent reads and collecting
cleanup deletes into single statements.
**cloud-api.ts**
- Batch subscription + cloudSessions reads in connect handler with db.batch()
- Refactor stale/dead session cleanup: collect IDs then single DELETE WHERE IN
instead of N individual db.delete() calls
- Fix activeCount: count fresh pending rows directly instead of subtraction
formula that double-counted dead BU rows
- BU API liveness checks run in Promise.all for parallel execution
**db.ts**
- ensureOrg now returns stripeCustomerId via OrgInfo type
- New getOrgWithSubscription() fetches org + active subscription in one D1
query using nested with: relations with SQL-level status filtering
**stripe.ts**
- getOrCreateStripeCustomer accepts optional stripeCustomerId to skip D1
re-read when caller already has a truthy customer ID; always re-reads
for null to prevent duplicate Stripe customers from concurrent requests
- resolveOrgId batches both org lookups (by id + by stripeCustomerId) into
one db.batch() when both identifiers are present
**actions.tsx**
- Pass stripeCustomerId from requireOrgSession to skip redundant D1 reads
- Run getOrgSubscription + hasExistingStripeSubscription in Promise.all
(independent D1 vs Stripe API calls)
**server.tsx**
- Dashboard uses getOrgWithSubscription for the happy path (1 D1 call
instead of 2 sequential calls); falls back to ensureOrg + separate
subscription read only on first visit when org needs creating
- Add docs link and footer navigation
D1 calls saved per flow:
Dashboard: 3 → 1 (happy path)
Cloud connect: 5+N → 3+1
startCheckout: 4 → 2
openBillingPortal: 3 → 2
Stripe webhook: 2-4 → 1-2
Session: ses_10c3bdbddffeKDrBcnAAetTDyO
- removed "to stop the cloud browser" from tip since shared VMs
are only stopped when all attached sessions are deleted
- use the cloudTracking variable captured before execute() instead
of a fresh map lookup, so long-running executes that outlive idle
cleanup still report isCloud correctly
Session: ses_10c89c8b8ffe5EaurFv0P4Q2jW
relay now includes isCloud in the execute response so the CLI knows
whether the session is backed by a cloud VM. after each execute call
on a cloud session, prints a dimmed reminder to delete the session
when the task is finished.
Session: ses_10c89c8b8ffe5EaurFv0P4Q2jW
Add explicit Cloudflare route bindings for the production playwriter.dev site and the preview.playwriter.dev environment.
Keeping the route config in wrangler.jsonc makes deploys self-contained: production deploys bind to playwriter.dev/* and preview deploys bind to the preview custom domain while retaining their separate D1 bindings and required secrets.
Session: ses_10c501577ffeCjsqBQlaEqba64
Use the full dashboard content width for the subscribed Cloud Browsers billing card instead of keeping the narrower max-width style.
This matches the active subscription state with the dashboard layout and avoids a cramped card once the panel shows subscription status, renewal date, and the billing portal button.
Session: ses_10c501577ffeCjsqBQlaEqba64
Catch BetterAuth getSession failures caused by stale, corrupt, or refresh-failing cookies and return null from the shared session helper.
Protected pages now fail closed by redirecting to /login instead of surfacing a dashboard 500 with FAILED_TO_GET_SESSION. This keeps the normal logged-out path working even when a local dev or browser profile has an old BetterAuth cookie.
Session: ses_10c501577ffeCjsqBQlaEqba64
Add durable per-org cloud session slots so quota claims are enforced by SQLite uniqueness instead of a timing-sensitive count-and-recheck flow.
The cloud connect API now inserts a pending row into the first available slot before creating a Browser Use VM. Concurrent requests race on the unique `(org_id, slot_index)` constraint, so only one request can own each subscribed slot and the loser tries the next slot or receives the quota error.
Stripe Checkout now checks Stripe directly for existing active, trialing, past_due, or incomplete subscriptions before creating a new Checkout session. This closes the gap where the subscription exists in Stripe but the webhook has not mirrored it into D1 yet.
Also align cloud access checks with the shared active subscription status list so dashboard billing state and cloud-connect authorization use the same definition.
Session: ses_10c501577ffeCjsqBQlaEqba64
Replace the "Dashboard" h1 text with the PlaywriterLogo component
(h-8 size, matching login page) in the dashboard page header.
Session: ses_10c41903effeBIedYEpbbVRKME
fixes 3 of 4 issues found by oracle review:
- **Network.enable before setBlockedURLs**: added Network.enable call
before Network.setBlockedURLs to ensure reliable blocking across
Chromium versions
- **cloud key typo shows available list**: handleCloudBrowserNotFound
now takes hasCloudOptions param. if cloud options exist (user has
cloud sessions), a typo like cloud-99 falls through to the generic
"Browser not found" message showing available keys
- **expired token vs no subscription**: handleCloudBrowserNotFound
now verifies the token with a quick API call when auth file exists.
shows "authentication expired, please log in again" instead of
"you may need a subscription" when the token is invalid
skipped fix 1 (persist blockProxyResources for reattach) because it
requires API changes to include proxy state in cloud session status.
minor edge case: user must re-pass --proxy when reattaching.
Session: ses_10c89c8b8ffe5EaurFv0P4Q2jW
when user passes --browser cloud (or cloud-N) and no cloud browsers are
available, detect the cloud key prefix and show step-by-step instructions
instead of a generic "Browser not found" error.
two cases:
- not logged in: shows login + subscribe + retry steps
- logged in but no sessions: shows subscribe link + retry
checked at all 4 code paths where browser key lookup can fail:
direct-only mode, cloud-only mode, multi-browser mode, and the
no-extensions fallback path.
Session: ses_10c89c8b8ffe5EaurFv0P4Q2jW
**proxy disabled by default**: proxyCountryCode now defaults to null instead of us.
users must pass --proxy <region> explicitly to enable residential proxies.
saves cost for users who do not need anti-detection or geo-targeting.
**proxy bandwidth acceleration**: when proxy is enabled, raster images
(png, jpg, jpeg, gif, webp, ico, bmp, avif) are automatically blocked
via Network.setBlockedURLs CDP command. this is a single fire-and-forget
command with zero per-request overhead. SVGs, fonts, and videos are allowed.
opt out with --disable-proxy-bandwidth-acceleration on session new.
**cloud browser tips**: added printCloudTip() shown after every local
session creation and in browser list when no cloud sessions exist.
updated EXTENSION_NOT_CONNECTED_ERROR to mention cloud as option 3.
**skill.md cloud section expanded**: full feature list covering CAPTCHA
bypass, anti-detection, residential proxies, VPS usage, parallel execution
with subagents, and multiple identity control.
Session: ses_10c89c8b8ffe5EaurFv0P4Q2jW
- Add stripe ^22.2.2 to website dependencies
- Declare STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET in wrangler.jsonc
secrets.required for both production and preview environments
- Add stripe:listen script with tuistory for background webhook forwarding
using --live mode for non-test Stripe accounts
Session: ses_10d035e28ffe5cVA8SzrKoNgAP
Following the holocron reference implementation pattern.
billing-rules.ts: pure decision constants.
- CLOUD_PRICE_LOOKUP_KEYS (cloud_browser_monthly/yearly) for price rotation
- ACTIVE_SUBSCRIPTION_STATUSES (active, trialing, past_due)
- BillingSubscription lightweight type for dashboard UI
lib/stripe.ts: Stripe helpers with error-as-values pattern.
- getStripe() with fetch HTTP client for Workers
- getOrCreateStripeCustomer(): idempotent, single source of truth for
stripe.customers.create. Writes metadata.orgId on the Stripe customer.
- getCloudPrices/getCloudPriceId: fetch by lookup key, not hardcoded IDs
- handleSubscriptionChange: re-fetches latest from Stripe, resolves orgId
via metadata -> stripeCustomerId -> customer metadata fallback chain,
upserts subscription row idempotently on subscriptionId
stripe-webhook.ts: refactored to import from lib/stripe.ts instead of
duplicating client construction and handler logic. Uses SubtleCryptoProvider
for Workers HMAC verification.
actions.tsx: server actions extracted from server.tsx.
- startCheckout: creates Stripe Checkout with managed_payments enabled,
adjustable_quantity (min 1, max 100, default 4), metadata on both session
and subscription_data. Short-circuits to portal if already subscribed.
- openBillingPortal: opens Stripe billing portal for existing subscribers
- approveDevice/denyDevice: moved from inline server.tsx
billing-panel.tsx: client component for dashboard.
- Active state: shows quantity, status, renewal date, cancel status,
"Manage subscription" button via openBillingPortal action
- Free state: feature list, Subscribe monthly/yearly buttons with quantity=4
db.ts: add getOrgSubscription(orgId) helper and requireOrgSession().
server.tsx: dashboard page queries subscription, renders BillingPanel.
Device flow actions imported from actions.tsx.
Session: ses_10d035e28ffe5cVA8SzrKoNgAP
browser-use.ts: typed HTTP client for Browser Use Cloud API v3.
- BrowserUseClient class: createBrowser, getBrowser, stopBrowser, listBrowsers
- Supports proxy regions (195+ countries), custom proxies, screen dimensions,
recording, and session timeouts (1-240 min)
- API key never reaches the client; only used server-side in the Worker
cloud-api.ts: Spiceflow sub-app mounted at /api/cloud/*.
- GET /status: returns org active cloud sessions with live VM status from BU API.
Dead VMs are auto-cleaned from D1 (resolveActiveSession helper).
- POST /connect: creates a new BU VM after checking subscription quota.
Uses placeholder-then-update pattern for atomic slot claiming to prevent
two concurrent requests from both creating VMs. Cleans up on all failure paths.
- POST /disconnect: stops a BU VM and removes the D1 mapping row.
All routes require authenticated session + org via requireOrgSession().
Session: ses_10d035e28ffe5cVA8SzrKoNgAP
New CLI commands:
- cloud login: device flow auth with playwriter.dev, stores token in
~/.playwriter/auth.json. Uses goke openInBrowser for cross-platform URL opening.
- cloud status: shows active cloud VMs with expiry times
- cloud subscribe: opens dashboard in browser for subscription management
session new enhancements:
- Discovers cloud browsers alongside local extension and direct CDP instances
- --browser cloud creates a fresh VM, --browser cloud-N reattaches to running VM
- --proxy <region> enables residential proxy (us, de, jp, etc.)
- --custom-proxy user:pass@host:port for custom proxy servers
- --timeout <minutes> sets VM timeout (1-240, default 60)
- --disable-proxy-bandwidth-acceleration allows images/video/fonts through proxy
- Cloud-only users (no extension) see cloud options without error
- discoverCloudBrowsers() fetches active VMs + always adds a "cloud" new option
browser list also discovers and displays cloud browsers.
skill.md: document cloud browser CLI usage (login, status, session new with
proxy/region options, auto-stop behavior).
Session: ses_10d035e28ffe5cVA8SzrKoNgAP
Cloud session lifecycle management in the relay server:
**Activity tracking**: CloudSessionTracking map tracks lastActivityAt and
activeExecutions count per relay session. Execute endpoint bumps activity
timestamps before and after each code execution. Sessions with active
executions are never idle-timed-out.
**Idle timeout**: 60s interval checks all cloud-backed sessions. Sessions
idle >10 minutes (or past BU hard timeout) are auto-disconnected via the
website /api/cloud/disconnect endpoint. Reference counting prevents stopping
a VM that is still used by another relay session (e.g. user attached twice).
**Crash recovery**: cloud session IDs are persisted to
~/.playwriter/cloud-sessions.json on every state change. On relay startup
(after successful listen), orphaned VMs from a previous crash are
disconnected. Atomic write (tmp+rename) prevents corrupt JSON on crash
during write. File is removed when no active sessions remain.
**Session creation**: /cli/session/new accepts optional cloud metadata
(cloudSessionId, cloudBaseUrl, cloudToken, timeoutAt, blockProxyResources)
and registers tracking. Session deletion cleans up tracking and stops the
VM if it is the last reference.
**Other**: global Hono error handler to prevent silent 500s.
Session: ses_10d035e28ffe5cVA8SzrKoNgAP