From 5eb3faf877ae00d4bb8ce8405eaefac1ca8699b8 Mon Sep 17 00:00:00 2001 From: robertjamesprior <83608739+robertjamesprior@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:38:56 +0000 Subject: [PATCH] Add Per-User Sessions guide Assembles the bring-your-own-credentials path for running one authenticated browser per end user: profiles, the one-writer rule, pools with reuse: false, standby economics, and per-user tagging. Profiles currently sit under the Auth group, so customers who aren't using Managed Auth have a reason to skip the page that explains the primitive they most need. This page sits alongside that group and routes to the existing sections without the Managed Auth assumption. Also documents that readOnly on a live view URL is a display option rather than a security boundary, which was not stated anywhere. Co-Authored-By: Claude Opus 5 --- browsers/per-user-sessions.mdx | 184 +++++++++++++++++++++++++++++++++ docs.json | 1 + 2 files changed, 185 insertions(+) create mode 100644 browsers/per-user-sessions.mdx diff --git a/browsers/per-user-sessions.mdx b/browsers/per-user-sessions.mdx new file mode 100644 index 0000000..508c465 --- /dev/null +++ b/browsers/per-user-sessions.mdx @@ -0,0 +1,184 @@ +--- +title: "Per-User Sessions" +description: "Run one authenticated browser per end user when your application owns the credentials" +--- + +When your application signs in your own users — you hold their credentials, or they enter them +themselves — each user needs browser state that's isolated from every other user. This guide covers +that shape: where the state lives, how to avoid corrupting it, how to keep cold starts off your +latency budget, and what a per-user session costs. + +If you'd rather Kernel perform the login and keep it healthy for you, use +[Managed Auth](/auth/overview) instead. Everything below assumes you're doing the login yourself. + +## Give each user their own profile + +A [profile](/auth/profiles) is the unit of per-user state: it carries cookies and local storage into +a browser. Create one per user, drive your own login flow in a browser that references it, and set +`save_changes` so the resulting session is written back when the browser is deleted. + + +```typescript Typescript/Javascript +await kernel.profiles.create({ name: 'user-8f21c3' }); + +const kernelBrowser = await kernel.browsers.create({ + profile: { name: 'user-8f21c3', save_changes: true }, +}); + +// ... run your login flow as this user ... + +await kernel.browsers.deleteByID(kernelBrowser.session_id); +``` + +```python Python +kernel.profiles.create(name="user-8f21c3") + +kernel_browser = kernel.browsers.create( + profile={"name": "user-8f21c3", "save_changes": True}, +) + +# ... run your login flow as this user ... + +kernel.browsers.delete_by_id(kernel_browser.session_id) +``` + +```go Go +if _, err := client.Profiles.New(ctx, kernel.ProfileNewParams{ + Name: kernel.String("user-8f21c3"), +}); err != nil { + panic(err) +} + +kernelBrowser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{ + Profile: shared.BrowserProfileParam{ + Name: kernel.String("user-8f21c3"), + SaveChanges: kernel.Bool(true), + }, +}) +if err != nil { + panic(err) +} + +// ... run your login flow as this user ... + +if err := client.Browsers.DeleteByID(ctx, kernelBrowser.SessionID); err != nil { + panic(err) +} +``` + + +State is written on browser deletion, not on `browser.close()` — see [Profiles](/auth/profiles). + +Keep one profile per user per target site rather than accumulating every site a user touches into a +single profile. Large profiles carry more cookies and origins, and that slows browser startup on +every later session. + +## Keep one writer per profile + +Saving replaces a profile's entire stored state — it doesn't merge. If two browsers use the same +profile with `save_changes: true`, the one that ends last wins and the other user's work is lost +silently. In a per-user deployment the symptom is a user who appears to get logged out at random. + +Run exactly one writer per profile at a time, and omit `save_changes` on everything else so parallel +work reads the profile without racing to write it. Before you start a writer, check for an existing +one — [Prevent concurrent profile writes](/auth/profiles#prevent-concurrent-profile-writes) has the +query and examples. + + +That check and the browser creation are separate requests. If more than one of your workers can +start a session for the same user, hold your own lock or lease across both operations. + + +## Serve many users from one browser pool + +Per-user traffic is bursty, so cold start is the latency your users feel. +[Browser pools](/browsers/pools) keep warm browsers ready, but a profile attached to a pool is +shared and read-only, which is the opposite of what you need here. + +Create the pool with no profile, attach the user's profile after you acquire a browser, and release +with `reuse: false`. See +[Per-user profiles with browser pools](/browsers/pools#per-user-profiles-with-browser-pools) for the +full example. + + +Releasing with `reuse: true` hands that user's logged-in browser to whoever acquires next. Always +release per-user browsers with `reuse: false`. + + +## Share a live view with your user + +You might want your user to finish a step themselves — entering a password, clearing MFA, or +approving a prompt. [Live view](/browsers/live-view) is how you show them the browser, but treat the +URL as a credential rather than a link. + +- **The live view URL grants control of that browser.** Anyone who has it can drive the session. +- **`readOnly` is a display option, not a security boundary.** It makes the embedded view + non-interactive. Don't rely on it to stop a recipient from acting on the browser. +- **Don't hand the URL to a user directly.** Serve it from your own backend behind your own + authorization, or embed it in a page you control, so you decide who reaches it and for how long. +- **Deleting the browser is how you revoke access.** The URL stays usable while the browser exists, + independent of whether anyone is watching. + +Because each browser belongs to one user, a URL that leaks exposes only that user's session — which +is another reason to keep pooled browsers on `reuse: false`. + +## Understand what an idle user costs + +A browser enters [standby](/browsers/standby) after five seconds with no CDP client, live view, or +computer-controls call in flight. State is preserved and usage costs stop, so a per-user browser +that's waiting on its user is cheap to leave running. You don't need to tear a session down and +rebuild it to avoid paying for idle time. + +Two things to plan around: + +- [GPU-accelerated browsers](/browsers/gpu-acceleration) don't support standby, so an idle + GPU browser keeps costing you. Reach for GPU only when a workload needs it. +- Standby starts the browser's [timeout](/browsers/termination#automatic-deletion-via-timeout) + countdown, and `timeout_seconds` defaults to **60**. A browser waiting on its user is deleted a + minute later unless you raise it. Set it to cover how long you're willing to hold a session open — + the maximum is 259200 (72 hours). + +To attribute cost per user, tag sessions at creation and break usage down by tag later. + + +```typescript Typescript/Javascript +const kernelBrowser = await kernel.browsers.create({ + profile: { name: 'user-8f21c3', save_changes: true }, + timeout_seconds: 1800, + tags: { end_user: 'user-8f21c3', workflow: 'inbox-triage' }, +}); +``` + +```python Python +kernel_browser = kernel.browsers.create( + profile={"name": "user-8f21c3", "save_changes": True}, + timeout_seconds=1800, + tags={"end_user": "user-8f21c3", "workflow": "inbox-triage"}, +) +``` + +```go Go +kernelBrowser, err := client.Browsers.New(ctx, kernel.BrowserNewParams{ + Profile: shared.BrowserProfileParam{ + Name: kernel.String("user-8f21c3"), + SaveChanges: kernel.Bool(true), + }, + TimeoutSeconds: kernel.Int(1800), + Tags: kernel.Tags{ + "end_user": "user-8f21c3", + "workflow": "inbox-triage", + }, +}) +if err != nil { + panic(err) +} +``` + + +## Before you go to production + +- One profile per user per site, populated by your own login flow. +- One writer per profile, with your own lock around the check and the create. +- Pools created without a profile; attach after acquire, release with `reuse: false`. +- Live view URLs served from your backend, never handed to a user directly. +- A `timeout_seconds` that matches how long a user's session may stay open, and tags on every browser. diff --git a/docs.json b/docs.json index bb0e9df..f42d33a 100644 --- a/docs.json +++ b/docs.json @@ -111,6 +111,7 @@ "browsers/replays", "browsers/viewport", "browsers/gpu-acceleration", + "browsers/per-user-sessions", { "group": "Auth", "pages": [