server-runtime
Server runtime
How Verklet moves from the browser backend to managed server execution for server-only Python, uv, and native-tool workloads.
Verklet is browser-first. The default backend runs Node-style workloads inside the visitor's tab, using workers, WebAssembly, OPFS, and the preview service worker.
Some workloads need a real server environment: uv, native Python
wheels, subprocess-heavy Python, native Linux tools, larger memory
ceilings, or a workspace that should outlive browser storage. Supported
Python runs in the browser through Pyodide first. For everything else,
the SDK can boot a server backend directly or promote from the browser
backend when a command requires it.
Choosing a backend
import { Runtime } from '@verklet/sdk';
const runtime = await Runtime.boot({
projectId: 'prj_your_project_id',
backend: 'auto',
persistenceKey: 'tutorial-session',
server: {
grant: async (request) => {
const response = await fetch('/api/verklet/server-grant', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(request),
});
return response.json();
},
prepare: [['npm', 'ci']],
},
});
backend: 'auto' is the recommended default for hybrid products. It
starts in the browser, runs supported python, python3, pip, and
pip3 through Pyodide, then promotes to the server backend for
unsupported Python workloads, uv, and native-tool commands.
Server promotion requires a short-lived broker grant. The SDK does not
fetch that grant automatically because production apps usually need
their own authentication, authorization, headers, and budget checks.
Pass server.grant as a grant object, token string, or callback. The
callback receives projectId, runtimeId, workspaceKey, the action,
and the browser installation ID, so your backend can mint a scoped
grant only after it knows what workspace is being promoted.
Use backend: 'browser' when you explicitly want browser-only
execution. Use backend: 'server' when the whole session should start
on the server.
Account setup
Before server promotion can work, configure the project in /account:
- Enable managed server runtime grants.
- Add the exact browser origins that may boot the project, such as
https://app.example.com. Origins include scheme, host, and port. - Set caps for monthly server spend, max session duration, active sessions, and active workspaces. These caps apply to the whole project, across browsers and backend callers.
- Create one or more named private grant secrets. They start with
vks_, are shown only once at creation time, and should only live in backend environment variables. - Keep public browser grant minting off for production unless you have a deliberate reason to expose grant creation to browser code.
The fixed beta resource policy is one CPU and 512 MB memory per server session. Those limits are enforced by the broker and are not editable in the account UI yet.
Private grant endpoint
For production, create a small endpoint in your app that authenticates
the user, checks that the requested project/workspace belongs to that
user or tenant, then asks Verklet to mint the broker grant with your
private vks_... secret.
This example uses a Next.js route handler:
// app/api/verklet/server-grant/route.ts
import type { ServerRuntimeGrantRequest } from '@verklet/sdk';
const verkletGrantSecret = process.env.VERKLET_PRIVATE_GRANT_SECRET;
export async function POST(request: Request) {
if (!verkletGrantSecret) {
return Response.json({ error: 'Server grants are not configured' }, { status: 503 });
}
const user = await requireUser(request);
const body = (await request.json()) as ServerRuntimeGrantRequest;
if (!await userCanUseVerkletProject(user, body.projectId, body.workspaceKey)) {
return Response.json({ error: 'Not found' }, { status: 404 });
}
const response = await fetch(
`https://verklet.com/api/runtime/server/grants?projectId=${encodeURIComponent(body.projectId)}`,
{
method: 'POST',
headers: {
authorization: `Bearer ${verkletGrantSecret}`,
'content-type': 'application/json',
},
body: JSON.stringify(body),
},
);
const grant = await response.json();
return Response.json(grant, {
status: response.status,
headers: { 'cache-control': 'no-store' },
});
}
The SDK calls your server.grant callback with:
| Field | Meaning |
|---|---|
action | create_session when starting or promoting a server session, delete_persistence when deleting server persistence. |
projectId | The public project ID from Runtime.boot(). Verify it belongs to the current tenant. |
runtimeId | The SDK runtime ID. Required for create_session. |
workspaceKey | The server workspace key. Defaults to server.workspaceKey, then persistenceKey, then runtimeId. |
persistenceKey | Included when the workspace came from a persistence key. |
browserInstallationId | A stable browser installation identifier for grant telemetry and rate checks. |
The hosted grant endpoint returns:
type ServerRuntimeGrant = {
brokerToken: string;
brokerUrl?: string;
expiresAt?: string;
};
Return that JSON directly from your backend. Do not store it in local storage, cookies, or logs. Grants are short-lived, action-scoped, and single-use for create-session and persistence deletion.
Browser SDK wiring
Point server.grant at your endpoint. The callback runs only when the
SDK has enough context to request a scoped grant:
const runtime = await Runtime.boot({
projectId: 'prj_your_project_id',
backend: 'auto',
persistenceKey: 'tutorial-session',
server: {
grant: async (grantRequest) => {
const response = await fetch('/api/verklet/server-grant', {
method: 'POST',
headers: {
authorization: `Bearer ${await getYourAppSessionToken()}`,
'content-type': 'application/json',
},
body: JSON.stringify(grantRequest),
});
if (!response.ok) {
throw new Error(`Server grant denied: ${response.status}`);
}
return response.json();
},
},
});
If you already minted a grant on the server while rendering the page,
you can pass that object directly. A direct grant is useful for
backend: 'server', but a callback is usually better for
backend: 'auto' because promotion may happen later and needs the final
runtimeId and workspaceKey.
const runtime = await Runtime.boot({
projectId: 'prj_your_project_id',
backend: 'server',
server: { grant: initialServerGrant },
});
Public grant minting
Public minting lets browser code call Verklet's hosted grant endpoint
without your private vks_... secret. It is disabled by default and
only works after you explicitly enable it in /account.
const runtime = await Runtime.boot({
projectId: 'prj_your_project_id',
backend: 'auto',
server: {
grant: async (grantRequest) => {
const response = await fetch(
`https://verklet.com/api/runtime/server/grants?projectId=${encodeURIComponent(grantRequest.projectId)}`,
{
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(grantRequest),
},
);
return response.json();
},
},
});
Use this only for controlled demos or low-risk apps with strict caps.
Origin checks reduce drive-by use, but they are not a secret or
authentication boundary. Anything that can run JavaScript on an allowed
origin can attempt to mint public grants until the project caps stop it.
For example, a request with an allowed Origin header is enough to ask
for a public grant when public minting is enabled.
Server dependency setup
Promotion sends source files and project manifests to the server
workspace, not browser-specific install output such as node_modules.
Successful deterministic install commands run in the browser before
promotion are replayed on the server by default. This includes commands
such as npm install, npm ci, pnpm install, yarn install,
pip install, python -m pip install, uv sync, uv pip install,
and poetry install.
Use server.prepare for explicit server-only setup:
const runtime = await Runtime.boot({
projectId: 'prj_your_project_id',
backend: 'auto',
server: {
grant: window.__serverGrantFromYourBackend,
prepare: [
['npm', 'ci'],
['pip', 'install', '-r', 'requirements.txt'],
],
},
});
On first promotion, Verklet mounts the source snapshot, replays the
successful browser install commands unless server.replayInstalls is
false, runs server.prepare, then reruns the command that required
the server backend.
What promotion does
When automatic promotion happens, Verklet exports a binary snapshot of the browser filesystem without browser-only dependency artifacts, creates a server runtime session, mounts that snapshot into the server workspace, performs server dependency setup, tears down the browser runtime, and continues through the same SDK object.
Your UI keeps using the same APIs:
runtime.fsfor file reads and writes.runtime.spawn()for commands and stdio streams.runtime.on(...)for process, diagnostic, port, and server-ready events.runtime.persist()andRuntime.clearPersistence()for the active backend's persistence path.
projectId is public and safe in browser code. Broker grants and
vks_... private grant secrets are not. Use a backend endpoint for
production grants, keep the private secret server-side, and set allowed
origins plus caps in /account. Hosted public minting is available only
when explicitly enabled for a project and should be treated as an
abuse-prone convenience path.
Deleting server persistence also needs a scoped grant:
await Runtime.clearPersistence('tutorial-session', {
backend: 'server',
projectId: 'prj_your_project_id',
grant: async (grantRequest) => {
const response = await fetch('/api/verklet/server-grant', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(grantRequest),
});
return response.json();
},
});
Capabilities
The browser backend is best for cheap, low-latency JavaScript, Node-compatible demos, and supported Python/Pyodide work. It uses OPFS for persisted snapshots when the browser supports it.
The browser backend reports python: 'pyodide'. The server backend
reports capabilities such as backend: 'server', linux: true,
nativePackages: true, python: 'system', checkpoints: true, and
persistentFilesystem: 'server'. It is the right path for Python
package stacks that need native wheels, agent tasks that call native
tools, and workflows that should not depend on browser storage.
Pricing and server hours
Server-runtime usage is included during beta. After beta, non-commercial projects keep browser-runtime access and paid projects include a monthly pool of server hours.
A server hour is one hour where a Verklet server runtime is active for a project. Browser-only execution does not use server hours. Usage is measured by the minute and shown as hours on invoices.
Starter includes 40 server hours per month, Pro includes 300 server hours per month, and Scale includes 2,500 server hours per month. Extra server time is billed at $0.30/hr on Starter and Pro, and $0.18/hr on Scale. See pricing for the current plan limits.
Data and limits
Server runtime sessions send workspace snapshots, commands, stdout/stderr, exit codes, quota events, and usage metadata to Verklet's runtime broker. The broker enforces beta limits for session creation, active sessions, active processes, snapshot size, and request size. Grant attempts, denials, allowed origins, project caps, and usage events are stored so Verklet can enforce suspension, budget, and quota decisions.
Browser runtime files stay in the origin's OPFS unless you explicitly promote or start a server backend. See the privacy policy for the data boundary and pricing for usage terms.