"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
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
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
- 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
Shared UI components for login, device flow, and dashboard pages.
Uses Tailwind CSS with class-variance-authority for button variants
and radix-ui Slot for polymorphic rendering.
Components:
- AuthPage: centered layout with title, description, optional
children and footer. Used by login, device, and error states.
- PlaywriterLogo: logo image with dark mode invert
- Button: CVA-based button with 6 variants, 4 sizes, form pending
states via useFormStatus, and loading spinner
- SignInButton: redirects to /login/google OAuth flow with loading
state (uses server-side redirect, not client authClient.signIn)
- SignOutButton: uses authClient.signOut() POST endpoint
- DeviceActionButtons: approve/deny forms with ErrorBoundary wrapper
Utilities:
- auth-redirect.ts: normalizeAuthRedirectPath strips RSC transport
params (__rsc) and .rsc extensions from callback URLs so auth
redirects land on browser-facing pages, not internal RSC URLs
- lib/utils.ts: cn() helper combining clsx + tailwind-merge
- globals.css: @reference tailwindcss (no duplicate import since
Holocron already includes Tailwind), dark variant definition
Switch the website from a pure Holocron docs site to a Spiceflow
custom entry app deployed on Cloudflare Workers with D1.
vite.config.ts:
- Add holocron({ entry, pagesDir }) for custom entry mode
- Add @cloudflare/vite-plugin with rsc + ssr environments
wrangler.jsonc:
- D1 database binding for playwriter-db (+ preview env)
- Required secrets: BETTER_AUTH_SECRET, BETTER_AUTH_URL,
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, BROWSER_USE_API_KEY
- Preview environment with separate D1 database
src/db.ts (worker-level helpers):
- getDb(): drizzle-orm/d1 client bound to env.DB
- getAuth(): per-request BetterAuth instance (not singleton,
Workers need fresh env per request). Google social provider
with select_account prompt, device authorization plugin with
schema: {} to avoid ZodError, bearer plugin for CLI tokens,
1-year session expiry, 5-min cookie cache.
- getSession() / requireSession(): session resolution
- ensureOrg(): race-safe org creation with unique constraint catch
src/server.tsx:
- Auth middleware intercepting /api/auth/* before holocron
- /login page with Google sign-in via server-side OAuth redirect
(createGoogleSignInRedirect forwards Set-Cookie headers for CSRF)
- /login/google GET route that builds the OAuth redirect
- /dashboard page with session check, org auto-creation
- /device page for CLI device flow (verify, approve, deny)
- Server actions for device approve/deny with requireSession
- normalizeAuthRedirectPath for safe callback URLs
- Holocron mounted last as catch-all for docs pages
- ExportedHandler fetch export for Cloudflare Workers
src/lib/auth-client.ts:
- createAuthClient() for client-side better-auth React hooks
Holocron custom entry requires docs under a subfolder so they render
at /docs/* and do not collide with app routes like /login, /dashboard,
/device, /api/*.
Changes:
- Copy all MDX files from src/docs/ to src/pages/docs/
- Copy index.mdx to src/pages/index.mdx (homepage stays at /)
- Fix relative import paths in skill.mdx and mcp-setup.mdx
(one level deeper now: ../../../ becomes ../../../../)
- Add knownPaths for /api/*, /login, /dashboard, /device in docs.json
so holocron does not try to render 404 pages for app routes
The navigation slugs in docs.json remain unchanged because pagesDir
changes from src to src/pages, keeping the same relative paths.
Create the `db` package at the repo root as a pnpm workspace member.
Contains the full database schema for Playwriter cloud browser support:
BetterAuth core tables:
- user, session, account, verification (Google OAuth + cookie sessions)
- deviceCode (CLI device authorization flow)
Organization tables:
- org (with stripeCustomerId placeholder for future billing)
- orgMember (admin/member roles, unique constraint on userId for
race-safe ensureOrg, unique on orgId+userId for membership)
Cloud browser tables:
- cloudBrowser (Browser Use profiles tied to an org)
- cloudSession (active sessions, org derived via cloudBrowser FK
to avoid denormalization drift)
All timestamp columns use the epochMs custom type which converts
Date objects to epoch milliseconds in toDriver, required because
BetterAuth passes Date params but D1 .bind() only accepts
string | number | null | ArrayBuffer.
Package exports:
- workerd condition: drizzle-orm/d1 via cloudflare:workers env.DB
- default condition: drizzle-orm/sqlite-proxy via D1 HTTP API
for Node.js scripts querying remote D1 outside Workers
Relations use defineRelations v2 API with many-to-many user<->org
through orgMember junction table.
Initial migration generated with drizzle-kit for all tables and indexes.
Move each comparison (vs Playwright MCP, vs Playwright CLI, vs BrowserMCP,
vs Claude Extension) from inline tables on the homepage into dedicated
pages under docs/. Add a new in-depth "vs Chrome Direct CDP" page with a
screenshot of Chrome permission dialog that blocks autonomous agents.
Homepage comparison section now links to the individual pages instead of
showing inline tables. Sidebar has a new "Comparison" group with distinct
lucide icons per page: shield-alert, drama, terminal, puzzle, bot.
Session: ses_120bfd373ffex2vJTLlX06mQ83
- Create website/src/docs/remote-access.mdx with full remote access guide
(traforo tunnel setup, Docker/devcontainer, LAN, security, env vars)
- Add docs/remote-access to website navigation in docs.json
- Add "Remote access" section to skill.md with quick-start snippet
linking to https://playwriter.dev/docs/remote-access
- Converted original docs/remote-access.md content to MDX with
diagram code fences and Aside components
Session: ses_1257fc4baffeU7BhTtBImAFQ1a
- Add HeroSection component with VideoBackgroundShader (dot-grid fluid sim)
using hero-bg.mp4 video and orange dot color at 70% opacity
- Two CTA buttons: Chrome Extension (white) and GitHub (outlined)
- 4.7 star rating badge with "20k+ users" linking to Chrome Web Store
- Serif title: "let agents control your Chrome browser."
- "Learn more" arrow linking to #getting-started
- Replace old Hero/Logo block with Above + HeroSection in index.mdx
- Set rendering: static for prerendered HTML at build time
- Set primary color to #FF9500 (bright orange, matching screenshot labels)
- Update @holocron.so/vite to 0.21.0 and wrangler to ^4.101.0
- Fix spiceflow duplication via pnpm dedupe
Session: ses_12885eca3ffenoMavJE7csK9E1
Update the website dependency set before publishing Playwriter so the workspace starts from a clean tree.
- update @holocron.so/vite to 0.16.0
- update spiceflow RSC build to 1.26.0-rsc.1
- update wrangler and workerd through the lockfile
- allow workerd as an approved built dependency for pnpm
Session: ses_unknown