# Viby SDK > Framework-neutral infrastructure for vibe coding products. ## Chats and projects URL: /docs/api/chats Create, import, search, update, retain, and inspect tenant-scoped project timelines. # Chats and projects A chat is the durable project boundary. It owns messages, generations, versions, attachments, environment variables, provider histories, tool selections, and application metadata. ## `ChatCollection` Access the collection through `viby.forUser(scope).chats`. | Method | Returns | Behavior | | --- | --- | --- | | `create(input?)` | `Chat` | Creates an empty chat with optional title and JSON metadata. No generation starts. | | `import(input)` | `Chat` | Validates files, a ZIP, or a configured source adapter and creates the chat plus its first immutable version atomically. | | `get(id)` | `Chat` | Loads an active in-scope chat. Deleted, missing, or cross-scope records are not exposed. | | `list(options?)` | `CursorPage` | Lists active chats in stable order with an opaque cursor and optional nested metadata filter. | | `snapshot(id, options?)` | chat, messages, and versions | Loads one consistent detail-view snapshot with independently paginated message and version windows. | | `restore(id)` | `Chat` | Clears a soft-delete tombstone while the record still exists. | | `purgeDeleted({ limit? })` | `number` | Permanently removes eligible deleted chats and their scoped durable resources in bounded batches. | ### Create and search ```ts const chat = await user.chats.create({ title: "Customer portal", metadata: { workspace: "acme", template: "saas" }, }); const page = await user.chats.list({ limit: 20, after: savedCursor, metadata: { workspace: "acme" }, }); ``` Metadata values are JSON. Filters match the requested nested structure and never bypass tenant/user scope. Cursor strings are opaque; do not parse or synthesize them. ### Prompt-derived titles Use the portable `titleFromPrompt()` helper when a product should show a concise sidebar title as soon as the first prompt is submitted. It is deterministic and does not contact a model, so it adds no provider cost or latency to chat creation: ```ts import { titleFromPrompt } from "@viby/sdk/core"; const prompt = "Build a polished SaaS analytics dashboard with revenue charts"; const chat = await user.chats.create({ title: titleFromPrompt(prompt), metadata: { titleSource: "prompt" }, }); await chat.start({ prompt }); ``` The default result contains at most seven words and 48 characters. The helper removes common request language and implementation detail clauses, preserves meaningful casing such as `SaaS` or `AI`, and returns `New project` when no usable prompt text remains. Automatic titles remain application-owned: products can later call `chat.update({ title })` after a user rename or an optional model-assisted refinement. `snapshot()` uses one aggregate PostgreSQL query when available. Custom persistence adapters can provide `readChatSnapshot`; adapters without it use the portable fallback with the same ownership checks and cursor semantics. ### Import files or a ZIP ```ts const fromFiles = await user.chats.import({ title: "Imported app", filePolicy: { locked: ["package.json", "farm.config.ts"] }, source: { type: "files", files: [ { path: "src/main.tsx", content: source }, { type: "artifact", path: "public/logo.png", bytes, mediaType: "image/png" }, ], }, }); const fromZip = await user.chats.import({ source: { type: "zip", bytes: uploadedZip }, }); ``` Paths are normalized and traversal is rejected. Text must be valid UTF-8. Binary entries are stored through the configured artifact store. Locked paths cannot be changed by generation, workspace tools, direct patches, moves, or deletes in descendant versions. ## `Chat` ### Properties | Property | Meaning | | --- | --- | | `id` | Opaque chat identifier. | | `title` | Current user-facing title. | | `metadata` | Current application-owned JSON metadata snapshot. | | `framework` | Framework identifier fixed when the chat is created or imported. | | `createdAt` / `updatedAt` | Durable lifecycle timestamps. | | `environment` | Chat-scoped environment-variable collection when environment support is configured. | | `toolSources` | Explicit durable tool-source selection for this chat. | ### Lifecycle and generation | Method | Behavior | | --- | --- | | `update({ title?, metadata? })` | Replaces supplied mutable fields and returns a refreshed handle. Omitted fields are preserved. | | `delete({ retentionMs? })` | Soft-deletes the chat and returns `deletedAt` plus nullable `purgeAfter`. It does not perform an immediate provider-side cleanup. | | `start(input)` | Persists a logical generation and first attempt, schedules according to execution mode, and returns an addressable handle. | | `generate(input)` | Convenience method that waits for success and returns a version. It throws on failure, cancellation, or required tasks. | | `getGeneration(id)` | Loads a generation belonging to this chat. | | `startFromVersion(input, version)` | Starts a durable generation from an exact version snapshot. | | `generateFromVersion(input, version)` | Synchronous convenience around `startFromVersion()` and `wait()`. | `GenerateInput` accepts `prompt`, a configured `model` alias, host `instructions`, categorized `skills`, JSON `metadata`, and up to the configured attachment limits. Attachments are immutable and checksum-verified before later attempts reuse them. ### Versions and messages | Method | Behavior | | --- | --- | | `latestVersion()` | Returns the newest version or `null` when the chat has no source yet. | | `getVersion(id)` | Loads one version from this chat. | | `listVersions({ limit?, after? })` | Returns a cursor page of immutable versions. | | `listMessages({ limit?, after? })` | Returns durable user and assistant messages in stable order. | | `getMessage(id)` | Loads one message by ID without scanning pages. | | `getAttachment(id)` | Reads scoped attachment bytes and verified metadata from the artifact store. | Assistant messages include an optional provider-neutral finish reason and ordered typed parts such as text, status, reasoning summary, file activity, search, command, tool calls, errors, and usage. Incomplete trace parts never become final message parts. ### Repository and deployment history | Method | Behavior | | --- | --- | | `repositoryLinks()` | Lists durable links between the chat and remote repositories. | | `repositoryPushes()` | Lists version-bound push and pull-request outcomes, including failures and conflicts. | | `deploymentProjects()` | Lists durable links between the chat and provider projects. | | `deployments()` | Lists deployment lifecycle records across all chat versions. | These methods return Viby records and work after a restart. They do not query providers unless an explicit provider operation is invoked. ## Project environment variables Enable the feature with `environment: {}` or an application-provided store. ```ts await chat.environment.set({ environment: "preview", name: "PUBLIC_API_ORIGIN", value: "https://api.example.com", }); await chat.environment.set({ environment: "preview", name: "SERVICE_TOKEN", value: token, secret: true, }); const records = await chat.environment.list({ environment: "preview" }); await chat.environment.delete({ environment: "preview", name: "SERVICE_TOKEN" }); ``` Public values are returned through `value`. Secret records always return `value: null`; bytes remain behind `storage.secrets`. Values resolve only when a sandbox, preview, build, or deployment explicitly selects the environment. ## Deletion and retention The default retention is 30 days. Configure `retention.deletedChatsMs`, override it on one `delete`, and run `purgeDeleted()` from a host-owned schedule. `null` retains indefinitely. Purge is destructive and cannot be undone; products should make that distinction explicit in their UI. --- ## Client and configuration URL: /docs/api/client Configure Viby, bind application identity, choose execution, and close resources safely. # Client and configuration `createViby()` creates the long-lived server-side SDK client. Configure infrastructure once, then derive a tenant/user-scoped client for each authenticated operation. ## `createViby(config)` ```ts import { createViby } from "@viby/sdk"; import { openai } from "@ai-sdk/openai"; const viby = createViby({ framework: "farmjs", model: openai("your-model-id"), storage: { // Omit database to use DATABASE_URL. artifacts, connections, secrets, }, skills: { design: ["farming-labs/design-engineer"], }, }); ``` The function validates mutually exclusive configuration, opens the selected storage and adapter boundaries, and returns `Viby`. It does not run database migrations or contact the model. Configuration failures throw `ConfigurationError` before work begins. ## `VibyConfig` | Field | Type | Behavior | | -------------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `framework` | `FrameworkId` | Required single target string. It is persisted with chats and versions and preserved in the generic return type. | | `model` | AI SDK `LanguageModel` | Convenient default generation path. Mutually exclusive with `engine`. | | `models` | record of `LanguageModel` | Optional request-selectable aliases. `default` is reserved for the top-level model. | | `engine` | `GenerationEngine` | Provider-neutral replacement for the default agent/model runtime. Mutually exclusive with `model`. | | `engines` | record of `GenerationEngine` | Optional request-selectable engine aliases. `default` is reserved. | | `storage.database` | `DatabaseAdapter \| PersistenceAdapter` | Structured durable records. Omit it to open PostgreSQL from `DATABASE_URL`. | | `storage.artifacts` | `ArtifactStore` | Binary attachments, project entries, generated outputs, screenshots, and prepared deployment archives. | | `storage.connections` | `IntegrationConnectionStore` | Public provider connection metadata and authorization state. PostgreSQL is the default. | | `storage.secrets` | `SecretStore` | Provider credentials and secret project variables. The default requires `VIBY_SECRET_KEY`. | | `skills` | `SkillGroups` | Default categorized skill references included in generation configuration. | | `skillResolver` | `SkillResolverAdapter` | Resolves application-owned catalogs or opaque locators into immutable skill files. | | `tools` | `ToolSourcesConfig` | Static sources, durable source adapters, selection, and effect policy. | | `sandbox` | `SandboxAdapter` | Optional source execution and preview infrastructure. | | `sandboxPolicy` | `SandboxCommandPolicy` | Authorizes or rejects normalized commands before any provider sees them. | | `preview` | `PreviewConfig` | Framework server command, port, environment, timeout, and readiness behavior. | | `browser` | `BrowserAdapter` | Optional browser inspection and visual evaluation boundary. | | `environment` | `EnvironmentConfig` | Enables durable per-chat project variables and secret injection. | | `integrations` | `VibyIntegrations` | Repository and deployment adapters grouped by capability. | | `deployment.preparation` | `DeploymentPreparationConfig` | Install/build/output contract for providers that require prebuilt files. | | `generation.execution` | `"embedded" \| "worker"` | Defaults to `embedded`; `worker` leaves new attempts queued. | | `generation.quality` | `GenerationQualityConfig` | Optional provider-neutral preparation and quality commands that must pass before generated source becomes an immutable version. | | `generation.workspace` | `{ preview: "eager" }` | Opens the base version in one reusable sandbox while generation runs, emits the preview as soon as it is ready, and hands that workspace to final quality checks. | | `retention.deletedChatsMs` | `number \| null` | Default soft-delete retention. `null` keeps tombstones indefinitely; `0` allows immediate purge. | | `events.sinks` | `OutboundEventSink[]` | Named outbound delivery targets. Delivery is explicit and durable. | | `telemetry` | `VibyTelemetry` | Provider-neutral spans and metrics. Telemetry failures are fail-open. | | `cost` | `GenerationCostConfig` | Host-owned cost calculator stored in integer micro-units. | The deprecated top-level `persistence`, `artifactStore`, `connectionStore`, and `secretStore` aliases remain for compatibility. Do not configure an alias and its corresponding `storage.*` field at the same time. ## Model or generation engine Use `model` for the built-in bounded AI SDK agent. Use `engine` when the application supplies its own agent, orchestration runtime, or model protocol: ```ts import { defineGenerationEngine } from "@viby/sdk/core"; const engine = defineGenerationEngine({ id: "company-agent", provider: "internal", model: "frontend-v3", async generate(input, options) { return companyAgent.generate(input, options); }, }); const viby = createViby({ framework: "farmjs", engine }); ``` Concrete provider and model identity are stored on each attempt. Model or engine objects and their credentials are never persisted. ### Multiple models from one provider Use `modelsFrom()` when one AI SDK provider instance should expose several models. `default` becomes the top-level model and the remaining keys become stable aliases accepted by generation requests: ```ts import { createViby, modelsFrom } from "@viby/sdk"; import { createOpenAI } from "@ai-sdk/openai"; const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY }); const viby = createViby({ framework: "farmjs", ...modelsFrom(openai, { default: "gpt-5.6-sol", terra: "gpt-5.6-terra", luna: "gpt-5.6-luna", fast: "gpt-5.6-sol-fast", }), }); ``` The provider instance still owns authentication, requests, streaming, tools, and usage reporting. Viby receives ordinary AI SDK `LanguageModel` objects and stores only their provider/model identity on durable attempts. Pass `model: "terra"` when creating or retrying a generation to select an alias. The same helper accepts another provider instance without changing Viby configuration: ```ts import { anthropic } from "@ai-sdk/anthropic"; const claudeModels = modelsFrom(anthropic, { default: "claude-sonnet-4-6", opus: "claude-opus-4-6", }); ``` Spreading `claudeModels` into `createViby()` makes Sonnet the default and exposes `opus` as a request alias. Viby invokes the returned AI SDK model objects internally; it does not call the provider instance through a separate private integration. ## `Viby` | Member | Returns | Behavior | | ----------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------- | | `framework` | `Framework` | The configured framework identifier. | | `integrations` | `IntegrationClient` | Unscoped callback and adapter-management surface. User operations should use `ScopedViby.integrations`. | | `toolSources` | `ToolSourceAuthorizationCallbacks` | Public provider callback completion surface for durable tool registrations. | | `forUser(scope)` | `ScopedViby` | Validates non-empty identity and returns a tenant/user-bound client. | | `worker(options)` | `GenerationWorker` | Creates a durable worker restricted to this client's framework and model/engine identities. | | `close(options?)` | `Promise` | Aborts local runs and closes owned resources. `preserveSandboxes` keeps durable preview leases running. | ## `ScopedViby` ```ts const user = viby.forUser({ tenantId, userId }); ``` | Property | Purpose | | -------------- | ---------------------------------------------------------- | | `scope` | The validated identity pair used by every child operation. | | `chats` | Create, import, search, restore, and purge projects. | | `generations` | Rehydrate a durable generation by ID. | | `sandboxes` | Read and reconnect durable sandbox leases. | | `previews` | Read, reconnect, stop, list, and clean preview sessions. | | `toolSources` | Manage durable tool-source registrations and connections. | | `integrations` | User-scoped repository and deployment providers. | Scoped clients are lightweight and do not own independent database connections. A resource outside the scope resolves to `NotFoundError`. ## `GenerationWorker` ```ts const worker = viby.worker({ id: "generation-worker-eu-1", concurrency: 4, leaseMs: 30_000, heartbeatMs: 10_000, pollIntervalMs: 500, }); ``` | Member | Behavior | | ---------------------- | ------------------------------------------------------------------------------------ | | `id` | Stable host-provided worker identity recorded on claimed attempts. | | `running` | Reports whether `run()` currently owns its processing loop. | | `runOnce({ signal? })` | Claims and processes at most one eligible attempt; returns whether work was claimed. | | `run({ signal? })` | Runs the polling loop until aborted or stopped, respecting configured concurrency. | | `stop()` | Stops accepting work and waits for the active worker loop to settle. | Workers claim with leases and heartbeat while running. Lease tokens fence writes; a stale worker cannot append output after another worker safely reclaims an expired attempt. ## Shutdown behavior Call `viby.close()` once per root client. It stops embedded runs, previews, sandboxes, tool sources, integrations, environment stores, and persistence resources that Viby owns. The host remains responsible for shutting down its HTTP server, scheduler, telemetry exporter, and application-owned adapter clients. Request-scoped durable generation workers can preserve sandbox-backed previews while still closing their local stores and provider clients: ```ts await viby.close({ preserveSandboxes: true }); ``` Use this only after the worker has handed its sandbox to a durable preview. The default remains `false`, so normal application shutdown still stops every locally tracked preview and sandbox. --- ## Package entry points URL: /docs/api/entry-points Choose the portable core, full client, provider adapters, and conformance suites without accidental runtime coupling. # Package entry points `@viby/sdk` publishes explicit entry points so portable application code does not accidentally load filesystem, process, PostgreSQL, Docker, browser-driver, or provider SDK dependencies. ## Primary entry points | Import | Use it for | | --- | --- | | `@viby/sdk` | Backward-compatible full Node client, public types, core helpers, and high-level resources. | | `@viby/sdk/core` | Web-standard contracts and helpers shared with browsers, Workers, Bun, or Node. No Node built-ins are reachable from this graph. | | `@viby/sdk/node` | Explicit full Node client and Node-owned defaults. Recommended for new server applications. | | `@viby/sdk/node/migrations` | Programmatic PostgreSQL migration helpers. The `viby db migrate` CLI is the usual deploy-time path. | | `@viby/sdk/node/skills` | Local filesystem and skills.sh/GitHub skill resolution. | | `@viby/sdk/package.json` | Package metadata for tooling. | The portable core uses Web `Request`, `Response`, `Headers`, `ReadableStream`, `AbortSignal`, `Uint8Array`, text encoding, structured cloning, and Web Crypto-compatible shapes. It also includes pure product helpers such as `titleFromPrompt()` that do not require a model or runtime-specific APIs. ## Storage and persistence | Import | Exports and behavior | | --- | --- | | `@viby/sdk/storage/postgres` | `postgres(...)`, the explicit structured-database adapter using PostgreSQL. | | `@viby/sdk/persistence` | Provider-neutral durable `PersistenceAdapter` contract. | | `@viby/sdk/persistence/postgres` | Low-level PostgreSQL persistence implementation. | | `@viby/sdk/persistence/conformance` | Reusable durable adapter lifecycle and isolation tests. | | `@viby/sdk/artifact/filesystem` | Local filesystem artifact store for development and single-host deployments. | | `@viby/sdk/storage/s3` | S3-compatible artifact store for AWS S3, Cloudflare R2, MinIO, and compatible services. | | `@viby/sdk/artifact/s3` | Alias of the same S3-compatible artifact adapter. | | `@viby/sdk/artifact/conformance` | Reusable artifact-store immutability, checksum, scope, and deletion tests. | | `@viby/sdk/environment/postgres` | PostgreSQL project-environment metadata store. Secret bytes still use `SecretStore`. | The S3 adapter requires optional peer `@aws-sdk/client-s3`. ## Sandboxes | Import | Optional peer / environment | | --- | --- | | `@viby/sdk/sandbox/e2b` | `e2b` | | `@viby/sdk/sandbox/vercel` | `@vercel/sandbox` | | `@viby/sdk/sandbox/docker` | Docker CLI and daemon | | `@viby/sdk/sandbox/daytona` | `@daytona/sdk` | | `@viby/sdk/sandbox/modal` | `modal` | | `@viby/sdk/sandbox/cloudflare` | `@cloudflare/sandbox` and configured Worker bindings | | `@viby/sdk/sandbox/conformance` | No provider peer; pass a caller-owned disposable adapter fixture. | Provider SDK types stay inside their adapter configuration and never enter `SandboxAdapter`. ## Browser and generation conformance | Import | Use it for | | --- | --- | | `@viby/sdk/browser/playwright` | Playwright browser adapter with screenshot, DOM, console, readiness, and accessibility support. | | `@viby/sdk/browser/conformance` | Reusable browser lifecycle and normalized-result tests. | | `@viby/sdk/generation/conformance` | Reusable conformance checks for custom provider-neutral generation engines. | The Playwright adapter requires `playwright`; accessibility scans additionally use optional `@axe-core/playwright`. ## Integration entry points | Import | Use it for | | --- | --- | | `@viby/sdk/integrations/postgres` | PostgreSQL connection metadata and encrypted secret-store defaults. | | `@viby/sdk/integrations/conformance` | Connection-store and authorization lifecycle conformance. | | `@viby/sdk/integrations/repository/conformance` | Disposable repository discovery, source, push, conflict, and PR tests. | | `@viby/sdk/integrations/deployment/conformance` | Disposable project create, deploy, lookup, idempotency, and cancellation tests. | | `@viby/sdk/integrations/github` | GitHub repository adapter. | | `@viby/sdk/integrations/bitbucket` | Bitbucket Cloud repository adapter. | | `@viby/sdk/integrations/vercel` | Vercel deployment adapter. | | `@viby/sdk/integrations/cloudflare` | Cloudflare Pages deployment adapter. | ## MCP entry points | Import | Direction | | --- | --- | | `@viby/sdk/tools/mcp` | Consumes host-configured MCP servers as generation tool sources. Requires optional `@modelcontextprotocol/client`. | | `@viby/sdk/mcp` | Exposes a supplied `ScopedViby` as MCP server tools. Requires optional `@modelcontextprotocol/server`. | ## Runtime rules - Import types and Web helpers from `@viby/sdk/core` in portable/shared modules. - Import the full client from `@viby/sdk/node` in new Node-only server code. - Import providers only from their explicit subpaths. - Install optional peers only for adapters the application actually configures. - Do not deep-import files under `dist`; they are not part of the compatibility contract. - Run the relevant conformance suite for every custom adapter implementation. See [Runtime boundaries](/docs/runtime) for compatibility gates and supported runtimes. --- ## Errors URL: /docs/api/errors Typed SDK and Web-client failures, stable codes, safe metadata, and recovery guidance. # Errors Direct SDK failures extend `VibyError`. Each error has a stable `code` and a safe message suitable for application logs. Provider credentials, prompts, source, secret values, and raw provider responses are not copied into ordinary error records. ## Base error ```ts try { await generation.retry(); } catch (error) { if (error instanceof VibyError) { logger.warn({ code: error.code }, error.message); } } ``` Use classes for local TypeScript narrowing and `code` when crossing a process or HTTP boundary. ## Configuration and data | Error | When it occurs | Recommended handling | | --- | --- | --- | | `ConfigurationError` | Invalid or mutually exclusive config, identifiers, prompts, source shapes, or bounds. | Treat as a developer or validation error; do not retry unchanged input. | | `DatabaseNotReadyError` | The configured database does not contain current Viby migrations. | Run `viby db migrate` in the release environment before serving traffic. | | `NotFoundError` | A resource is missing, deleted, or outside the current tenant/user scope. | Return a generic not-found response without revealing cross-scope existence. | | `SkillResolutionError` | A skill locator cannot be resolved or validated safely. | Fix the catalog/path/reference or remove it; inspect the safe locator and cause. | | `SourceImportError` | A provider-neutral import adapter failed. | Surface a retry option; log the adapter ID and protected cause server-side. | ## Generation lifecycle | Error | When it occurs | Recommended handling | | --- | --- | --- | | `GenerationError` | The synchronous convenience path observed a failed durable generation. | Use `generationId` to load attempts/events and offer retry. | | `GenerationCancelledError` | The synchronous convenience path observed cancellation. | Show the recorded cancellation state; retry only on explicit user intent. | | `GenerationStateError` | Cancel, retry, resume, or resolution is invalid for the current state. | Refresh `generation.data()` and render valid actions for that state. | | `GenerationTaskRequiredError` | `chat.generate()` reached typed blocking tasks. | Load the generation by `generationId`, present `taskIds`, resolve, and continue. | Prefer `chat.start()` plus `Generation.wait()` in interactive products because the outcome union makes waiting, failure, and cancellation explicit without exceptions. ## Integrations and tool sources | Error | When it occurs | Recommended handling | | --- | --- | --- | | `IntegrationAuthorizationError` | Authorization state, callback, exchange, refresh, or revocation is invalid. | Restart the connection flow; do not reuse callback state. | | `IntegrationConnectionRequiredError` | A repository/deployment operation has no healthy selected connection. | Call the scoped category's `connect()` and resume after authorization. | | `IntegrationOperationError` | A connected provider operation failed. | Use category/provider/operation for safe logs; apply idempotent retry policy. | | `ToolSourceAuthorizationError` | A durable tool-source connection flow failed. | Restart authorization for the registration. | | `ToolSourceConnectionRequiredError` | A selected source needs a usable connection. | Connect or disable the source before generating again. | ## Sandbox and browser | Error | When it occurs | Recommended handling | | --- | --- | --- | | `SandboxUnavailableError` | No sandbox is configured or a required capability is missing. | Disable the product action or configure a compatible adapter. | | `SandboxCommandDeniedError` | Policy denied a normalized command. | Show the safe reason; changing provider does not bypass policy. | | `SandboxCommandApprovalRequiredError` | Policy requires explicit approval. | Persist/present `proposedAction`, then resume only with an approved grant. | | `SandboxError` | A provider operation failed. | Log provider and operation, clean up the lease, and retry only when safe. | | `BrowserError` | Navigation, screenshot, DOM, console, accessibility, or readiness failed. | Log provider/operation and keep the source version unchanged. | | `PreviewError` | Preview startup, readiness, reconnect, or stop failed. | Load `previewId` when present; the failure is already durable. | ## Outbound delivery | Error | When it occurs | Recommended handling | | --- | --- | --- | | `OutboundEventSinkError` | The sink transport rejected or failed one event. | Inspect the durable delivery record; do not change generation state. | | `OutboundEventDeliveryError` | Delivery stopped at a specific cursor. | Resume after `lastDeliveredCursor`; redrive only explicit dead letters. | | `OutboundEventSignatureError` | Signature, timestamp, key, or body verification failed. | Reject the request and do not parse it as a trusted event. | ## Web client errors `createVibyWebClient()` uses three transport-specific errors: | Error | Meaning | | --- | --- | | `VibyApiClientError` | A non-success HTTP response with `status`, API `code`, message, and parsed safe body. | | `VibyStreamDisconnectedError` | SSE disconnected more than the configured reconnect budget; includes the last cursor and reconnect count. | | `VibyStreamProtocolError` | The server returned malformed SSE or event JSON. | Network exceptions from the supplied `fetch` implementation are not wrapped. Products may therefore distinguish transport failure from a typed Viby API response. ## HTTP mapping `createVibyApi()` converts known errors to JSON with an appropriate status and stable `code`. It preserves product `Response` objects returned by authentication or preview callbacks. Unexpected errors become a generic server response; raw exception details remain server-side. --- ## Generations and events URL: /docs/api/generations Durable generation states, attempts, resumable streams, tasks, workers, artifacts, and recovery. # Generations and events A `Generation` is an addressable logical request. Attempts are immutable executions appended for initial work, retry, resume, or task resolution. ## Start a generation ```ts const generation = await chat.start({ prompt: "Add a billing settings page", model: "fast", instructions: "Preserve the existing navigation contract.", skills: { security: ["company/skills/billing-security"] }, }); ``` `chat.start()` commits the generation before execution begins. `user.generations.get(id)` or `chat.getGeneration(id)` can rehydrate the same handle after a request, reload, or process restart. ## Status model | Generation status | Meaning | | --- | --- | | `queued` | Durable work exists but no active attempt currently owns execution. | | `running` | An embedded runner or leased worker owns the active attempt. | | `waiting` | A typed plan, question, or permission task must be resolved. | | `succeeded` | A final assistant message and immutable version were committed. | | `failed` | The active attempt ended with a durable safe error. | | `cancelled` | Cancellation was committed with a reason. | Attempt statuses also include `interrupted`. Retrying, resuming, or resolving a task never overwrites an older attempt. ## `GenerationCollection` | Method | Behavior | | --- | --- | | `get(id)` | Loads an in-scope generation by ID and returns a live handle over its durable state. | ## `Generation` ### Read state | Method | Returns | Behavior | | --- | --- | --- | | `data()` | `GenerationData` | Current logical status, active attempt, configuration, base/result versions, usage, cost, and timestamps. | | `attempts()` | `GenerationAttemptData[]` | Immutable execution history in order. | | `tasks()` | `GenerationTaskData[]` | All proposed and resolved typed tasks for the generation. | | `toolCalls()` | `ToolCallData[]` | Redacted durable arguments, results, ownership, status, and idempotency. | | `artifacts()` | `GeneratedArtifactData[]` | Metadata for generated images, audio, video, documents, or binary outputs. | | `getArtifact(id)` | `GeneratedArtifactContent` | Checksum-verifies and returns one scoped artifact's bytes. | | `events({ after?, limit? })` | `GenerationEventPage` | Reads one durable event page after an opaque cursor. | | `outboundDeliveries({ sink, status? })` | delivery records | Inspects persisted delivery attempts and dead letters for one configured sink. | ### Subscribe and wait | Method | Behavior | | --- | --- | | `stream({ after?, pollIntervalMs?, signal? })` | Yields durable events after the cursor and ends at `waiting`, `succeeded`, `failed`, or `cancelled`. Aborting only stops this subscriber. | | `toEventStreamResponse(options?)` | Returns a Web-standard SSE `Response`, reading `Last-Event-ID` unless `after` is explicit. | | `wait({ pollIntervalMs?, signal? })` | Waits for a terminal or waiting state and returns a discriminated `GenerationOutcome`. Aborting does not cancel work. | ```ts for await (const event of generation.stream({ after: lastCursor })) { lastCursor = event.cursor; await persistAcknowledgedCursor(lastCursor); publishToUi(event); } ``` Events are persisted in cursor order. Lifecycle events, structured output, usage, generated artifacts, and agent trace parts share the same cursor; no provider stream ID is required for recovery. ### Control execution | Method | Valid states and behavior | | --- | --- | | `cancel(reason?)` | Commits cancellation for active work, then aborts a local model signal when present. Repeated cancellation returns durable state. | | `retry()` | Appends a retry attempt for failed or cancelled work and schedules it according to execution mode. | | `resume()` | Appends a resume attempt for waiting or orphaned queued/running work. An unexpired worker lease cannot be stolen. | | `resolve(input)` | Validates a task-specific resolution, commits it with a new attempt, and resumes the same logical generation. | Invalid transitions throw `GenerationStateError`. `chat.generate()` translates waiting to `GenerationTaskRequiredError`, failure to `GenerationError`, and cancellation to `GenerationCancelledError`. ## Typed tasks | Kind | Request | Resolution | | --- | --- | --- | | `plan` | Proposed ordered steps | `approve`, or `revise` with feedback | | `question` | Prompt plus optional choices | An explicit answer | | `permission` | Proposed effect and permissions | `allow` or `deny`, with optional note | Permission tasks may include a normalized sandbox or tool action. Secret values are omitted from the proposal. Resolving approval reuses the proposed action's idempotency identity so a completed external effect is not silently repeated. ## Agent trace events Trace parts use four event phases: - `part.started` assigns a stable part ID, type, and position; - `part.delta` appends incremental content for that ID; - `part.completed` stores the typed final part on the assistant message; - `part.failed` records a safe error and retryability without creating a final message part. Supported final message-part types are `text`, `status`, `reasoning-summary`, `file-read`, `file-edit`, `search`, `command`, `tool-call`, `error`, and `usage`. Completed `file-edit` parts distinguish `create`, `update`, `delete`, and `move` operations. The legacy `write` operation remains part of the readable contract for older persisted messages. ## Outbound delivery `deliverEvents({ sink, after?, limit?, retry?, signal? })` sends one ordered page through a configured sink, persisting claims, receipts, retry schedule, and dead-letter state. It returns delivered records, the safe resume cursor, `hasMore`, dead letters, and the earliest retry time. `redriveOutboundEvent({ sink, cursor })` explicitly makes one dead letter eligible again. Delivery failure never changes generation state. Receivers must deduplicate stable event IDs because a remote endpoint may accept a request before the sender observes a transport failure. ## Generated-source quality gates Configure provider-neutral quality commands when generated source must install, typecheck, test, or build before it becomes an immutable version: ```ts const viby = createViby({ framework: "farmjs", model, sandbox, preview: { prepare: [{ command: "pnpm", args: ["install", "--prefer-offline"] }], start: { command: "pnpm", args: ["dev", "--host", "0.0.0.0"] }, port: 3000, }, generation: { workspace: { preview: "eager" }, quality: { prepare: [{ id: "install", command: "pnpm", args: ["install", "--prefer-offline"] }], checks: [ { id: "typecheck", command: "pnpm", args: ["typecheck"] }, { id: "build", command: "pnpm", args: ["build"] }, ], checkConcurrency: 2, }, }, }); ``` With `workspace.preview: "eager"`, Viby opens the immutable base version immediately, starts preview preparation while the model works, and emits `workspace.started`, `workspace.prepared`, `preview.ready`, or `preview.failed` through the normal resumable generation cursor. The preview URL does not wait for typecheck or production build. Viby synchronizes the final candidate into the same sandbox, runs preparation, and executes up to `checkConcurrency` independent checks concurrently. The durable preview record is then retargeted to the committed version without reinstalling or restarting its server. Without an eager workspace, quality gates continue to use a fresh sandbox. In either mode, Viby commits only after every check succeeds. Failures end the attempt without persisting a partial version. Command output is deliberately not persisted because it may contain secrets, and the configured command policy applies to every preview and quality command. ## Worker execution Set `generation: { execution: "worker" }`, then run a worker in any suitable process: ```ts const worker = viby.worker({ id: "worker-1", concurrency: 4 }); await worker.run({ signal }); ``` Claims are database-backed, scoped to compatible framework and model/engine identities, and fenced with lease tokens. The runtime provides at-least-once execution; provider effects must remain idempotent. --- ## Integrations URL: /docs/api/integrations Connect repository and deployment providers through scoped authorization and durable histories. # Integrations Integrations are grouped by product capability: `repository` and `deployment`. Provider credentials, options, SDK clients, and account semantics remain inside adapters; application workflows use the same Viby handles across providers. ## Configure providers ```ts const viby = createViby({ framework: "farmjs", model, integrations: { repository: { github: github({ /* provider app configuration */ }), bitbucket: bitbucket({ /* OAuth consumer configuration */ }), }, deployment: { vercel: vercel({ /* integration configuration */ }), cloudflare: cloudflare({ /* OAuth configuration */ }), }, }, }); ``` Omit a category or provider when the product does not offer it. Use the provider-specific guides for registration fields and callback URLs. ## Connection lifecycle User-scoped categories expose the same methods: | Method | Behavior | | --- | --- | | `list()` | Lists configured adapters with redacted connections and a computed `connected` flag. | | `connections(integrationId?)` | Lists scoped durable connection metadata, optionally for one provider. | | `connect(integrationId, input)` | Returns a usable existing connection or starts authorization and returns its URL/expiry. | | `disconnect(integrationId, options?)` | Revokes remotely when supported, deletes secret material, and records local revocation. | | `use(integrationId, { connectionId? })` | Creates a repository or deployment handle; provider work resolves/refreshes credentials just in time. | `connect` requires an absolute callback URL and a bounded return path. It also accepts optional scopes, `force`, and a provider-neutral `authorization` preference. Set `authorization.account` to `"existing"` when reconnecting an account or `"new"` when provisioning one; `externalAccountId` can carry a provider identifier selected by the product. Adapters that do not distinguish those paths may treat the preference as a hint. Callback state is hashed, expiring, and single use. Complete repository and deployment callbacks through `viby.integrations.callback(request)`, then redirect only to an application- allowlisted `returnTo` value. ## Repository handles ```ts const repository = user.integrations.repository.use("github"); ``` | Surface | Methods and behavior | | --- | --- | | `owners` | `list()` cursor-pages organizations, workspaces, or personal owners. | | `repositories` | `list()`, `get()`, and `create()` provider-neutral repositories. | | `branches` | `list()`, `get()`, and `create()` branch records. | | `pullRequests` | `create()` and `merge()` provider-neutral pull requests. | | handle | `connect()`, `disconnect()`, `source()`, `readSource()`, `pushSource()`, and advanced `run()`. | `source(input)` creates a safe descriptor for `user.chats.import({ source })`; credentials remain in the handle. `version.push()` is the preferred high-level publish operation because it writes durable pending/history records and uses the immutable version as its complete source snapshot. ## Deployment handles ```ts const deployment = user.integrations.deployment.use("vercel"); ``` | Surface | Methods and behavior | | --- | --- | | handle metadata | `id`, `provider`, `displayName`, `sourceMode`, and optional `outputDirectory`. | | `projects` | `list()`, `get()`, and `create()` provider-neutral projects. | | `deployments` | `get()` refreshes one provider deployment; `cancel()` requests cancellation. | | handle | `connect()`, `disconnect()`, `deploySource()`, and advanced `run()`. | `version.deploy()` is the preferred high-level method because it prepares the correct source form, persists project links and deployment records, applies stable idempotency, and observes status transitions. ## Source and prebuilt deployment modes An adapter declares `sourceMode`: - `source` receives the complete immutable framework source tree; - `prebuilt` receives files collected from the configured sandbox build output. For prebuilt deployments Viby materializes the version, resolves the selected project environment, runs the declarative install/build commands, stores an immutable ZIP through `storage.artifacts`, and passes the collected files to the provider. Command records contain environment names, not values. Raw `version.download()` output remains unchanged. ## Durable history and idempotency Repository pushes and deployments write a pending record before contacting the provider. The record captures scope, chat, immutable version, provider, selected connection, target, idempotency key, status, result identity, safe error, and timestamps. Repeated completed keys replay durable results. Provider refresh and cancellation append deployment status transitions rather than erasing observations. History methods are database reads and remain available after restarts. ## Direct provider operations Low-level discovery and administrative calls are available for selection UIs. Prefer high-level `version.push()` and `version.deploy()` for product effects. The `run()` escape hatch is intentionally advanced: it resolves a short-lived credential and passes the adapter plus operation context to a callback without adding a Viby history record for arbitrary provider work. ## Included adapters | Capability | Entry point | Guide | | --- | --- | --- | | GitHub repositories | `@viby/sdk/integrations/github` | [GitHub](/docs/integrations/github) | | Bitbucket repositories | `@viby/sdk/integrations/bitbucket` | [Bitbucket](/docs/integrations/bitbucket) | | Vercel deployments | `@viby/sdk/integrations/vercel` | [Vercel](/docs/integrations/vercel) | | Cloudflare Pages deployments | `@viby/sdk/integrations/cloudflare` | [Cloudflare](/docs/integrations/cloudflare) | Custom stores and adapters should run the integration, repository, or deployment conformance suite from the corresponding package entry point before production use. --- ## SDK reference URL: /docs/api The public TypeScript client, lifecycle methods, adapters, errors, and package entry points. # SDK reference The reference is organized around the objects an application uses at runtime. Signatures show the stable public shape; behavior sections explain durability, scope, side effects, and failure modes. ## Core client - [Client and configuration](/docs/api/client) — `createViby`, `VibyConfig`, user scope, workers, storage, models, skills, and shutdown. - [Chats and projects](/docs/api/chats) — create, import, search, update, delete, restore, messages, project environments, and history. - [Generations and events](/docs/api/generations) — start, stream, wait, cancel, retry, resume, tasks, artifacts, outbound delivery, and workers. - [Versions and artifacts](/docs/api/versions) — source inspection, immutable changes, iteration, restore, fork, preview, evaluation, push, deploy, and download. ## Optional capabilities - [Previews and sandboxes](/docs/api/previews) — sessions, leases, command policy, process readiness, reconnect, and cleanup. - [Tool sources](/docs/api/tool-sources) — static tools, durable registrations, per-chat selection, authorization, permission policy, and redaction. - [Integrations](/docs/api/integrations) — provider-neutral repository and deployment connections plus durable effect history. ## Hosting and compatibility - [Web API host](/docs/api-host) — all framework-neutral HTTP routes and the typed Web client. - [Errors](/docs/api/errors) — stable error families and safe handling guidance. - [Package entry points](/docs/api/entry-points) — portable core, Node client, providers, optional peers, and conformance suites. - [Complete v1 contract](/docs/api/v1) — normative persistence and lifecycle details in one page. ## Reading the reference All resource IDs are opaque strings. All timestamps returned by the direct TypeScript client are `Date` objects. Cursor values are opaque strings and must be stored and returned without parsing. Methods that contact a model, sandbox, browser, tool server, repository, or deployment provider are server-side operations unless a product intentionally exposes them through its own authenticated API. Every scoped operation enforces both `tenantId` and `userId`; a resource owned by another scope is reported as not found rather than revealed. --- ## Previews and sandboxes URL: /docs/api/previews Run immutable source safely with provider-neutral capabilities, policies, leases, and readiness. # Previews and sandboxes Sandboxes execute one immutable version. Previews add a durable lifecycle around a long-running sandbox process and its reachable URL. ## Configure a sandbox and preview ```ts const viby = createViby({ framework: "farmjs", model, sandbox, sandboxPolicy: sandboxCommandPolicy({ allowCommands: ["npm", "pnpm", "npx"], maxTimeoutMs: 120_000, }), preview: { prepare: [{ command: "pnpm", args: ["install", "--prefer-offline"] }], start: { command: "pnpm", args: ["dev", "--host", "0.0.0.0"] }, port: 3000, environment: "preview", env: { CI: "1" }, readiness: { path: "/", timeoutMs: 60_000 }, }, }); ``` The preview command is framework or product configuration. Viby does not infer package-manager or framework commands. `env` supplies safe host defaults to every preview; values passed to `version.preview({ env })` override those defaults for that opening only. Set `generation.workspace: { preview: "eager" }` to open the base version while a generation runs. The generation stream emits the preview URL as soon as preparation and readiness finish. Source edits, final dependency reconciliation, and quality checks continue in that same sandbox. After the new immutable version commits, Viby retargets the durable preview record without restarting the process or creating a second lease. ## Capability discovery Every adapter declares the same capability record: - `files` — materialize and read project files; - `commands` — run foreground commands; - `commandStreaming` — emit stdout/stderr while a command runs; - `portUrls` — resolve an externally reachable URL for a port; - `backgroundProcesses` — start, wait for, and kill long-running work; - `reconnect` — restore an adapter instance from a durable provider ID; - `snapshots` — provider snapshot support where implemented. Use `session.supports(name)` instead of branching on the provider string. ## `SandboxCollection` | Method | Behavior | | --- | --- | | `get(leaseId)` | Loads one scoped durable lease record. | | `reconnect(leaseId, options?)` | Validates lease status and provider compatibility, reconnects the instance, and returns `SandboxSession`. | ## `SandboxSession` | Member | Behavior | | --- | --- | | `id` / `provider` / `leaseId` | Provider instance identity, adapter identity, and nullable durable Viby lease. | | `capabilities` / `supports(name)` | Normalized capability discovery. | | `stopped` | Whether this session has been stopped locally. | | `writeFiles(files, options?)` | Writes normalized text or binary paths when file capability exists. | | `readFile(path, options?)` | Reads one file as `Uint8Array`. | | `authorizeCommand(command, action?)` | Evaluates policy and returns a grant or throws a typed denial/approval error. | | `run(command, grant?)` | Executes one bounded foreground command and returns exit code, output, and duration. | | `start(command, grant?)` | Starts a background process when supported. | | `url(port)` | Resolves a provider URL when supported. | | `waitForPort(port, options?)` | Polls readiness with an optional host-supplied check and returns the resolved URL. | | `stop(options?)` | Idempotently stops the provider instance and closes its durable lease. | Commands use separate `command` and `args` values. Policy receives the executable, arguments, working directory, environment variable names, timeout, action, and immutable version context; secret values are omitted. ## Command policy Policy returns `allow`, `deny`, or `approval-required`. `sandboxCommandPolicy(options)` is a convenient allow/deny implementation; applications may supply an async policy backed by their own authorization system. `approval-required` throws `SandboxCommandApprovalRequiredError` with a serializable `proposedAction`. The default agent converts that proposal into a durable permission task. An approved action key can then be resumed safely without bypassing policy for unrelated commands. ## `PreviewCollection` | Method | Behavior | | --- | --- | | `get(id)` | Loads a scoped preview session. | | `list({ chatId?, versionId?, status? })` | Filters durable preview records. | | `cleanupExpired(limit?)` | Stops and marks a bounded set of expired sessions; intended for a host-owned schedule. | ## `Preview` | Member | Behavior | | --- | --- | | `id`, `chatId`, `versionId`, `framework` | Durable ownership and source identity. | | `status` | `starting`, `ready`, `failed`, `stopped`, or `expired`. | | `url` | Provider URL when ready, otherwise `null`. | | `data()` | Returns the complete durable preview record. | | `reconnect(signal?)` | Reconnects the underlying sandbox, re-runs readiness, and refreshes this handle. A non-aborted readiness failure stops the stale sandbox and persists `failed`. | | `stop(signal?)` | Idempotently stops the process/sandbox and persists `stopped`. | Starting a preview materializes source, starts the configured process, obtains a port URL, and waits for readiness. Any failure is persisted with a safe message before `PreviewError` is thrown. ## Included sandbox adapters | Entry point | Provider/runtime note | | --- | --- | | `@viby/sdk/sandbox/e2b` | E2B sandbox client; optional peer required. | | `@viby/sdk/sandbox/vercel` | Vercel Sandbox; optional peer required. | | `@viby/sdk/sandbox/docker` | Local Docker CLI/daemon; intended for trusted local or single-tenant use. | | `@viby/sdk/sandbox/daytona` | Daytona sandbox client; optional peer required. | | `@viby/sdk/sandbox/modal` | Modal sandbox client; optional peer required. | | `@viby/sdk/sandbox/cloudflare` | Cloudflare Worker binding and container runtime; optional peer required. | Use `@viby/sdk/sandbox/conformance` to verify a custom adapter against the provider-neutral lifecycle before using it with generated code. --- ## Tool sources URL: /docs/api/tool-sources Expose host-configured tools with durable selection, isolated credentials, approval policy, and idempotent effects. # Tool sources Tool sources let a generation discover and call host-configured capabilities without coupling Viby to a specific protocol. Sources may be process-configured, registered durably by a user, or supplied through the MCP client adapter. ## Core `ToolSource` ```ts const source = defineToolSource({ id: "product-catalog", async list(context) { return [{ name: "find_product", description: "Find a product by SKU", inputSchema: { type: "object", properties: { sku: { type: "string" } } }, effect: "read", }]; }, async call(call, context) { return catalog.find(call.arguments.sku); }, }); ``` `list(context)` returns tool definitions. `call(call, context)` returns bounded JSON. The context contains tenant, user, chat, generation, attempt, framework, metadata, and abort signal. It never contains a provider credential unless the application intentionally closes over one inside its adapter. Every tool declares a `read`, `write`, or `external` effect. External effects receive a stable idempotency key and must use it when contacting downstream systems. ## Configure static sources ```ts const viby = createViby({ framework: "farmjs", model, tools: { sources: { catalog: source }, select: ({ available, context }) => context.metadata.toolset === "catalog" ? ["catalog"] : [], policy: ({ tool }) => tool.effect === "read" ? "allow" : "approval-required", }, }); ``` When `select` is omitted, all configured sources are eligible. The default policy allows reads and requires approval for write/external effects. A policy may return `allow`, `deny`, or `approval-required` asynchronously. ## Durable registrations Register adapter types once, then let each scoped user create credential-free source records: ```ts const viby = createViby({ framework: "farmjs", model, tools: { adapters: { mcp: defineToolSourceAdapter({ type: "mcp", open: ({ source, credential }) => openCompanyMcp({ source, credential }), }), }, }, }); const registered = await user.toolSources.create({ type: "mcp", name: "Company tools", configuration: { endpoint: "https://tools.example.com/mcp" }, }); await chat.toolSources.set([registered.id]); ``` Configuration is JSON-only and rejects credential-like keys. Registrations are explicitly selected per chat and resolved into the same `ToolSource` contract as static sources. ## `RegisteredToolSourceCollection` | Method | Behavior | | --- | --- | | `create(input)` | Validates a configured adapter type and persists an active registration. | | `get(id)` | Loads one scoped registration. | | `list({ status?, type?, limit? })` | Lists registrations with bounded optional filters. | ## `RegisteredToolSource` | Member | Behavior | | --- | --- | | `id`, `type`, `name`, `status` | Current public identity and lifecycle state. | | `data()` | Returns the complete credential-free registration record. | | `update(input)` | Updates name, description, public configuration, or enabled state and refreshes the handle. | | `archive()` | Permanently archives the registration and removes it from chat selections. | | `connection()` | Returns redacted provider connection metadata or `null`. | | `connect(input)` | Returns a healthy existing connection or an authorization URL with expiry. | | `disconnect(signal?)` | Revokes when supported, removes secret material, and records local revocation. | ## `ChatToolSourceSelection` | Method | Behavior | | --- | --- | | `list()` | Returns active durable sources explicitly selected for the chat. | | `set(sourceIds)` | Atomically replaces the selection after validating every source belongs to this scope. | Static selection from `tools.select` and durable chat selection are combined by the configured runtime. Archived or disabled registrations cannot be exposed to a new generation. ## Authorization callbacks An adapter may define `startAuthorization`, `completeAuthorization`, optional refresh, and optional revocation. Start the flow from an authenticated request: ```ts const result = await registered.connect({ callbackUrl: "https://app.example.com/api/viby/tool-sources/callback", returnTo: "/settings/tools", }); ``` Complete it in the public callback route: ```ts const completed = await viby.toolSources.callback(request); return Response.redirect(new URL(completed.returnTo, appOrigin)); ``` Viby hashes bounded, expiring, single-use state and restores the original tenant/user/source scope. The provider credential is stored through `storage.secrets`; public connection data contains account, scope, status, and expiry only. The host must allowlist safe return paths before redirecting. ## Tool calls, approval, and redaction Arguments and results are validated as bounded JSON and redacted before persistence. Common password, token, authorization, cookie, credential, secret, private-key, and API-key fields become `[REDACTED]`; Viby does not retain an unredacted ordinary record. Denied tools are not called. Approval-required tools produce a durable permission task containing a safe proposed action. External effects require an idempotency key; duplicate starts return the existing tool-call record so a retry can reuse or reconcile its outcome. ## MCP adapter Import `mcp` from `@viby/sdk/tools/mcp`. It supports Streamable HTTP and application-supplied transports while keeping per-chat credentials inside the transport factory. Install the optional `@modelcontextprotocol/client` peer only when using this adapter. `@viby/sdk/mcp` is the opposite direction: it exposes selected Viby operations through an official MCP server. The supplied `ScopedViby` already establishes identity, so tenant/user IDs are never tool arguments. --- ## Complete v1 contract URL: /docs/api/v1 The normative Viby lifecycle, persistence, ownership, and provider-neutral behavior contract. # Viby v1 API This is the normative contract for the current Viby-native SDK. Viby v1 means the product API generation, even while the npm package is released under `0.x` during early development. For a method-oriented reference, start with the [SDK reference](/docs/api). ## Ownership boundary The host application owns: - authentication and authorization; - model-provider credentials and the selected AI SDK provider; - its user, organization, subscription, and product UI; - Postgres provisioning and `DATABASE_URL`; - sandbox and browser infrastructure, artifact-store provisioning, HTTP hosting, integration-app credentials, and redirect routes. Viby owns: - tenant- and user-scoped chat history; - durable logical generations, immutable attempts, ordered events, and usage; - immutable attempt cost and cumulative logical-generation cost when the host configures it; - typed plan, question, and permission tasks; - resolved skill snapshots; - immutable, parent-linked source versions; - framework-native source ZIPs and Web-standard stream/download responses; - durable binary and visual-artifact metadata with opaque external-store references; - durable sandbox-backed preview sessions bound to immutable versions; - immutable design evaluations linked to validated source, attachment, and visual evidence; - migrations for the dedicated `viby` schema. There is no Viby API key, managed Viby database, globally hosted preview URL, or deployment URL in v1. A configured sandbox adapter may produce a provider URL after the host starts a preview process. Configured repository and deployment integrations may use the durable connection lifecycle, but the host still owns each provider application, callback route, authorization policy, and provider adapter. ## Installation and environment ```bash npm install @viby/sdk ai npx viby db migrate ``` ```dotenv DATABASE_URL=postgresql://postgres:postgres@localhost:5432/viby VIBY_SECRET_KEY=base64-encoded-32-byte-key ``` The selected AI SDK provider reads its own environment variables. Viby records the provider and model IDs plus token usage, but never stores the provider credential. ## Client ```ts import { createViby, modelsFrom, skillRead } from "@viby/sdk"; import { openai } from "@ai-sdk/openai"; const viby = createViby({ framework: "farmjs", ...modelsFrom(openai, { default: "gpt-5.6-sol", fast: "gpt-5.6-sol-fast", }), skills: { core: [skillRead("./skills/product")], design: ["owner/repository/design-skill"], frontend: ["owner/repository/frontend-skill"], security: [], }, }); ``` ### `VibyConfig` ```ts interface VibyConfig { framework: Framework; // built-in autocomplete plus custom string values model: LanguageModel; // or engine: GenerationEngine models?: Readonly>; storage?: { database?: DatabaseAdapter | PersistenceAdapter; artifacts?: ArtifactStore; connections?: IntegrationConnectionStore; secrets?: SecretStore; }; integrations?: { repository?: Record; deployment?: Record; }; environment?: { store?: EnvironmentVariableStore }; deployment?: { preparation?: { install?: SandboxCommand; build: SandboxCommand; outputDirectory?: string; collectorCommand?: string; maxFiles?: number; maxBytes?: number; }; }; persistence?: PersistenceAdapter; // deprecated alias for storage.database artifactStore?: ArtifactStore; // deprecated alias for storage.artifacts connectionStore?: IntegrationConnectionStore; // deprecated alias secretStore?: SecretStore; // deprecated alias skills?: Record; skillResolver?: SkillResolverAdapter; tools?: { sources?: Readonly>; adapters?: Readonly>; select?: ToolSourceSelector; policy?: ToolSourcePolicy; }; sandbox?: SandboxAdapter; preview?: { files?: readonly SandboxFile[]; prepare?: readonly SandboxCommand[]; start: SandboxCommand; port: number; path?: string; environment?: EnvironmentName; env?: Readonly>; sandboxTimeoutMs?: number; readiness?: Omit; }; browser?: BrowserAdapter; sandboxPolicy?: SandboxCommandPolicy; generation?: { execution?: "embedded" | "worker"; workspace?: { preview: "eager" }; quality?: { prepare?: readonly GenerationQualityCommand[]; checks: readonly GenerationQualityCommand[]; checkConcurrency?: number; }; }; retention?: { deletedChatsMs?: number | null }; events?: { sinks?: readonly OutboundEventSink[] }; telemetry?: VibyTelemetry; cost?: GenerationCostConfig; } type SkillReference = | `${string}/${string}/${string}` | { source: "file"; path: string } | { source: "inline"; name: string; description?: string; files: readonly SkillFile[] } | { source: "resolver"; resolver: string; locator: string; metadata?: Record; }; ``` `framework` is one type-safe string, not an array. Built-in suggestions include `farm`, `tanstack-start`, and `next`, while custom framework strings remain legal. Framework-specific behavior is supplied through skills and the model prompt instead of a hard-coded `defineFramework` registry. `storage.database` owns structured durable Viby records. Omit it to select the built-in PostgreSQL adapter from `DATABASE_URL`, or use `postgres({ url })` from `@viby/sdk/storage/postgres` for explicit configuration. `storage.artifacts` owns binary bytes independently. A custom database factory defined with `defineDatabaseAdapter` receives the configured artifacts adapter through `open({ artifacts })`, while a raw `PersistenceAdapter` continues owning its own artifact references. `storage.connections` and `storage.secrets` override the integration stores. Legacy top-level aliases remain accepted but cannot be combined with their categorized equivalents. `s3(...)` from `@viby/sdk/storage/s3` is the production S3-compatible artifact implementation. It accepts a bucket plus optional region, endpoint, credentials, path-style, prefix, and server-side-encryption configuration. AWS S3 uses its normal endpoint; R2, MinIO, and compatible providers supply `endpoint`. The adapter stores opaque tenant/user-scoped keys, validates checksums before writes, preserves immutable existing objects, returns defensive byte copies, and treats missing reads and repeated deletes idempotently. `environment: {}` enables durable chat-scoped project variables with PostgreSQL metadata from `DATABASE_URL` and encrypted secret values from `storage.secrets` or the `VIBY_SECRET_KEY` default. `environment.store` replaces only the metadata adapter. A variable belongs to one type-safe environment string, allowing distinct development, preview, production, or custom values. Public records expose a secret flag but never the secret value or reference. `chat.environment.set`, `list`, and `delete` manage the public surface. `version.sandbox({ environment })` opts into just-in-time sandbox injection. `version.deploy({ environment })` resolves that environment automatically for prebuilt sandbox/build commands and the deployment adapter. Resolved values are runtime-only and must not enter prompts, messages, durable events, telemetry, histories, or command records. `model` is any AI SDK `LanguageModel`, so the application can choose a direct provider, a gateway, or its own compatible model implementation. `models` adds stable aliases for request-level selection. The top-level model is always addressed as `default`; model objects are never written to the database. `skills` are categorized. Built-in categories are `core`, `product`, `design`, `frontend`, `backend`, `data`, `ai`, `testing`, `security`, `accessibility`, `performance`, and `delivery`; custom categories are allowed. `skillRead(path)` points at a local `SKILL.md` file or its containing directory. A string uses the stable skills.sh-compatible `owner/repository/slug` form. `skillInline(...)` supplies an immutable in-memory snapshot. `skillFrom(resolver, locator, metadata?)` creates an opaque serializable reference for a custom `skillResolver`. Define the adapter with `defineSkillResolver({ id, resolve })`; its `resolve` method receives one reference plus the selected category and prompt, and returns exact skill files or `null` to use the next/built-in resolver. This allows application-owned catalogs, databases, object storage, and Git services without leaking their clients into the Viby contract. References and resolved snapshots are durable, so resolver metadata must never contain credentials. `sandbox` is an optional provider adapter. Viby passes only the selected immutable source snapshot and explicitly supplied environment values to it. Provider credentials stay in the adapter configuration. `sandboxPolicy` authorizes normalized foreground and background commands before an adapter receives them. It is shared by all providers and is optional for compatibility; products executing untrusted model actions should configure it. `integrations` groups external providers by portable product capability. The durable connection lifecycle uses tenant/user-scoped metadata, hashed single-use authorization state, bounded callbacks, refresh, and local revocation. `DATABASE_URL` supplies the default connection store; `VIBY_SECRET_KEY` supplies the separate AES-256-GCM key for the default encrypted secret store. Advanced hosts can replace them through `storage.connections` and `storage.secrets`. Provider credentials never appear in public connection records, messages, events, telemetry, or model inputs. `connect()` accepts an optional provider-neutral authorization preference with `account: "existing" | "new"` and an optional `externalAccountId`. This lets product UI distinguish reconnecting an installed provider account from provisioning another account without introducing provider-specific routes into the SDK boundary. Durable `tools.adapters` may independently declare an `authorization` lifecycle. A registered source exposes `connect`, `connection`, and `disconnect`; the root client exposes `viby.toolSources.callback(request)` for provider redirects. Authorization state is hashed and single use, callback origin/path substitution is rejected, public source configuration remains credential-free, and opaque credentials resolve only inside the selected adapter immediately before tool discovery or execution. Connection metadata is part of `storage.database`; secret bytes use `storage.secrets`. The host remains responsible for provider app registration, the HTTP callback route, and authenticating product requests. `generation.execution` defaults to `embedded`. Use `worker` when request handlers should only persist queued attempts and separate Viby worker processes should claim them. `retention.deletedChatsMs` defaults to 30 days. It defines when newly deleted chats become eligible for permanent purge. Use `null` for indefinite retention or `0` for immediate eligibility; applications may override the value for one deletion. `events.sinks` is an optional provider-neutral registry. Configure signed sinks with `signedOutboundEventSink`; delivery remains explicit so the embedding product chooses its worker, queue, cron, or workflow runtime. Viby persists per-event delivery claims, retry state, and dead letters. `telemetry` receives provider-neutral spans and metrics. `openTelemetry({ tracer, meter? })` adapts structural OpenTelemetry-compatible objects without taking ownership of global SDK registration or exporters. Telemetry failures are fail-open. `cost` defines a host-owned currency or credit unit plus a calculator returning safe integer micro-units. Attempt cost is immutable and logical-generation cost is cumulative across retries, resumes, and task resolution. ### Repository integrations `user.integrations.repository.use(id)` returns a user-scoped handle for one configured repository adapter. The same handle exposes owner, repository, branch, and pull-request operations; creates source-import descriptors for `user.chats.import(...)`; and is passed to `version.push(...)`. ```ts const provider = user.integrations.repository.use("github"); const chat = await user.chats.import({ source: provider.source({ repository: { owner: "acme", name: "starter" }, ref: { branch: "main" }, }), }); const result = await (await chat.latestVersion())!.push({ using: provider, repository: { owner: "acme", name: "app", createIfMissing: true }, branch: { name: "main", createIfMissing: true }, commit: { message: "feat: publish generated app", expectedHead: "last-observed-commit" }, // Optional. The SDK otherwise derives this from the immutable version and target. idempotencyKey: "publish-generated-app-v1", }); const links = await chat.repositoryLinks(); const pushes = await (await chat.latestVersion())!.repositoryPushes(); ``` Every push sends the complete immutable version snapshot, including artifact-backed binary entries. Missing remote paths are therefore removed by the adapter's new tree. A stale `expectedHead` produces a typed `conflict` result instead of overwriting another commit. Branch creation and an optional pull request can be requested in the same call. Repository provider credentials remain inside the selected adapter operation context. `version.push(...)` writes a pending record before contacting the provider and completes it as `pushed`, `conflict`, or `failed`. The record retains the immutable version, selected integration and connection, repository target and durable link, branch, commit and pull-request results, error, timestamps, and idempotency key. Repeating a completed idempotency key replays the persisted result instead of creating another remote effect. Use `version.repositoryPushes()` for one snapshot or `chat.repositoryPushes()` and `chat.repositoryLinks()` after a process restart. The provider-neutral contract is `RepositoryIntegration`; adapter packages can expose provider-specific options through its generics. `verifyRepositoryIntegration` from `@viby/sdk/integrations/repository/conformance` exercises a disposable repository and leaves cleanup to the adapter test owner. Included implementations are exported from `@viby/sdk/integrations/github` and `@viby/sdk/integrations/bitbucket`; vendor types do not enter the core contract. ### Deployment integrations `user.integrations.deployment.use(id)` returns a connected deployment handle. It exposes project discovery/creation and deployment lookup/cancellation. An immutable version can deploy through that handle: ```ts const provider = user.integrations.deployment.use("hosting"); const deployment = await version.deploy({ using: provider, project: { name: "generated-app", createIfMissing: true }, environment: "preview", }); const history = await version.deployments(); const projects = await chat.deploymentProjects(); const prepared = history[0] ? await version.deploymentArtifact(history[0].id) : null; ``` A string project target means an existing project name; `{ id }` selects an existing provider project ID; `{ name, createIfMissing: true }` explicitly allows creation. Each deployment adapter declares whether it accepts immutable framework source or prebuilt files. For source adapters, the SDK materializes the complete immutable version, including artifact-backed binary paths. For prebuilt adapters, it materializes the version in any configured `SandboxAdapter`, runs the declarative install/build contract, stores the build ZIP through the provider-neutral artifact store, and gives the adapter only the built files. The integration contract contains no sandbox, framework, or artifact-store implementation types. Every deployment receives a stable idempotency key derived from the version, integration, project, and environment. Applications can override that key for an app-owned operation identity. A retry reuses an existing prepared artifact when provider work must be attempted again. `preparation.env` reaches the sandbox at runtime but is not persisted; artifact command metadata records only environment names. `version.deploymentArtifact(id)` verifies the stored ZIP size and checksum before returning bytes. `version.download()` remains the independent raw framework-source archive. Deployment URLs are provider results, not SDK-hosted preview URLs. They remain `null` until the selected provider reports one. `version.deploy(...)` persists the version, selected integration connection, project target and durable project link, environment, provider deployment ID, URL, errors, timestamps, and idempotency key. Every changed provider observation is appended as an ordered status transition. Calls through `provider.deployments.get(...)` and `provider.deployments.cancel(...)` update a matching durable deployment automatically. Use `version.deployments()` for one immutable snapshot or `chat.deployments()` and `chat.deploymentProjects()` after a process restart. `verifyDeploymentIntegration` from `@viby/sdk/integrations/deployment/conformance` verifies project creation, deployment idempotency, lookup, and optional cancellation against a caller-owned disposable project. The Vercel implementation is exported from `@viby/sdk/integrations/vercel`; the Cloudflare Pages Direct Upload implementation is exported from `@viby/sdk/integrations/cloudflare`. Provider-specific project and deployment options do not enter this contract. ### Methods ```ts interface Viby { readonly framework: Framework; forUser(scope: UserScope): ScopedViby; worker(options: GenerationWorkerOptions): GenerationWorker; close(options?: { preserveSandboxes?: boolean }): Promise; } interface UserScope { tenantId: string; userId: string; } ``` The canonical built-in IDs are `farmjs`, `nextjs`, `svelte`, `sveltekit`, `vue`, `nuxt`, `solid`, `solidstart`, `tanstack-start`, `react-router`, `astro`, and `vite`. Selecting one automatically adds its package-owned immutable skill to the `core` skill group. Custom strings remain valid and are governed entirely by host-provided skills and source. `farm` and `next` are migration aliases; new records should use `farmjs` and `nextjs`. Durable Streamable HTTP MCP registrations use `mcpAdapter()` from `@viby/sdk/tools/mcp`. Its default public registration schema is `{ url: "https://…" }`; loopback HTTP is allowed for local development. Pass a provider-neutral `authorization` lifecycle for tenant-owned OAuth. The live opaque credential becomes a bearer header inside the transport boundary by default, while `headers` and `connect` support custom authentication and transports without changing the core tool-source contract. New generations snapshot the public durable tool-source registrations selected for their chat in `GenerationConfigurationData.toolSources`. The snapshot fixes adapter type, name, description, public configuration, and revision timestamps for workers and retries; credentials are neither copied nor persisted in the generation and remain governed by the live secret-backed connection. Durable tool-source adapter packages can use `verifyToolSourceAdapter` from `@viby/sdk/tools/conformance`. The reusable suite accepts a caller-owned registration, generation context, harmless call, and optional opaque credential resolver; it verifies the provider-neutral adapter lifecycle without selecting a protocol or provider. Every sandbox adapter declares a provider-neutral capability record. Applications can inspect `session.capabilities` or call `session.supports(name)` instead of branching on the provider name. Capability flags describe the configured Viby adapter surface, not every feature that may exist in the provider's native SDK. `session.start(command)` returns a provider-neutral background process with `wait` and idempotent `kill`. It is available only when `backgroundProcesses` is true. `session.waitForPort(port, options)` polls an HTTP readiness check through the adapter's port URL and supports a caller-provided check for protected previews. Neither API assumes a framework command, package manager, image, or provider. Call `close` during process shutdown or at the end of a short-lived script. ## Tenant and user scope ```ts const userViby = viby.forUser({ tenantId: organization.id, userId: session.user.id, }); ``` Both values are required, non-empty application identifiers. Every repository query constrains both values. The SDK does not infer identity from request globals and does not query the host application's auth tables. ## Chats ```ts interface ChatCollection { create(input?: { title?: string; metadata?: ChatMetadata }): Promise>; import(input: ImportProjectInput): Promise>; get(id: string): Promise>; list(options?: ChatListOptions): Promise>>; snapshot( id: string, options?: { messages?: PageOptions; versions?: PageOptions }, ): Promise<{ chat: Chat; messages: CursorPage; versions: CursorPage>; }>; restore(id: string): Promise>; purgeDeleted(input?: { limit?: number }): Promise; } interface ChatListOptions extends PageOptions { metadata?: ChatMetadata; } interface Chat { readonly id: string; readonly title: string; readonly metadata: ChatMetadata; readonly framework: Framework; readonly createdAt: Date; readonly updatedAt: Date; update(input: UpdateChatInput): Promise>; delete(input?: { retentionMs?: number | null }): Promise; start(input: GenerateInput): Promise>; generate(input: GenerateInput): Promise>; getGeneration(id: string): Promise>; latestVersion(): Promise | null>; getVersion(id: string): Promise>; listVersions(options?: PageOptions): Promise>>; getMessage(id: string): Promise; listMessages(options?: PageOptions): Promise>; getAttachment(id: string): Promise; } interface GenerateInput { prompt: string; model?: string; instructions?: string; skills?: Record; metadata?: ChatMetadata; attachments?: readonly AttachmentInput[]; } interface AttachmentInput { filename: string; mediaType: string; bytes: Uint8Array; } ``` `chats.snapshot(...)` loads the chat plus independently paginated message and version windows for a detail view. PostgreSQL serves the complete snapshot with one aggregate query. Custom persistence adapters may implement the optional `readChatSnapshot` fast path; adapters that omit it use a parallel portable fallback with identical cursors and ownership checks. Generation overrides are flat and optional. `model` selects a configured alias, request skills overlay the client defaults by category, and metadata is host-owned JSON. Viby stores the effective serializable configuration before execution, records the resolved provider/model identity, and snapshots resolved skill contents on the first attempt. A worker can therefore resume the same request after a process restart without persisting provider objects or credentials. Attachments are immutable byte snapshots, not expiring remote URLs. The default repository inserts them atomically with the user message and generation, then supplies standard AI SDK file parts to the configured model or agent. Message pages contain only `AttachmentData` metadata; `chat.getAttachment(id)` performs explicit tenant-, user-, and chat-scoped content retrieval. Inputs are limited to 10 files, 10 MB each, and 25 MB total per generation. `chats.import` accepts typed text and binary source entries, ZIP bytes, or a provider-neutral source adapter. It creates the chat and its first immutable version atomically without invoking the model. Binary inputs use `{ type: "artifact", path, bytes, mediaType }`; archive imports classify known binary assets and invalid UTF-8 octet streams as artifact-backed entries. Imports reject unsafe or duplicate paths, encrypted and ZIP64 entries, symbolic links, oversized inputs, and excessive expanded size. `filePolicy.locked` accepts `"all"` or a path list; file-list imports may additionally set `locked: true` per entry. Policy paths are normalized and must exist in the imported tree. ```ts interface SourceImportAdapter { readonly name: string; import( input: Input, context: { tenantId: string; userId: string; framework: Framework; signal?: AbortSignal; }, ): Promise<{ source: ImportProjectSource; title?: string; summary?: string; }>; } ``` Use `{ type: "adapter", adapter, input }` as the import source. Adapter input stays application-owned and is never persisted. The adapter may return title and summary hints, while explicit caller values win. All returned data re-enters the ordinary file/ZIP validation and immutable snapshot pipeline. Adapter failures use `SourceImportError` without copying provider error text or credentials into the public message. `create` only creates a durable chat. `start` commits the user message, logical generation, initial queued attempt, and first events in one transaction before contacting the model. It returns an addressable handle while execution continues. `generate` is the synchronous convenience form of `start` followed by `wait`; it resolves only after a successful immutable version and assistant message have been committed. Paginated methods default to 20 and accept 1–100. Each page contains `items` and an opaque `nextCursor`; pass that value back as `after`. Chat records are ordered by most recently updated, versions by descending number, and messages chronologically. `chat.getMessage(id)` returns one message with its complete ordered parts and rejects ids from another chat, tenant, or user as not found. `chats.list({ metadata })` applies tenant-scoped JSON containment before pagination, including nested objects and array members; reuse the same filter with subsequent cursors. A cursor is resource-specific and malformed or cross-resource cursors throw `ConfigurationError`. `chat.update` returns a refreshed handle and can replace the title, metadata, or both. Metadata must be a JSON object, is limited to 64 KB and 20 levels, and rejects non-finite numbers, unsafe keys, cycles, and non-JSON values. `chat.delete` atomically tombstones a chat and returns `{ chatId, deletedAt, purgeAfter }`. Deleted chats are excluded from get, list, and pre-existing resource handles immediately. Deletion fails while a generation is queued, running, or waiting. `chats.restore(id)` is tenant- and user-scoped and succeeds only before `purgeAfter`, or at any time for indefinite retention. `chats.purgeDeleted({ limit })` permanently deletes only due tombstones in bounded batches and returns the count; the host application decides when to call it from a worker or scheduled task. ## Typed message parts `MessageData.content` remains the convenient plain-text transcript. `MessageData.parts` is the ordered, durable representation for products that render agent activity: ```ts interface MessageData { readonly id: string; readonly chatId: string; readonly generationId: string | null; readonly role: "user" | "assistant"; readonly content: string; readonly finishReason: string | null; readonly parts: readonly MessagePart[]; readonly attachments: readonly AttachmentData[]; readonly createdAt: Date; } interface AttachmentData { readonly id: string; readonly chatId: string; readonly messageId: string; readonly generationId: string; readonly filename: string; readonly mediaType: string; readonly size: number; readonly checksum: string; readonly createdAt: Date; } type MessagePartType = | "text" | "status" | "reasoning-summary" | "file-read" | "file-edit" | "search" | "command" | "tool-call" | "error" | "usage"; type FileEditMessagePartData = | { readonly operation: "create" | "update" | "write" | "delete"; readonly path: string; } | { readonly operation: "move"; readonly from: string; readonly to: string; }; ``` `finishReason` is the model completion reason committed with an assistant message. It is `null` for user messages and historical assistant messages created before this field was introduced. `MessagePart` is a discriminated union on `type`; each variant has a corresponding typed `data` object. Every part has an `id`, `messageId`, nullable `generationId` and `attemptId`, zero-based `position`, and `createdAt`. The SDK emits `text`, `status`, `file-edit`, and `usage` parts for its current built-in flow. Agent runners and tool providers use the remaining portable variants without putting provider-specific payloads in the message contract. `reasoning-summary` is provider-safe summary text, not hidden chain-of-thought. Part order is immutable after a message is committed. Live lifecycle updates use durable generation events and converge on these persisted message parts. New file writes are classified against the immutable base version as `create` or `update`. `write` is retained only so applications can render historical records created before this distinction was available. ## Versions and iteration ```ts interface Version { readonly id: string; readonly chatId: string; readonly generationId: string | null; readonly parentVersionId: string | null; readonly number: number; readonly origin: "generated" | "imported" | "edited" | "forked" | "restored"; readonly framework: Framework; readonly title: string; readonly summary: string; readonly createdAt: Date; startIteration(input: { prompt: string }): Promise>; iterate(input: { prompt: string }): Promise>; apply(input: ApplySourceChangesInput): Promise>; fork(input?: ForkVersionInput): Promise>; restore(input?: RestoreVersionInput): Promise>; files(): Promise; entries(): Promise; projectArtifact(id: string): Promise; changes(): Promise; workspace(): Promise>>; generation(): Promise; recordDesignEvaluation(input: RecordDesignEvaluationInput): Promise; getDesignEvaluation(id: string): Promise; listDesignEvaluations(options?: PageOptions): Promise>; download(): Promise; sandbox(options?: SandboxOpenOptions): Promise; } ``` `iterate` generates from that exact version, even when it is not the latest. The built-in generator requests typed `write`, `delete`, and `move` changes for an existing project, validates them against that exact base, and persists both the ordered changes and a new complete snapshot. Its `parentVersionId` is the selected version. Historical rows and files are immutable. `apply` performs typed `write`, `delete`, and `move` operations without a model request. Viby validates the complete result and persists it as an `edited` child snapshot. Missing deletes, unsafe paths, move collisions, locked-file changes, duplicate paths, empty projects, and size-limit violations fail before persistence. `fork` atomically creates a new chat and a `forked` first version containing the exact selected files. Its parent points to the source version across chats. `restore` creates a new latest `restored` version in the same chat, with the selected historical snapshot as its parent. Neither operation invokes the model or mutates existing files. Both preserve locked-file metadata. `files` remains the source-compatible text-only view and returns paths sorted lexicographically. `entries` returns the complete `VersionEntry` union: text entries carry `content`, while artifact entries carry `artifactId`; both include `path`, `mediaType`, byte `size`, SHA-256 `checksum`, and `locked`. `projectArtifact` loads bytes only when that artifact is referenced by the selected version and owned by the caller's tenant and user. Once an entry is locked, direct changes, generated changes, and workspace tools cannot write, delete, or move it. ```ts type VersionEntry = | { type: "text"; path: string; content: string; mediaType: string; size: number; checksum: string; locked: boolean } | { type: "artifact"; path: string; artifactId: string; mediaType: string; size: number; checksum: string; locked: boolean }; ``` `changes` returns the ordered operations that produced an `edited` or incrementally generated version. Full-tree generations, imports, forks, and restores return an empty array. Change records are immutable and scoped with their version. ### Design evaluations ```ts interface RecordDesignEvaluationInput { evaluator: string; status: "passed" | "warning" | "failed"; score: number; summary: string; criteria: readonly { id: string; label: string; status: "passed" | "warning" | "failed"; score: number; summary: string; evidence?: readonly DesignEvaluationEvidence[]; }[]; evidence?: readonly DesignEvaluationEvidence[]; metadata?: ChatMetadata; } type DesignEvaluationEvidence = | { type: "version-file"; path: string; description?: string } | { type: "attachment"; attachmentId: string; description?: string } | { type: "artifact"; artifactId: string; description?: string } | { type: "url"; url: string; description?: string } | { type: "note"; text: string }; ``` Evaluation execution is host-owned and provider-neutral: a browser audit, model judge, visual regression service, or custom rubric can all record the same contract. `recordDesignEvaluation` validates scores and evidence, verifies version-file paths against the exact immutable snapshot, and verifies attachment and visual-artifact ownership. `evaluateVisual` composes the configured browser with a URL or sandbox preview, captures routes, stores screenshots through the persistence adapter's artifact store, invokes caller-supplied quality gates, and records the result. No design model is hard-coded. ### Agent workspace ```ts interface AgentWorkspace { readonly committed: boolean; readonly tools: { listFiles(input?: { prefix?: string }): Promise; readFile(input: { path: string }): Promise; search(input: { query: string; prefix?: string; caseSensitive?: boolean; limit?: number; }): Promise; writeFile(input: { path: string; content: string; mediaType?: string }): Promise; deleteFile(input: { path: string }): Promise; moveFile(input: { from: string; to: string }): Promise; }; changes(): readonly SourceChange[]; files(): readonly VersionFile[]; commit(input?: { title?: string; summary?: string }): Promise; } ``` `version.workspace()` copies the selected snapshot into process memory. Tool calls read or stage changes only in that copy and never invoke a model, sandbox, database write, or external provider. The default Viby agent maps these functions into AI SDK tools; a host-provided generation engine may map the same plain functions into another model runtime. Every staged operation is normalized and validated against the full base snapshot, including immutable file locks. `listFiles` exposes `locked` so an agent can avoid proposing prohibited changes. `commit` requires at least one change, accepts concurrent callers as one atomic operation, and creates exactly one `edited` child version. A failed persistence attempt can be retried; after success the workspace is sealed. Use a new workspace for further edits. ### Default workspace and sandbox agent `createViby` uses `AgentProjectGenerator`, a provider-neutral bounded tool-loop over the configured AI SDK `model`. The agent always receives workspace list, read, search, write, delete, and move tools. It must stage a complete source tree for an initial generation or an immutable change set for an iteration before returning its typed completion output. ```ts const viby = createViby({ framework: "farmjs", model, agent: { maxSteps: 20, maxDurationMs: 300_000, maxTokens: 200_000, maxCommands: 20, commandTimeoutMs: 60_000, maxCommandOutputBytes: 200_000, sandboxPorts: [3000], }, }); ``` The limits bound completed model steps, total wall time, observed aggregate input/output tokens, sandbox command count, individual command duration, and returned stdout/stderr. A token or step boundary stops the loop rather than permitting another model step; if no valid final output exists, the attempt fails durably instead of committing partial source. When an iteration has an immutable base version and a sandbox adapter is configured, the runner materializes that version for the attempt and always stops the session afterward. Sandbox file reads, argv commands, streamed command output, and configured port URLs are included only if the session declares `files`, `commands`, `commandStreaming`, and `portUrls`, respectively. No provider name is inspected. Commands accept separate executable and argument fields, no environment values, pass through the configured sandbox command policy, and consume the command budget. An `approval-required` decision pauses before adapter execution with a durable permission task. An initial generation has no persisted base version and therefore uses workspace tools without a sandbox. Workspace writes are the source of truth and successful writes are synchronized into the active sandbox when possible. The final workspace change set, not sandbox filesystem state, creates the immutable version. This keeps generated output deterministic across providers. ## Sandboxes `version.sandbox()` creates an isolated provider session and writes the selected version's complete source snapshot into its project root. It throws `SandboxUnavailableError` when the client has no adapter. ```ts interface SandboxAdapter { readonly provider: string; readonly capabilities: SandboxCapabilities; create(input: SandboxCreateInput): Promise; reconnect?(input: SandboxReconnectInput): Promise; } interface SandboxSession { readonly id: string; readonly provider: string; readonly leaseId: string | null; readonly capabilities: SandboxCapabilities; readonly stopped: boolean; authorizeCommand( command: SandboxCommand, action?: "run" | "start", ): Promise; run(command: SandboxCommand, grant?: SandboxCommandGrant): Promise; start(command: SandboxCommand, grant?: SandboxCommandGrant): Promise; writeFiles(files: readonly SandboxFile[]): Promise; readFile(path: string): Promise; url(port: number): Promise; stop(): Promise; } ``` All paths are relative to the project root and validated before reaching an adapter. Commands contain a distinct `command` and `args` array, optional `cwd`, environment overrides, timeout, abort signal, and `onOutput` callback. A non-zero process exit is returned as a normal typed result; provider transport and lifecycle failures throw `SandboxError`. ### Command policy ```ts type SandboxCommandPolicy = ( request: SandboxCommandPolicyRequest, ) => | { decision: "allow" } | { decision: "deny"; reason: string } | { decision: "approval-required"; reason: string } | { allow: true } | { allow: false; reason: string } | Promise; const policy = sandboxCommandPolicy({ allowCommands: ["node", "pnpm"], denyCommands: ["sudo"], actions: ["run", "start"], environment: ["CI"], maxTimeoutMs: 300_000, maxArgs: 100, }); ``` The request contains the action, adapter provider, tenant/user/chat/version/framework context, executable, copied argument list, normalized project-relative working directory, environment variable names, and timeout. Environment values, signals, and output callbacks are excluded. The core evaluates the policy immediately before `SandboxInstance.run` or `start`; denial throws `SandboxCommandDeniedError`, while `approval-required` throws `SandboxCommandApprovalRequiredError` with the secret-free proposed action. The legacy `{ allow: true | false, reason? }` result is normalized to allow or deny. A thrown policy error or invalid decision throws `SandboxError` and still prevents execution. The same policy is restored on a session reconnected from a durable lease. The default agent catches the approval-required result before invoking the adapter, persists `SandboxCommandProposedAction` on its permission task, and stops the attempt. An allowed task resolution starts a new attempt with only that exact action fingerprint approved. A denied resolution keeps the fingerprint denied. The resulting command tool call is an `external` effect whose stable idempotency key is the proposed-action fingerprint: a prior success is reused, and a prior pending effect is never executed again without reconciliation. Low-level consumers can apply the same handshake with `authorizeCommand(command, action)` and pass the opaque, one-use grant to `run` or `start`. Opening a version persists a portable sandbox lease before materialization completes. Lease records are constrained by `tenantId` and `userId` and contain the framework, chat and version ids, adapter provider, opaque provider sandbox id, declared ports, status, and expiration. Environment values, adapter credentials, and provider response payloads are deliberately excluded. ```ts const session = await version.sandbox({ ports: [3000] }); const lease = await userViby.sandboxes.get(session.leaseId!); const reconnected = await userViby.sandboxes.reconnect(lease.id); ``` Reconnect first loads and scopes the Viby lease, rejects stopped or expired leases, checks that the currently configured adapter owns the same provider name, and calls the adapter's explicit reconnect primitive. E2B, Vercel Sandbox, and Cloudflare currently declare `reconnect: true`; adapters without that primitive fail with `SandboxUnavailableError` instead of recreating an unrelated environment. ### E2B adapter Install the optional `e2b` peer and import `e2bSandbox` from `@viby/sdk/sandbox/e2b`. It uses `E2B_API_KEY` by default or accepts an explicit server-side `apiKey`, plus `template`, `domain`, request timeout, security, and metadata options. Viby command arguments are individually shell-quoted because E2B accepts a shell command string. ### Vercel Sandbox adapter Install the optional `@vercel/sandbox` peer and import `vercelSandbox` from `@viby/sdk/sandbox/vercel`. Authentication uses Vercel OIDC automatically, or an explicit `token`, `teamId`, and `projectId` tuple. The adapter accepts an image or legacy runtime, resources, network policy, tags, and an optional name factory. Viby sessions are non-persistent and support up to four declared ports. ### Daytona adapter Install the optional `@daytona/sdk` peer and import `daytonaSandbox` from `@viby/sdk/sandbox/daytona`. Authentication uses the Daytona environment variables by default or accepts explicit API key, API URL, and target options. The adapter creates ephemeral, TTL-bound sandboxes from an image, snapshot, or the Daytona default; supports resource, network, secret, label, user, language, and name configuration; streams stdout and stderr through Daytona command sessions; and exposes directly usable signed preview URLs. ### Modal adapter Install the optional `modal` peer and import `modalSandbox` from `@viby/sdk/sandbox/modal`. The provider package requires a server-side Node.js 22 or newer runtime and authenticates from the standard Modal token environment variables or an explicit token ID and secret pair. The adapter creates a bounded-lifetime sandbox in a configurable Modal App, writes binary source files, executes argv without shell interpolation, streams both output channels, exposes declared encrypted tunnels, and terminates the sandbox on cleanup. Modal-specific image, secret, GPU, resource, region, cloud, OIDC, tag, idle timeout, and network policy options remain isolated to the adapter. ### Cloudflare Sandbox adapter Install the optional `@cloudflare/sandbox` peer and import `cloudflareSandbox` from `@viby/sdk/sandbox/cloudflare` inside a Cloudflare Worker. The application owns the required Container, Durable Object binding, migration, image, and capacity configuration and passes `env.Sandbox` as the adapter binding. The adapter creates a unique normalized Durable Object identity, performs sessionless command execution with safely quoted arguments, streams output, transfers binary files, maps inactivity to the Viby session timeout, supports quick tunnels or Worker-hostname previews, and destroys the container on cleanup. Quick tunnels require RPC transport. Preview URLs are public, and Cloudflare reserves port `3000` for its control server. ### Local Docker adapter Import `dockerSandbox` from `@viby/sdk/sandbox/docker`; it has no provider package dependency but requires access to a Docker CLI and daemon. Defaults include a read-only root, temporary project filesystem, dropped Linux capabilities, `no-new-privileges`, resource limits, and loopback-only random host ports. Source is streamed through `docker exec` and is never bind-mounted from the host. Docker is intended for trusted local or single-tenant environments rather than hostile multi-tenant isolation. ## Durable generations A generation is the stable logical request. An attempt is one immutable execution of that request. Retry, resume, and task resolution append attempts instead of overwriting failure history. `GenerationData.configuration` contains the durable `model` alias, optional instructions, effective categorized skill references, and request metadata. The immutable attempt records continue to carry the concrete provider and model IDs used for cost and telemetry attribution. Retries and resumed workers reload attachment bytes by generation ID and checksum. The public attachment contract does not expose PostgreSQL or require a provider-specific upload handle, so a future repository implementation can place byte content in object storage without changing generation calls. ```ts interface Generation { readonly id: string; readonly chatId: string; data(): Promise; attempts(): Promise; tasks(): Promise; steering(): Promise; steer(input: string | GenerationSteeringInput): Promise; toolCalls(): Promise; events(options?: { after?: string; limit?: number }): Promise; deliverEvents(options: { sink: string; after?: string; limit?: number; signal?: AbortSignal; }): Promise<{ deliveries: readonly OutboundEventReceipt[]; cursor: string; hasMore: boolean; }>; stream(options?: { after?: string; pollIntervalMs?: number; signal?: AbortSignal; }): AsyncGenerator; wait(options?: { pollIntervalMs?: number; signal?: AbortSignal; }): Promise>; cancel(reason?: string): Promise; retry(): Promise; resume(): Promise; resolve(input: ResolveGenerationTaskInput): Promise; } ``` Logical generation statuses are `queued`, `running`, `waiting`, `succeeded`, `failed`, and `cancelled`. Attempt statuses additionally include `interrupted`. Attempt reasons are `initial`, `retry`, `resume`, and `task_resolution`. `ScopedViby.generations.get(id)` restores a handle after a request, page reload, or process restart. `retry` is available for failed or cancelled generations. `resume` also takes over a durable `queued` or `running` record whose original process is gone; an attempt with an unexpired worker lease cannot be interrupted. `cancel` first commits cancellation, then aborts the local AI SDK model signal. Aborting `stream` or `wait` only stops that subscriber and never cancels the generation. `steer` commits a tenant-scoped user message and `GenerationSteeringData` before it reaches a worker. It is valid in `queued`, `running`, and `waiting` states; terminal generations reject new steering and should be iterated instead. An optional `idempotencyKey` deduplicates repeated product requests for the same logical generation, and attachments use the normal immutable attachment contract. `steering()` returns ordered queued and applied records. Workers consume steering under the active attempt lease and append `steering.queued` and `steering.applied` to the ordinary resumable cursor. The built-in bounded agent consumes between model/tool steps through AI SDK loop preparation. Provider-neutral engines receive `GeneratorOptions.steering.consume()` and choose equivalent safe boundaries. Applying steering never interrupts an in-flight command or authorizes replay of an external effect. Configure provider-neutral quality gates when generated source must install and build before it becomes an immutable version: ```ts const viby = createViby({ framework: "farmjs", model, sandbox, preview: { prepare: [{ command: "pnpm", args: ["install", "--prefer-offline"] }], start: { command: "pnpm", args: ["dev", "--host", "0.0.0.0"] }, port: 3000, }, generation: { workspace: { preview: "eager" }, quality: { prepare: [{ id: "install", command: "pnpm", args: ["install", "--prefer-offline"], }], checks: [ { id: "typecheck", command: "pnpm", args: ["typecheck"] }, { id: "build", command: "pnpm", args: ["build"] }, ], checkConcurrency: 2, }, }, }); ``` `workspace.preview: "eager"` materializes the base version and begins preview preparation while generation runs. `workspace.started`, `workspace.prepared`, `preview.ready`, and `preview.failed` use the existing resumable cursor, so a product can show the first coherent preview without waiting for final validation. The completed candidate is synchronized into the same sandbox, preparation is reconciled, and up to `checkConcurrency` checks run concurrently. Viby commits only after every command exits with code `0`, then retargets the existing durable preview to that immutable version without restarting it. Without eager preview, quality gates retain their fresh-sandbox lifecycle. Failed checks fail the attempt without persisting a partial version. `quality.started` and `quality.completed` remain durable, while command output is intentionally not persisted because it may contain secrets. The configured sandbox command policy applies to every quality command. Quality and workspace policy are host configuration and must be identical on all durable workers. Embedded execution remains the default. Set `generation: { execution: "worker" }` to leave new attempts queued for a portable database-backed worker: ```ts const worker = viby.worker({ id: "worker-eu-1", concurrency: 4, leaseMs: 30_000, heartbeatMs: 10_000, pollIntervalMs: 500, }); await worker.run({ signal }); // Or claim and process at most one attempt: await worker.runOnce(); ``` Claims use `FOR UPDATE SKIP LOCKED` and are restricted to the client's framework, model provider, and model id. Each running attempt records its worker id, heartbeat, and lease expiration. An opaque lease token fences output events, skill snapshots, task pauses, failures, and completion transactions. A worker can reclaim a running attempt only after its lease expires; the old owner then loses write access. This is an at-least-once execution contract and assumes no queue service, workflow vendor, or particular JavaScript host. When a request-scoped worker hands its generation sandbox to an eager durable preview, close the request-owned client with `viby.close({ preserveSandboxes: true })`. This closes stores and provider clients without stopping reconnectable sandbox leases. Ordinary shutdown keeps the default `viby.close()` behavior and stops locally tracked previews and sandboxes. ### Events and resumable cursors Every lifecycle transition, structured-output text delta, and agent trace update is stored in `viby.generation_events`. `cursor` is an opaque, monotonically increasing integer string. Consumers persist the last acknowledged cursor and pass it back through `after`: ```ts for await (const event of generation.stream({ after: lastCursor })) { lastCursor = event.cursor; publishToClient(event); } ``` For Web-standard HTTP hosts, `generation.toEventStreamResponse({ request })` and `generationEventStreamResponse(generation, { request })` return a standards-compliant SSE `Response`. They read the resumable cursor from `Last-Event-ID` unless `after` is explicit, emit `id`, typed `event`, and JSON `data` fields, set no-buffer/no-cache headers, and combine request cancellation with an explicit signal. The event stream ends at `waiting`, `succeeded`, `failed`, or `cancelled`. Calling it again with the saved cursor replays only later events. The initial release uses portable Postgres polling; applications do not need Redis or provider-specific stream storage. Agent traces use `part.started`, `part.delta`, `part.completed`, and `part.failed`. A started event assigns a stable UUID, typed message-part kind, and trace-local position. Delta events carry incremental text for that id. A completed event carries the typed part data and the completed part is persisted into the final assistant message with the same id. A failed event carries a normalized message, nullable code, and retryable flag; failed and incomplete parts do not become final message parts. Part events share the generation's existing durable cursor. No second checkpoint or provider stream identifier is required, and attempt ownership prevents a stale worker from appending trace data after its lease is fenced. ### Signed outbound event sinks `signedOutboundEventSink({ id, secret, keyId?, source?, send })` adapts the durable generation event log to an application-owned transport. `generation.deliverEvents({ sink, after, limit, retry? })` reads one durable page and sends it in cursor order. Delivery state is persisted by generation, event cursor, and sink. A row is claimed with a lease before transport execution, so concurrent workers cannot intentionally repeat the same active delivery. Each request contains a CloudEvents-style JSON body plus lowercase `viby-event-id`, `viby-event-key-id`, `viby-event-timestamp`, and `viby-event-signature` headers. The signature is `v1=` over `timestamp.eventId.body`. Secrets must contain at least 32 bytes and remain in process memory. `verifySignedOutboundEvent(request, { secret, keyId, toleranceMs })` verifies the key, replay window, raw body, and signature using constant-time comparison before returning the typed envelope. Retry policy bounds attempts, exponential backoff, maximum delay, and claim lease duration. An exhausted delivery becomes `dead_lettered`; inspect records with `generation.outboundDeliveries({ sink, status? })` and explicitly redrive one with `generation.redriveOutboundEvent({ sink, cursor })`. A redrive resets attempts and makes the event claimable again. Sink or transport errors never change generation state. Because an endpoint may accept a request before the caller observes failure, receivers must still deduplicate stable event IDs. Viby owns durable delivery state, not a scheduler or queue runtime. ### MCP tools `registerVibyMcpTools(server, { viby, prefix? })` from `@viby/sdk/mcp` exposes a scoped subset of the durable API on an official `@modelcontextprotocol/server` `McpServer`. It registers chat list/create/get, generation start/get/events/cancel/steering/task resolution, version list/files/iteration, and source download tools. The supplied `ScopedViby` is already bound to one authenticated tenant/user scope. Viby does not accept identity as tool arguments and does not own MCP transport, authentication, OAuth, connection storage, or third-party credentials. ### Tool calls and results Every tool execution can be recorded through the runner's provider-neutral tool-call writer. Applications inspect the durable records through `generation.toolCalls()`: ```ts interface ToolCallData { readonly id: string; readonly generationId: string; readonly attemptId: string; readonly messageId: string | null; readonly providerCallId: string; readonly name: string; readonly effect: "read" | "write" | "external"; readonly arguments: JsonValue; readonly result: JsonValue | null; readonly status: "pending" | "succeeded" | "failed"; readonly error: string | null; readonly idempotencyKey: string | null; readonly createdAt: Date; readonly completedAt: Date | null; } ``` Arguments and results are validated as finite, bounded JSON. Keys commonly used for passwords, tokens, cookies, authorization headers, credentials, private keys, secrets, and API keys are replaced with `[REDACTED]` before the repository receives them. The SDK never stores an unredacted copy. External effects require a non-empty idempotency key. `(tenant, user, tool name, idempotency key)` identifies the effect across attempts and generations; a duplicate start returns the original record with `created: false`. Callers must reuse its terminal result or reconcile a pending record instead of repeating an uncertain external action. Lease fencing applies to call creation and settlement. Tool calls retain attempt ownership immediately and are linked to the final assistant message when its `tool-call` part is committed. ### Typed tasks When the model cannot safely continue, a generation can enter `waiting` with one of three discriminated tasks: - `plan`: steps that require `approve` or `revise` with feedback; - `question`: a question, optional choices, and a required answer; - `permission`: an action and requested permissions requiring `allow` or `deny`; sandbox-agent tasks also include a typed `proposedAction`. ```ts const outcome = await generation.wait(); if (outcome.status === "waiting") { const task = outcome.tasks[0]; if (task?.kind === "permission") { await generation.resolve({ taskId: task.id, resolution: { kind: "permission", decision: "allow", note: "Read-only access only", }, }); } } ``` The resolution kind must match the task kind. Resolving commits the response and a new queued attempt in one transaction, then continues the same logical generation. Proposed sandbox actions are stored inside the task record and contain command arguments and environment names, never environment values. ### Messages and usage Viby persists: - provider and model identifiers; - input, output, and total tokens when the provider returns them; - timestamps and error text; - the user and assistant messages; - the exact skills and content hashes reused by every attempt in the logical generation; - the base version from which generation started. A failed attempt stores its failure and does not create an assistant message or partial version. `Generation.wait` returns the durable failure. The synchronous `chat.generate` convenience throws `GenerationError` with `generationId`, so the host product can correlate logs and support requests without exposing a secret. Generated projects use Structured Outputs. Every file includes a required `mediaType` field that may be `null`; a null value is replaced by Viby MIME inference before persistence. ### Telemetry and cost Generation and attempt execution emit provider-neutral spans and low-cardinality counters/histograms for latency, attempts, outcomes, token usage, and attributed cost. Attributes intentionally exclude prompts, source, tool arguments/results, provider credentials, and tenant/user IDs. A throwing telemetry implementation cannot fail generation. When a cost calculator is configured, its estimate is committed with the attempt outcome and added to the logical generation in the same durable lifecycle. The value is `{ amountMicros, currency }`; Viby does not encode vendor prices, exchange rates, or billing policy. A configured result also appears in the final `usage` message part. ## Downloads ```ts const artifact = await version.download(); artifact.filename; // e.g. analytics-dashboard.zip artifact.contentType; // application/zip artifact.bytes; // Uint8Array artifact.toResponse();// standard Web Response ``` The ZIP is built from the selected persisted version and materializes both text and artifact-backed binary entries. It is the raw framework-native source tree and excludes secrets, lockfiles, dependency folders, build output, and provider-specific deployment configuration. ## Persistence model | Table | Purpose | Written when | | --- | --- | --- | | `viby.chats` | scoped conversation, framework, app metadata, and retention tombstone | creation, update, successful generation, deletion, and restoration | | `viby.messages` | user/assistant conversation | attempt creation and successful completion | | `viby.message_parts` | ordered typed message content and agent activity with attempt ownership | with the owning user or assistant message | | `viby.generations` | logical request, active attempt, aggregate status and usage, base version | before execution and on every transition | | `viby.generation_attempts` | immutable execution history, reason, model IDs, usage, errors | before every initial, retry, resume, or task-resolution execution | | `viby.generation_events` | ordered lifecycle, structured-output, and agent part trace events | in the same transaction as transitions and during model or agent streaming | | `viby.generation_tasks` | typed blocking requests, proposed actions, and resolutions | when pausing and resuming a generation | | `viby.generation_steering` | ordered queued/applied instructions, owning messages and attempts, and idempotency keys | when steering is submitted and consumed at a safe boundary | | `viby.tool_calls` | redacted typed tool arguments, results, ownership, status, and idempotency | before and after each runner tool execution | | `viby.tool_sources` | tenant/user-scoped provider-neutral tool registrations and public configuration | source creation, update, disable, or archive | | `viby.chat_tool_sources` | explicit source selection for one chat | selection replacement or source archive | | `viby.tool_source_authorization_sessions` | hashed, expiring, single-use provider callback state and secret-store references | authorization start and callback consumption | | `viby.tool_source_connections` | provider-neutral account, status, scopes, expiry, and secret-store reference | authorization completion, refresh, or revocation | | `viby.versions` | immutable source snapshot metadata, origin, and parent lineage | successful generation, source import, or direct source change | | `viby.version_files` | complete text or artifact-backed entries and immutable lock metadata for one version | successful completion | | `viby.project_artifacts` | scoped binary source metadata and external artifact-store references | project import and retention purge | | `viby.version_changes` | ordered typed operations that produced an incremental version | agent workspace commit, direct source change, or generated iteration | | `viby.repository_links` | provider-neutral links between a chat and a connected remote repository | first completed push and subsequent repository metadata refreshes | | `viby.repository_pushes` | version-bound push, conflict, commit, pull-request, error, and idempotency history | before and after every `version.push(...)` effect | | `viby.deployment_project_links` | provider-neutral links between a chat and a connected deployment project | first completed deployment and subsequent project metadata refreshes | | `viby.deployments` | version-bound deployment identity, environment, latest status, URL, error, and idempotency history | before and after every `version.deploy(...)` effect | | `viby.deployment_status_transitions` | ordered provider-neutral deployment lifecycle observations | initial request, provider result, lookup, cancellation, and failure | | `viby.deployment_artifacts` | immutable prebuilt ZIP metadata and external artifact-store reference linked to one deployment and source version | successful sandbox preparation before a prebuilt provider effect | | `viby.skill_snapshots` | content-addressed resolved skill files | first use of each exact skill revision | | `viby.generation_skills` | ordered category/activation links | before the first model request | | `viby.sandbox_leases` | portable sandbox identity, source context, ports, expiration, and lifecycle state | sandbox open, reconnect, stop, or expiration | | `viby.outbound_event_deliveries` | delivery claims, attempts, retry schedule, errors, receipts, and dead letters | before and after each configured sink effect | | `viby.schema_migrations` | migration ledger | explicit CLI migration | Generation and attempt rows also retain optional cost amount/currency columns. Migrations are explicit, transactional, and protected by a Postgres advisory lock. Application requests never run migrations implicitly. Published migration checksums, a historical-schema upgrade fixture, and frozen public API/export fixtures are enforced in CI. ## Error contract All SDK errors extend `VibyError`: - `ConfigurationError`: invalid config, prompt, identifier, or generated source shape; - `DatabaseNotReadyError`: `viby` migrations have not been applied; - `NotFoundError`: missing or out-of-scope resource; - `SkillResolutionError`: local or remote skill could not be safely resolved; - `SourceImportError`: an application-provided source adapter failed without exposing its input; - `IntegrationAuthorizationError`: a provider authorization request or callback failed; - `IntegrationConnectionRequiredError`: the selected operation has no usable connection; - `ToolSourceAuthorizationError`: a tool-source provider authorization request or callback failed; - `ToolSourceConnectionRequiredError`: a selected durable tool source has no usable connection; - `IntegrationOperationError`: a connected provider operation failed without copying credentials; - `OutboundEventSinkError`: a signed sink transport failed without copying transport details; - `OutboundEventDeliveryError`: delivery stopped and includes the exact safe resume cursor; - `OutboundEventSignatureError`: an inbound signed envelope failed verification; - `GenerationError`: the durable model attempt failed; - `GenerationCancelledError`: the synchronous convenience API observed cancellation; - `GenerationStateError`: retry, resume, cancel, or task resolution is invalid for the current state; - `GenerationTaskRequiredError`: the synchronous convenience API encountered pending typed tasks. ## Not in v1 The following names are intentionally not stubbed. Applications should not have to discover at runtime that a declared method is unavailable: - in-place version mutation or tarball output; - SDK-owned preview hosting, bundled deployment-provider adapters, or a bundled Bitbucket adapter. See [`v0-core.md`](./v0-core.md) for the audited parity roadmap and persistence rules. --- ## Versions and artifacts URL: /docs/api/versions Inspect, change, branch, preview, evaluate, publish, and download immutable source versions. # Versions and artifacts A version is a complete immutable project snapshot. Generation, import, direct changes, workspace commits, restore, and iteration create new versions instead of mutating existing source. ## Version identity | Property | Meaning | | --- | --- | | `id` | Opaque immutable version identifier. | | `chatId` | Owning project timeline. | | `generationId` | Logical generation that produced the version, or `null` for non-generation origins. | | `parentVersionId` | Previous snapshot in this lineage, or `null` for an initial import. | | `number` | Stable chat-local sequence number. | | `origin` | How the version was created: generation, import, changes, restore, fork, or workspace flow. | | `framework` | Framework identifier inherited from the chat. | | `title` / `summary` | Generated or host-supplied version metadata. | | `createdAt` | Durable creation timestamp. | ## Read source | Method | Returns | Behavior | | --- | --- | --- | | `files()` | `VersionFile[]` | Text-only compatibility view of the source tree. | | `entries()` | `VersionEntry[]` | Complete text and artifact-backed source entries with media type and lock state. | | `projectArtifact(id)` | `ProjectArtifactContent` | Reads and checksum-verifies one binary project entry. | | `changes()` | `SourceChange[]` | Ordered immutable change set that created this version. | | `generation()` | `GenerationData \| null` | Loads the producing logical generation when one exists. | Paths are normalized, relative, and unique. Artifact bytes stay in `storage.artifacts`; version rows store metadata and opaque references. ## Iterate and change source | Method | Behavior | | --- | --- | | `startIteration(input)` | Starts a durable generation with this exact version as its base and returns immediately. | | `iterate(input)` | Waits for successful iteration and returns the new immutable child version. | | `apply({ changes, title?, summary? })` | Validates an ordered write/delete/move set and atomically creates a child version. | | `workspace()` | Returns an `AgentWorkspace` for bounded reads, search, staged changes, and atomic commit. | ```ts const changed = await version.apply({ title: "Update navigation", changes: [ { type: "write", path: "src/navigation.ts", content: nextNavigation }, { type: "move", from: "src/old.ts", path: "src/new.ts" }, { type: "delete", path: "src/unused.ts" }, ], }); ``` Locked paths reject writes, deletes, and moves. A failed validation creates no partial version. ## Fork and restore | Method | Behavior | | --- | --- | | `fork({ title?, metadata? })` | Creates a new chat whose initial source equals this version. The original chat is unchanged. | | `restore({ title?, summary? })` | Creates a new child version in the same chat whose complete source equals this historical snapshot. | Restore does not move a mutable pointer backward. The restored source becomes a new auditable version at the head of the current timeline. ## Sandbox and preview | Method | Behavior | | --- | --- | | `sandbox(options?)` | Materializes this version in the configured sandbox and returns a live `SandboxSession`. | | `preview(options?)` | Starts the configured long-running preview command, waits for readiness, persists the session, and returns `Preview`. | Neither method exists as hosted magic. Without a configured adapter, sandbox methods throw `SandboxUnavailableError` and API-host preview routes report `501 preview_not_configured`. ## Repository publishing ```ts const github = user.integrations.repository.use("github"); const result = await version.push({ using: github, repository: { owner: "acme", name: "generated-app", createIfMissing: true }, branch: { name: "main", createIfMissing: true }, commit: { message: "feat: publish generated app" }, pullRequest: { title: "feat: publish generated app", base: "main" }, }); ``` | Method | Behavior | | --- | --- | | `push(input)` | Persists a pending effect, sends the complete immutable snapshot through a repository handle, and records push/conflict/failure plus optional PR. | | `repositoryPushes()` | Reloads durable push history for this exact version without contacting the provider. | Push inputs accept a stable idempotency key. Repeating a completed key returns the persisted outcome. An optimistic `expectedHead` conflict is recorded without overwriting the remote branch. ## Deployment ```ts const hosting = user.integrations.deployment.use("vercel"); const deployment = await version.deploy({ using: hosting, project: { name: "generated-app", createIfMissing: true }, environment: "preview", }); ``` | Method | Behavior | | --- | --- | | `deploy(input)` | Persists a pending effect, prepares source or prebuilt output as required, invokes the provider, and records status transitions. | | `deployments()` | Reloads durable deployment history for this version. | | `deploymentArtifact(deploymentId)` | Reads and checksum-verifies the immutable prebuilt ZIP used for that deployment, or returns `null`. | The URL is nullable until the provider returns one. Raw source remains separate from prebuilt output; `download()` always returns the framework-native project. ## Design and visual evaluation | Method | Behavior | | --- | --- | | `recordDesignEvaluation(input)` | Persists an immutable rubric evaluation linked to validated evidence. | | `getDesignEvaluation(id)` | Loads one evaluation for this version. | | `listDesignEvaluations(options?)` | Cursor-pages evaluation history. | | `evaluateVisual(input)` | Captures configured browser pages, stores screenshots as artifacts, runs quality gates, and records evidence. | | `visualArtifacts()` | Lists screenshot metadata for the version. | | `getVisualArtifact(id)` | Reads and checksum-verifies one visual artifact. | Visual evaluation requires a configured browser and a resolvable preview source. Quality gates are host-provided and may be rule-, model-, or agent-based. ## Download ```ts const download = await version.download(); download.filename; download.contentType; // application/zip download.bytes; // Uint8Array return download.toResponse(); ``` The ZIP is deterministic for the persisted source snapshot and includes artifact-backed binary entries. It excludes secrets, dependency directories, lockfiles, build output, and provider state. --- ## Web API host URL: /docs/api-host Mount Viby as a framework-neutral Web Request/Response API with a typed client and resumable SSE. # Web API host `createVibyApi()` turns a configured Viby client into one framework-neutral `fetch(Request): Promise` handler. It uses only Web `Request`, `Response`, `Headers`, `ReadableStream`, URL, and crypto-compatible SDK surfaces, so the same host can be mounted in Node, Bun, Workers, or a framework route. ```ts const api = createVibyApi({ viby, basePath: "/api/viby", authenticate: async (request) => { const session = await sessions.read(request); return session ? { tenantId: session.tenantId, userId: session.userId } : new Response("Unauthorized", { status: 401 }); }, }); ``` The host owns authentication. Returning `null` produces a JSON `401`; returning a `Response` preserves a product-specific redirect or denial. Every resource route after authentication uses the returned tenant/user scope. Integration and tool-source callbacks are deliberately public because their hashed, single-use authorization state establishes the original scope. An authenticator that creates or rotates a cookie can return the scope and response headers together. The headers are applied to both successful route responses and SDK error responses, and are never persisted or exposed to generation code: ```ts authenticate: async (request) => { const session = await sessions.readOrCreate(request); return { scope: { tenantId: session.tenantId, userId: session.userId }, headers: session.setCookie ? { "Set-Cookie": session.setCookie } : undefined, }; }, ``` ## Typed Web client `createVibyWebClient()` consumes this contract from browsers, Workers, Bun, Node, or another Web-compatible runtime. It owns route construction, attachment encoding, typed errors, binary download responses, and SSE reconnection from the last acknowledged cursor. ```ts import { createVibyWebClient } from "@viby/sdk/core"; const viby = createVibyWebClient<"farmjs">({ baseUrl: "/api/viby", fetch: authenticatedFetch, }); const { chat, generation } = await viby.chats.create({ title: "Analytics", prompt: "Build a polished analytics dashboard", }); const imported = await viby.chats.import({ title: "Existing app", source: { type: "zip", bytes: uploadedZip }, }); await viby.chats.versions.apply(imported.chat.id, imported.version.id, { changes: [{ type: "write", path: "src/theme.css", content: theme }], }); for await (const event of viby.generations.stream(generation.id)) { saveCursor(event.cursor); render(event); } const state = await viby.generations.get(generation.id); const source = await viby.chats.versions.download(chat.id, state.version!.id); const restored = await viby.chats.versions.restore(chat.id, state.version!.id); const forked = await viby.chats.versions.fork(chat.id, restored.version.id, { title: "Analytics experiment", }); const previews = await viby.previews.list({ chatId: chat.id, status: "ready" }); if (previews.previews[0]) await viby.previews.stop(previews.previews[0].id); const tools = await viby.toolSources.create({ type: "mcp", name: "Company tools", configuration: { endpoint: "https://tools.example.com/mcp" }, }); await viby.chats.toolSources.set(chat.id, [tools.toolSource.id]); ``` Pass `after` to `generations.stream()` when restoring a cursor from application storage. The client sends it as `Last-Event-ID`, updates it after each event, and reconnects a prematurely closed retryable stream without replaying acknowledged events. File, ZIP, and attachment bytes remain `Uint8Array` values in application code; the client owns their base64 HTTP encoding. Authentication remains host-owned: use `headers`, a header factory, or a custom `fetch` implementation to attach the product session. ## Routes All paths are relative to `basePath` (default `/api/viby`). | Method and path | Operation | | --- | --- | | `GET /chats` | list chats with `limit`, `after`, and JSON-encoded `metadata` filters | | `POST /chats` | create a chat; include `prompt` to start its first generation | | `POST /chats/imports` | import a project from text/binary files, a base64 ZIP, or a connected repository | | `GET/PATCH/DELETE /chats/:chatId` | load, update, or retention-delete a chat | | `POST /chats/:chatId/restore` | restore a retention-deleted chat before its purge deadline | | `GET/POST /chats/:chatId/messages` | list messages or start a generation | | `GET /chats/:chatId/messages/:messageId` | load one durable message | | `GET /chats/:chatId/attachments/:attachmentId` | stream private attachment bytes with verified metadata headers | | `GET /chats/:chatId/environment` | list public and redacted environment-variable records | | `PUT/DELETE /chats/:chatId/environment/:environment/:name` | set or delete a scoped variable or secret | | `GET/PUT /chats/:chatId/tool-sources` | list or replace the chat's explicit durable tool-source selection | | `GET /chats/:chatId/{repository-links,repository-pushes}` | reload durable repository connection and push history | | `GET /chats/:chatId/{deployment-projects,deployments}` | reload durable deployment project and deployment history | | `GET /chats/:chatId/versions` | list immutable versions | | `GET /chats/:chatId/versions/:versionId` | load version metadata and entries | | `GET/POST /chats/:chatId/versions/:versionId/changes` | inspect or apply immutable source changes | | `POST /chats/:chatId/versions/:versionId/restore` | create a new version from a previous immutable snapshot | | `POST /chats/:chatId/versions/:versionId/fork` | create a new chat whose first version references the source snapshot | | `POST /chats/:chatId/versions/:versionId/messages` | iterate from an exact version | | `GET /chats/:chatId/versions/:versionId/download` | download raw framework source as ZIP | | `POST /chats/:chatId/versions/:versionId/preview` | open a configured durable preview or invoke a host override | | `GET /chats/:chatId/versions/:versionId/artifacts/:artifactId` | stream a binary project entry | | `GET /chats/:chatId/versions/:versionId/visual-artifacts[/:artifactId]` | list visual evidence or stream one screenshot | | `GET/POST /chats/:chatId/versions/:versionId/repository-pushes` | list version push history or push/open a PR through an integration | | `GET/POST /chats/:chatId/versions/:versionId/deployments` | list deployment history or deploy the immutable version | | `GET /chats/:chatId/versions/:versionId/deployments/:deploymentId/artifact` | download immutable prebuilt deployment output | | `GET /generations/:generationId` | load status, attempts, tasks, tools, artifacts, and result version | | `GET /generations/:generationId/events` | stream resumable SSE using `Last-Event-ID` | | `GET /generations/:generationId/events/page` | read a JSON event page with `after` and `limit` | | `GET /generations/:generationId/artifacts/:artifactId` | stream a generated image, audio, document, or binary output | | `POST /generations/:generationId/cancel` | cancel an active generation | | `POST /generations/:generationId/retry` | add a retry attempt | | `POST /generations/:generationId/resume` | resume an interrupted attempt | | `POST /generations/:generationId/tasks/:taskId` | resolve a typed plan, question, or permission task | | `GET /previews` | list durable previews by chat, version, or status | | `GET/DELETE /previews/:previewId` | load or stop a durable preview | | `POST /previews/:previewId/reconnect` | reconnect the configured sandbox to a durable preview lease | | `POST /previews/cleanup` | stop and release expired previews with an optional bounded limit | | `GET/POST /integrations/callback` | complete a provider authorization through durable state | | `GET/POST /tool-sources/callback` | complete tool-source authorization through durable single-use state | | `GET/POST /tool-sources` | list/filter registrations or create a public credential-free registration | | `GET/PATCH/DELETE /tool-sources/:sourceId` | load, update, disable, or archive a registration | | `GET /tool-sources/:sourceId/connection` | inspect redacted connection metadata | | `POST /tool-sources/:sourceId/{connect,disconnect}` | authorize, reconnect, or revoke a source connection | | `GET /integrations[/:category]` | list configured repository/deployment adapters and connection state | | `GET/POST/DELETE /integrations/:category/:id/{connections,connect}` | inspect, authorize, or revoke user connections | | `GET/POST /integrations/repository/:id/{owners,repositories,branches,pull-requests}` | drive provider-neutral repository selection and PR workflows | | `GET/POST /integrations/deployment/:id/projects` | list or create deployment projects | | `GET/DELETE /integrations/deployment/:id/deployments/:deploymentId` | refresh or cancel a provider deployment | Compact `/versions/:versionId/{iterations,preview,download}` aliases accept `chatId` in the JSON body or download query for small clients that already keep the chat identity in UI state. Generation JSON accepts `prompt`, `model`, `instructions`, categorized `skills`, `metadata`, and attachments. HTTP attachments use `{ filename, mediaType, base64 }`; the host converts them to immutable byte inputs before calling Viby. `maxBodyBytes` defaults to 10 MiB and is enforced even when `Content-Length` is absent. Project imports use `{ source: { type: "files", files } }`, `{ source: { type: "zip", base64 } }`, or `{ source: { type: "repository", integrationId, connectionId?, repository, ref } }`. Binary file entries carry `type: "artifact"` and `base64`; text entries carry `content`. Repository credentials remain in the selected integration adapter and never enter the request or response. Provider workflow routes accept only provider-neutral fields at the stable boundary. Vendor-specific options, when needed, stay inside `providerOptions`. Push and deployment effects execute against one immutable version and use the SDK's durable idempotency and history records. Tool-source routes accept only public registration configuration. `connect` returns either an existing healthy connection or a provider authorization URL; callback state restores the original tenant/user scope without a product session. Connection responses never contain credential bytes or secret-store references. ## Preview boundary The API does not invent a preview URL. When `createViby()` has a durable `preview` configuration, opt into the built-in lifecycle with `preview: true`. It reuses a ready preview for the immutable version and otherwise materializes the version, runs every configured `prepare` command, starts the server, waits for readiness, and persists the result: ```ts const viby = createViby({ framework: "farmjs", model, sandbox, preview: { files: [{ path: "preview.config.ts", content: "export const preview = true;\n" }], prepare: [{ command: "pnpm", args: ["install", "--frozen-lockfile"] }], start: { command: "pnpm", args: ["dev", "--host", "0.0.0.0"] }, port: 3000, }, }); const api = createVibyApi({ viby, authenticate, preview: true, }); ``` `preview.files` contains preview-only overrides written after the immutable version is materialized. It is useful for host allowlists and development-server settings that should never alter version history or raw downloads. Send `Accept: text/event-stream` to the preview route, or use `web.chats.versions.previewStream(...)`, to receive workspace, command, stdout/stderr, readiness, and final result events while startup is running. Pass a callback instead when the product needs a custom cache, deployment result, or complete `Response`. Without `preview`, preview routes return `501 preview_not_configured`. CORS, CSRF policy, rate limits, and sessions remain application responsibilities; durable sandbox preview cleanup is available through `user.previews.cleanupExpired()`. --- ## Shipped capabilities URL: /docs/capabilities The implemented SDK, adapter, integration, verification, and ownership inventory. # Shipped capabilities This inventory describes the current `@viby/sdk` source on `main`. “Shipped” means an implemented, typed contract with automated coverage. It does not mean Viby hosts the resource or owns its credentials. ## Foundation and ownership | Capability | Surface | Ownership | | --- | --- | --- | | Framework-neutral project target | one `framework` string; typed built-ins, custom values, and automatic immutable framework skills | host selects; package skills cover common frameworks and host skills extend or define custom targets | | Runtime-neutral core | `@viby/sdk/core` with Web-standard contracts and helpers; Node and provider adapters use explicit subpaths | portable consumers do not load filesystem, process, crypto, migrations, Docker, or database clients | | Model selection | any AI SDK `LanguageModel` | host configures and owns provider credentials | | Generation engine | public provider-neutral engine plus conformance suite | host may replace the AI SDK shortcut with any agent, model runtime, or orchestrator | | Categorized skills | skills.sh slugs, `skillRead(...)` directories, inline snapshots, and provider-neutral resolvers | host selects; Viby resolves and snapshots exact content | | Categorized storage | `storage.database`, `storage.artifacts`, `storage.connections`, and `storage.secrets`; `DATABASE_URL` remains the PostgreSQL shortcut | host selects each provider-neutral implementation independently | | Project environments | chat-scoped development, preview, production, and custom variables; public values plus redacted secret metadata | PostgreSQL is the default metadata store; secret bytes stay in `storage.secrets` and resolve only for runtime operations | | Structured database | provider-neutral database factory or raw `PersistenceAdapter` plus conformance suite | host may provide another durable implementation and owns its credentials and migrations | | Binary artifact storage | provider-neutral `ArtifactStore`, conformance suite, filesystem reference adapter, and S3-compatible adapter for AWS S3, R2, MinIO, and compatible stores | host selects storage and owns its credentials; the database keeps metadata and opaque references | | Tenant isolation | `viby.forUser({ tenantId, userId })` | host authenticates; every Viby query enforces both IDs | | Viby API key | none | no managed Viby control plane is required | ## Conversations, generation, and source | Capability | Shipped surface | | --- | --- | | Chats | create, import, list, nested metadata filters, get, update, soft delete, restore, and retention-aware purge | | Messages | cursor pagination, lookup by ID, plain content, assistant finish reasons, and ordered typed parts | | Durable generation | synchronous convenience methods plus addressable async `Generation` handles | | Live updates | persisted event cursors, resumable async iterators, standard SSE, and Web `Response` helpers | | Recovery | cancel, retry, resume, immutable attempts, failures, and usage | | Steering | durable queued/applied user instructions, idempotency, attachments, safe-boundary agent consumption, resumable events, Web API, and MCP tool | | Durable workers | Postgres work claims, leases, heartbeats, fencing, and host-controlled concurrency | | Blocking work | typed plan, question, and permission tasks with durable resolution | | Agent trace | started, delta, completed, and failed events on the normal generation cursor | | Tool records | typed arguments/results, redaction, ownership, status, and external-effect idempotency | | Durable tool-source registry | tenant/user-scoped provider-neutral registrations, public configuration, adapter materialization, enable/disable/archive lifecycle, explicit per-chat selection, durable authorization connections, immutable generation-time registration snapshots, and an MCP registration adapter | | Tool-source connections | adapter-owned OAuth/authorization, hashed single-use state, callback substitution protection, refresh/revoke, durable connection metadata, isolated secret-store credentials, and a reusable provider-neutral adapter conformance suite | | Source import | validated UTF-8 file lists, ZIP archives, and provider-neutral source adapters | | Source policy | immutable locked files enforced across import, direct edits, model changes, and workspace tools | | Source history | immutable parent-linked versions, ordered changes, fork, restore, and message lookup | | Agent workspace | read, search, stage edits, inspect changes, and atomically commit an immutable child version | | Artifacts | framework-native source ZIP bytes and standard download `Response` | ## Sandboxes and previews The core contract branches on declared capabilities, never provider names. | Capability | Shipped surface | | --- | --- | | Common adapter | files, commands, output streaming, port URLs, background processes, reconnect, and snapshots | | Discovery | normalized `SandboxCapabilities` plus `supports(...)` checks | | Lifecycle | materialize an immutable version, idempotent cleanup, durable leases, and reconnect by lease ID | | Command safety | provider-neutral allow/deny/approval-required policy with bounded command metadata | | Agent execution | capability-gated sandbox tools with step, time, token, command, and output budgets | | Durable version previews | immutable-version materialization, preview-only file overlays, coalesced concurrent starts, live provider-neutral phases and stdout/stderr, tracked server startup, HTTP readiness, persisted URL/status/failure/expiry, reconnect, stop, and expired-session cleanup | | Conformance | reusable adapter test suite with caller-supplied harmless commands | | Included adapters | E2B, Vercel Sandbox, local Docker, Daytona, Modal, and Cloudflare Sandbox | Preview URLs exist only when the configured adapter exposes port URLs and background processes. `version.preview()` starts the configured framework server and persists its lifecycle, but Viby does not promise a globally hosted preview URL. The [reference application](../examples/reference) demonstrates the complete host composition. ## Browser inspection | Capability | Shipped surface | | --- | --- | | Common adapter | provider-neutral browser open/session contract with no driver-specific types | | Navigation | same-origin-by-default URL resolution with portable load states and timeouts | | Visual evidence | PNG/JPEG screenshot bytes with validated dimensions and defensive copies | | Inspection | bounded DOM HTML/text snapshots and normalized console errors | | Quality | provider-neutral accessibility issue/report vocabulary and readiness checks | | Conformance | reusable lifecycle suite against a caller-owned reachable page | | Included adapter | Playwright Chromium/Firefox/WebKit with axe-core accessibility scans and sandbox preview composition | | Visual workflows | multi-page capture, durable artifact references, configurable rule/model/agent gates, and immutable design-evaluation evidence | ## Integration, delivery, and observability | Capability | Shipped surface | | --- | --- | | Inbound tools | provider-neutral sources, per-chat selection, read/write/external effects, stable idempotency, durable permission tasks, and redacted call records | | MCP client | Streamable HTTP and custom-transport adapter with per-chat connection isolation; static headers or adapter-resolved credentials remain inside the transport factory | | MCP server | `registerVibyMcpTools` exposes scoped chats, generations, events, steering, task resolution, versions, iteration, and downloads through the official MCP server SDK | | Outbound events | signed CloudEvents-style envelopes with stable IDs and constant-time verification | | Durable delivery | database claims, retry backoff, lease fencing, inspection, dead letters, and explicit redrive | | HTTP streaming | `Last-Event-ID` parsing, standard SSE frames, request abort propagation, and Web `Response` headers | | Web API host | authenticated Web Request/Response routing for chats, messages, generation controls and steering, SSE/event pages, tasks, versions, iteration, ZIP downloads, tool-source registration/selection/connections, public provider callbacks, and host-owned previews | | Telemetry | provider-neutral hooks plus an OpenTelemetry-compatible tracer/meter adapter | | Cost attribution | host-defined currency/credit calculator, immutable attempt cost, cumulative generation cost, and usage parts | | Generation configuration | durable per-request model aliases, host instructions, categorized skill overlays, and JSON metadata | | Multimodal input | immutable attachment bytes in an external artifact store, lightweight PostgreSQL metadata, scoped retrieval, and standard AI SDK file parts | | Generated artifacts | durable images, audio, video, documents, and binary outputs with ownership, checksums, artifact-store references, and resumable creation events | | Binary project entries | immutable artifact-backed source paths with scoped external bytes across import, edits, history, sandbox materialization, and ZIP downloads | | Design evaluation | immutable version-bound rubric results, criterion scores, validated source/attachment/visual-artifact evidence, metadata, and cursor pagination | | Integration contracts | categorized `integrations.repository` and `integrations.deployment` configuration with provider-neutral authorization, repository, branch, commit, pull-request, project, and deployment adapter types | | Provider connections | tenant/user-scoped PostgreSQL metadata, hashed single-use authorization state, callback substitution protection, refresh, reconnect, local revocation, and provider selection discovery | | Integration secrets | standalone secret-store contract plus an AES-256-GCM PostgreSQL default keyed by `VIBY_SECRET_KEY`; credentials never enter normal SDK records | | Repository workflows | connected provider handles for owners, repositories, branches, source import, complete immutable snapshot pushes, optimistic conflicts, pull requests, and optional merges | | Repository history | durable chat-to-repository links plus version-bound pending, pushed, conflict, and failed records with commits, pull requests, errors, timestamps, and idempotent replay | | Repository conformance | reusable disposable-repository suite covering discovery, source round-trips, pushes, stale-head conflicts, and pull requests | | Included repository adapters | GitHub App installation/user verification, short-lived token refresh and revocation, and exact Git Data pushes; Bitbucket Cloud OAuth, rotating refresh tokens, workspace discovery, binary-safe source commits, branches, and pull requests | | Deployment workflows | connected provider handles for projects, immutable-version deployment, stable retry idempotency, status lookup, URLs, and optional cancellation | | Deployment history | durable chat-to-project links, version-bound deployment records, restart-safe idempotency, provider IDs and URLs, failures, and ordered status transitions | | Deployment preparation | adapter-declared source/prebuilt input; capability-gated sandbox install/build, immutable external build artifact, artifact reuse on retries, and unchanged raw-source downloads | | Environment injection | explicit sandbox environment selection plus automatic deployment/build selection; values never enter prompts, events, telemetry, histories, or command records | | Deployment conformance | reusable disposable-project suite covering creation, idempotent deployment, lookup, and cancellation | | Included deployment adapters | Vercel external-integration authorization with source deployment and cancellation; Cloudflare OAuth with multi-account Pages discovery, Wrangler-compatible prebuilt asset uploads, durable retry recovery, status, and URLs | Product authentication, provider-app registration, public callback routes, event scheduling, and transport infrastructure remain host-owned. Viby stores tenant-scoped repository, deployment, and tool-source connections plus delivery state, but does not run a hidden queue or scheduler. ## Verification and examples | Gate | Coverage | | --- | --- | | Unit suite | durable generation, source, agents, policy, adapters, MCP, SSE, telemetry, cost, delivery, and errors | | PostgreSQL integration | real migrations plus the complete persisted generation/iteration/download lifecycle | | Schema upgrade fixture | upgrades a disposable historical `0001`–`0004` database through the current schema and preserves data | | Migration immutability | published migration SHA-256 checksums; changes require a new migration | | API compatibility | frozen compile fixture and additive runtime export manifests for package entry points | | Sandbox integration | shared conformance suite plus a real local Docker integration job | | Reference E2E | standard API host request-level chat → SSE → preview → iterate → ZIP download through real Viby objects and deterministic adapters | | Generated-project quality matrix | Farm, TanStack Start, and a custom framework ID across generation, runtime checks, preview HTTP, iteration, evaluation, and ZIP parity | | Live provider verification | explicit, environment-gated GitHub, Bitbucket, Vercel, and Cloudflare tests with disposable resources and failure-safe cleanup | | Package smoke test | packed artifact install, public import, CLI, and exported subpaths | | Runtime compatibility | Node 20/22/24, Bun package import, portable dependency-graph guard, and Web Request/Response/streams/crypto behavior | ## Deliberately outside the current release - managed preview hosting; - managed authentication, billing, Postgres, workers, queues, secrets, or a Viby API key. The categorized contracts, durable connection lifecycle, repository orchestration, and deployment orchestration are shipped without pretending that a specific deployment vendor is present. Provider features enter through explicit adapters without weakening the framework-, runtime-, model-, or vendor-neutral boundary. --- ## Core concepts URL: /docs/concepts The ownership, durability, identity, versioning, and adapter model behind Viby. # Core concepts Viby is an embeddable product SDK, not a hosted control plane. Its contracts separate durable software-generation state from the application and infrastructure choices around it. ## Host-owned and SDK-owned concerns | The host application owns | Viby owns | | --- | --- | | authentication, authorization, billing, and product UI | tenant/user-scoped chats and messages | | model, provider, and infrastructure credentials | logical generations, immutable attempts, and ordered events | | PostgreSQL, object storage, sandboxes, browsers, and cloud accounts | typed tasks, tool records, and idempotency state | | framework instructions and product policy | immutable source versions, lineage, artifacts, and histories | | HTTP routes, queues, workers, cron, and observability backends | migrations for the dedicated `viby` schema | This division is intentional. Viby can coordinate a provider connection, for example, but the host still registers the provider application, authenticates the user starting the flow, and supplies the secret-storage boundary. ## Identity is explicit The root client has no ambient user. An authenticated product request must call: ```ts const user = viby.forUser({ tenantId, userId }); ``` The returned `ScopedViby` is the only entry to chats, generations, previews, tool registrations, and user-scoped integrations. Passing identity explicitly makes the same SDK safe to use from HTTP handlers, queues, workers, scripts, and tests. ## A chat is a durable project timeline A chat contains messages and an ordered graph of immutable source versions. It may begin empty, from a file list, from a ZIP, or through a source-import adapter. Chat metadata is application-owned JSON and can be used for search and product state; it must not contain credentials. Deleting a chat creates a retention tombstone. `restore()` is available until the record is purged. Permanent purge removes scoped child records and associated artifact bytes according to the configured retention policy. ## A generation is logical; an attempt is physical A generation represents one user intent. Initial execution, retry, resume, and task resolution each append an immutable attempt to that logical generation. This preserves failures, model attribution, usage, cost, and worker ownership instead of rewriting history. Generation events are persisted before subscribers see them. Cursors are opaque and monotonically ordered within a generation. A client can reconnect from its last acknowledged cursor without depending on a provider's transient stream identifier. ## A version never changes Successful generation, direct source changes, imports, restore, and workspace commits create new versions. A version records its parent, origin, complete source tree, ordered change set, and any artifact-backed binary entries. Pushes, deployments, previews, evaluation, and downloads always name an immutable version. This gives the product stable provenance: a deployment or pull request can be traced to the exact source the user approved. ## Adapters are capability boundaries The core contracts use provider-neutral interfaces for generation engines, persistence, artifacts, skill resolution, source import, tool sources, sandboxes, browsers, repositories, and deployments. Provider packages implement those interfaces behind explicit subpath exports. Application code should branch on declared capability or returned state, not provider names. For example, `session.supports("backgroundProcesses")` is portable; checking for `provider === "e2b"` is not. ## Secrets never become ordinary records Provider tokens and secret project variables live behind `storage.secrets`. Public records retain only redacted metadata and opaque references. Resolved secret bytes may enter a selected sandbox, build, deployment, or adapter call at runtime, but they do not enter prompts, messages, event logs, telemetry attributes, repository history, deployment history, or command records. ## Preview and deployment are different A preview is a durable Viby record around a sandbox process and its readiness state. It has a URL only when the selected sandbox can expose a port. A deployment is a provider-owned release result recorded through a deployment adapter. Viby does not invent either URL when no provider exists. ## Embedded and worker execution Embedded execution starts work in the application process after the durable attempt is created. Worker execution leaves attempts queued for a separately run `GenerationWorker`. Worker claims use leases, heartbeats, and fencing so a stale process cannot commit after another worker takes over. The contract is at-least-once. External effects must use Viby idempotency keys or an equivalent provider mechanism. See [Generations and events](/docs/api/generations) for lifecycle semantics and [Package entry points](/docs/api/entry-points) for runtime boundaries. --- ## Credentials and provider setup URL: /docs/credentials Create the server credentials Viby needs, connect user-owned providers, and verify the setup without exposing secrets. # Credentials and provider setup Viby is bring-your-own-infrastructure. Your product owns its database, model account, provider applications, and server environment. Viby uses those server credentials to create durable, tenant-scoped connections; it never returns provider tokens to the browser or places secrets in prompts, messages, events, telemetry, or ordinary records. There are two kinds of credentials: | Kind | Example | Scope | Storage | | --- | --- | --- | --- | | Host credential | OpenAI key, GitHub App private key, Vercel client secret | Your Viby product | Server environment or host secret manager | | User connection | A user's GitHub installation or Vercel account grant | One tenant and user | Encrypted by the configured Viby secret store | Do not ask end users to paste provider access tokens into the product. Configure one provider application for the host, then let each user complete the provider's authorization screen. ## 1. Create the foundation credentials Install Viby and the model adapter used by this example: ```bash npm install @viby/sdk ai @ai-sdk/openai ``` Add the database and model key to a server-only environment file: ```bash title=".env.local" DATABASE_URL=postgresql://postgres:postgres@localhost:5432/viby OPENAI_API_KEY= OPENAI_MODEL= ``` `OPENAI_API_KEY` is read by `@ai-sdk/openai`, not persisted by Viby. Use any other AI SDK model or a custom generation engine when OpenAI is not the desired provider. Run the Viby-owned migrations: ```bash npx viby db migrate ``` Connections and project secrets need a separate 32-byte encryption key. Generate one once, store it in the host's secret manager, and keep it stable across restarts: ```bash openssl rand -hex 32 ``` ```bash title=".env.local" VIBY_SECRET_KEY= ``` Rotating this key requires re-encrypting or reconnecting existing provider credentials. Never reuse the database password, an OAuth client secret, or a model key as `VIBY_SECRET_KEY`. ## 2. Choose the Vercel credential you actually need Vercel Sandbox and user-owned Vercel deployments solve different problems and use different credentials. ### Run generated code in Vercel Sandbox Use this when Viby needs an isolated build or preview runtime owned by your product. Enable **Secure backend access with OIDC federation** in the linked Vercel project's Security settings. Vercel then manages the workload credential in production. For local development, link the host project and pull its development environment: ```bash npx vercel login npx vercel link npx vercel env pull .env.local --environment=development ``` Do not commit the pulled file. The Vercel SDK can read the resulting short-lived workload credential without placing a long-lived token in application code: ```ts import { vercelSandbox } from "@viby/sdk/sandbox/vercel"; const sandbox = vercelSandbox({ name: ({ chatId }) => `viby-${chatId.slice(0, 12)}`, }); ``` For a non-Vercel host, pass an explicit `token`, `teamId`, and `projectId` together. Keep all three server-only. See Vercel's [OIDC guide](https://vercel.com/docs/oidc) for issuer settings and local token refresh behavior. ### Deploy into each user's Vercel workspace Use a Vercel Integration when the user should select and authorize their own personal or team workspace. 1. Open the Vercel Integration Console and create an external integration. 2. Choose a stable URL slug. The `slug` passed to `vercel()` must exactly match this value. 3. Set the redirect URL to your public Viby callback, for example `https://app.example/api/viby/integrations/callback`. 4. Grant Project and Deployment read/write access. 5. Copy the client ID and client secret into the server environment. ```bash title=".env.local" VERCEL_CLIENT_ID= VERCEL_CLIENT_SECRET= VERCEL_INTEGRATION_SLUG= ``` ```ts import { vercel } from "@viby/sdk/integrations/vercel"; const deployment = vercel({ clientId: env.VERCEL_CLIENT_ID, clientSecret: env.VERCEL_CLIENT_SECRET, slug: env.VERCEL_INTEGRATION_SLUG, }); ``` If `https://vercel.com/integrations//new` returns `404`, verify the slug from the integration's public URL, confirm the integration is available to the signed-in account or team, and ensure the integration is not still missing required listing fields. A client ID is not an integration slug. See the [Vercel deployment integration](/docs/integrations/vercel) for project creation, idempotent deployments, provider options, and durable history. Vercel's current [integration creation guide](https://vercel.com/docs/integrations/create-integration) documents the console fields, visibility, and credential rotation behavior. ## 3. Create the GitHub App Use a GitHub App rather than a personal access token. One host-owned app can let every Viby user choose the account, organization, and repositories they want to connect. Create the app under the GitHub account or organization that owns the product and configure: - Homepage URL: the public product URL. - Callback URL: `https://app.example/api/viby/integrations/callback`. - Request user authorization during installation: enabled. - Setup URL: the product page users should return to after installation, if one is used. - Metadata: read. - Contents: read and write. - Pull requests: read and write. - Administration: read and write only when the product creates repositories. Generate and download a private key after creating the app. Then copy the app ID, OAuth client ID, OAuth client secret, private key, and URL slug into the server environment: ```bash title=".env.local" GITHUB_APP_ID= GITHUB_APP_CLIENT_ID= GITHUB_APP_CLIENT_SECRET= GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----" GITHUB_APP_SLUG= ``` The private key may contain literal newlines or escaped `\n` sequences. Do not base64 it unless your own secret manager performs the matching decode before calling Viby. ### Automate creation with a GitHub App manifest A product setup command can create the app interactively without asking the developer to copy every form field. Render a form that posts a JSON `manifest` value to the personal or organization GitHub App creation URL: ```ts const manifest = { name: "Viby Builder", url: "https://app.example", redirect_url: "https://app.example/setup/github/manifest", callback_urls: ["https://app.example/api/viby/integrations/callback"], setup_url: "https://app.example/settings/integrations", request_oauth_on_install: true, public: false, default_permissions: { contents: "write", pull_requests: "write", }, }; ``` After the developer approves GitHub's creation screen, exchange the returned `code` server-side at `POST https://api.github.com/app-manifests/{code}/conversions`. Move the returned app ID, client credentials, PEM private key, slug, and webhook secret directly into the host secret manager. Do not render or log the conversion response; the manifest code is short-lived and the generated private key should be treated as a one-time credential delivery. See GitHub's [App manifest reference](https://docs.github.com/en/apps/sharing-github-apps/registering-a-github-app-from-a-manifest) for the personal and organization form URLs and the full parameter list. Add a webhook URL and events only when the host application actually consumes them; Viby repository operations do not require a webhook. ```ts import { github } from "@viby/sdk/integrations/github"; const repository = github({ appId: env.GITHUB_APP_ID, clientId: env.GITHUB_APP_CLIENT_ID, clientSecret: env.GITHUB_APP_CLIENT_SECRET, privateKey: env.GITHUB_APP_PRIVATE_KEY, slug: env.GITHUB_APP_SLUG, }); ``` Install the app on a test account and choose **Only select repositories** for the first check. Viby verifies that the authorizing GitHub user can access the returned installation before it stores an encrypted connection. See the [GitHub repository integration](/docs/integrations/github) for import, push, branch, pull-request, conflict, and source-limit behavior. ## 4. Configure the optional providers Cloudflare and Bitbucket use ordinary OAuth clients. Their client secrets are host credentials; their returned user tokens stay encrypted behind the Viby secret-store boundary. ### Cloudflare Pages Create a server-side OAuth client with the Authorization Code grant, the exact Viby callback URL, Pages Write access, and account-read access. Copy the scope identifiers exactly as configured by Cloudflare: ```bash title=".env.local" CLOUDFLARE_CLIENT_ID= CLOUDFLARE_CLIENT_SECRET= CLOUDFLARE_OAUTH_SCOPES= ``` Cloudflare Pages accepts prebuilt assets, so the host must also configure a sandbox, an artifact store, and the framework's build command. The [Cloudflare deployment integration](/docs/integrations/cloudflare) documents that complete path. ### Bitbucket Cloud Create an OAuth consumer in the Bitbucket workspace with the callback URL and these permissions: Account read; Repositories read/write; Repository admin only for repository creation; Pull requests read/write. ```bash title=".env.local" BITBUCKET_CLIENT_ID= BITBUCKET_CLIENT_SECRET= ``` See the [Bitbucket repository integration](/docs/integrations/bitbucket) for workspace discovery, imports, pushes, pull requests, and Bitbucket's disconnect limitation. ## 5. Configure Viby once Provider IDs are application-owned keys. The product can rename them, show friendly labels, and offer only the providers it supports without changing the repository or deployment workflow. ```ts import { createViby } from "@viby/sdk"; import { openai } from "@ai-sdk/openai"; import { github } from "@viby/sdk/integrations/github"; import { bitbucket } from "@viby/sdk/integrations/bitbucket"; import { cloudflare } from "@viby/sdk/integrations/cloudflare"; import { vercel } from "@viby/sdk/integrations/vercel"; export const viby = createViby({ framework: "farmjs", model: openai(env.OPENAI_MODEL), integrations: { repository: { github: github({ appId: env.GITHUB_APP_ID, clientId: env.GITHUB_APP_CLIENT_ID, clientSecret: env.GITHUB_APP_CLIENT_SECRET, privateKey: env.GITHUB_APP_PRIVATE_KEY, slug: env.GITHUB_APP_SLUG, }), bitbucket: bitbucket({ clientId: env.BITBUCKET_CLIENT_ID, clientSecret: env.BITBUCKET_CLIENT_SECRET, }), }, deployment: { vercel: vercel({ clientId: env.VERCEL_CLIENT_ID, clientSecret: env.VERCEL_CLIENT_SECRET, slug: env.VERCEL_INTEGRATION_SLUG, }), cloudflare: cloudflare({ clientId: env.CLOUDFLARE_CLIENT_ID, clientSecret: env.CLOUDFLARE_CLIENT_SECRET, scopes: env.CLOUDFLARE_OAUTH_SCOPES.split(" ").filter(Boolean), }), }, }, }); ``` Omit providers that are not configured. Do not pass empty credentials to their factories. ## 6. Mount one callback and connect a user The Web API host already exposes `GET` and `POST /integrations/callback`. If the product mounts Viby operations manually, use the same Web-standard callback for every provider: ```ts export async function integrationCallback(request: Request) { const completed = await viby.integrations.callback(request); return Response.redirect(new URL(completed.returnTo, request.url)); } ``` Start a connection from the authenticated product session: ```ts const user = viby.forUser({ tenantId, userId }); const connection = await user.integrations.repository.connect("github", { callbackUrl: "https://app.example/api/viby/integrations/callback", returnTo: `/projects/${projectId}/settings`, }); if (connection.status === "authorization-required") { return Response.redirect(connection.url); } ``` Viby hashes and expires the authorization state, binds it to the tenant, user, category, provider, and callback URL, and consumes it once. The provider access token never crosses this server boundary. ## 7. Verify before production Use a disposable provider account, repository, and project for the first end-to-end check: 1. Run `npx viby db migrate` against the intended database. 2. Start one provider connection and complete its authorization screen. 3. Reload the connection after restarting the host process. 4. Import or generate a small project. 5. Push it to a disposable branch and open a draft pull request. 6. Create a preview deployment, refresh its status, and reload deployment history. 7. Delete the disposable provider resources and disconnect the Viby connection. Repository maintainers can also run the environment-gated live-provider suite against disposable resources. It is intentionally separate from ordinary package CI and the public integration flow. Before shipping, confirm that: - every secret exists only in a server environment or secret manager; - `.env*`, private keys, and pulled Vercel environment files are ignored by Git; - development, preview, and production use separate callback URLs and provider registrations when their providers require exact redirects; - logs redact authorization codes, access tokens, private keys, database URLs, and model keys; - `VIBY_SECRET_KEY` is backed up and access is restricted; - provider permissions are the minimum needed for the product features you expose. --- ## Quickstart URL: /docs/getting-started Create, stream, iterate on, and download a durable generated project. # Quickstart This guide builds the smallest complete Viby workflow: configure the SDK, bind a user, create a chat, stream a generation, iterate from its immutable version, and download the resulting source. ## Requirements - Node.js 20 or newer for the full client and built-in PostgreSQL adapter; - a PostgreSQL database reachable through `DATABASE_URL`; - an AI SDK-compatible model and that provider's server-side credentials. Install the SDK, AI SDK, and the provider package used by your application: ```bash npm install @viby/sdk ai @ai-sdk/openai ``` ## 1. Prepare the database Set `DATABASE_URL` in the server environment, then run the Viby-owned migrations during deploy or release setup. Application requests never run migrations implicitly. ```bash DATABASE_URL=postgresql://postgres:postgres@localhost:5432/viby npx viby db migrate ``` Viby creates a dedicated `viby` schema. It does not read or modify the application's authentication, organization, or billing tables. ## 2. Create one server-side client ```ts import { createViby } from "@viby/sdk"; import { openai } from "@ai-sdk/openai"; export const viby = createViby({ framework: "farmjs", model: openai("your-model-id"), skills: { design: ["farming-labs/design-engineer"], }, }); ``` `framework` is one type-safe string and may be a built-in suggestion or an application-defined identifier. The model provider reads its own credential from the server environment; Viby never persists that credential. Keep the root client long-lived and call `viby.close()` during graceful process shutdown. ## 3. Bind application identity ```ts const user = viby.forUser({ tenantId: organization.id, userId: session.user.id, }); ``` Both values are required. Every durable query and mutation is constrained by the pair. Viby does not infer identity from cookies, request globals, or application tables. ## 4. Create and generate ```ts const chat = await user.chats.create({ title: "Analytics dashboard" }); const generation = await chat.start({ prompt: "Build a polished SaaS analytics dashboard with complete loading and empty states.", metadata: { source: "new-project" }, }); for await (const event of generation.stream()) { renderGenerationEvent(event); saveCursor(event.cursor); } const outcome = await generation.wait(); ``` `chat.start()` returns after the durable generation and its first attempt exist. `stream()` reads persisted events in cursor order; disconnecting a subscriber does not cancel the generation. Pass the last acknowledged cursor through `stream({ after })` when reconnecting. The final outcome is a discriminated union: - `succeeded` includes the immutable result `version`; - `waiting` includes typed plan, question, or permission tasks; - `failed` includes a durable safe error message; - `cancelled` includes the recorded cancellation reason. ## 5. Resolve blocking work ```ts if (outcome.status === "waiting") { const task = outcome.tasks[0]; if (task?.kind === "permission") { await generation.resolve({ taskId: task.id, resolution: { kind: "permission", decision: "allow" }, }); await generation.wait(); } } ``` Resolving a task appends a new immutable attempt. It does not overwrite the paused attempt or repeat an already recorded external effect. ## 6. Iterate from an exact version ```ts const version = await chat.latestVersion(); if (!version) throw new Error("The generation did not produce a version"); const refined = await version.iterate({ prompt: "Tighten the visual hierarchy and add a useful empty state to every table.", }); ``` Versions are immutable and parent-linked. Iteration always names its base version, so concurrent branches, restore, audit, and reproducible downloads remain possible. ## 7. Download raw source ```ts const artifact = await refined.download(); return artifact.toResponse(); ``` The response is a framework-native ZIP assembled from the selected immutable version. It excludes secrets, dependency directories, lockfiles, build output, and provider-specific deployment state. ## Production checklist - Authenticate every request before calling `forUser`. - Run `viby db migrate` as a release step, not in request handling. - Use worker execution for generations that must outlive a request process. - Configure an external artifact store when attachments or generated binaries should not live on one local filesystem. - Configure a secret store before enabling provider connections, private tool sources, or secret project environment variables. - Enforce a sandbox command policy before executing untrusted generated commands. - Persist the last acknowledged event cursor in the product client. - Call `close()` during graceful shutdown and clean up expired previews on a host-owned schedule. Continue with [Core concepts](/docs/concepts) or open the [client reference](/docs/api/client). --- ## Bitbucket repository integration URL: /docs/integrations/bitbucket Connect Bitbucket Cloud, import repositories, push immutable versions, and open pull requests. # Bitbucket repository integration `@viby/sdk/integrations/bitbucket` connects each product user to Bitbucket Cloud through an OAuth consumer and implements Viby's provider-neutral repository contract. The host owns the OAuth consumer, callback route, database, and secret-encryption key. Access and refresh tokens stay behind the Viby secret-store boundary. ## Bitbucket OAuth consumer Create an OAuth consumer in the Bitbucket workspace settings. Set its callback URL to the application route that calls `viby.integrations.callback(request)` and grant these permissions: - Account: read - Repositories: read, write, and admin if the product creates repositories - Pull requests: read and write Bitbucket configures permissions on the consumer. The authorization URL may include `scope`, but requesting fewer scopes does not reduce the consumer's configured grant. Keep the consumer secret only in the server environment. Set `DATABASE_URL` for the default durable connection store and `VIBY_SECRET_KEY` to an independent 32-byte encryption key. See Bitbucket's official [OAuth guide](https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/) and [repository API](https://developer.atlassian.com/cloud/bitbucket/rest/api-group-repositories/) for provider setup. ## SDK configuration ```ts import { createViby } from "@viby/sdk"; import { bitbucket } from "@viby/sdk/integrations/bitbucket"; const viby = createViby({ framework: "farmjs", model, integrations: { repository: { bitbucket: bitbucket({ clientId: env.BITBUCKET_CLIENT_ID, clientSecret: env.BITBUCKET_CLIENT_SECRET, }), }, }, }); ``` `apiUrl`, `authorizationUrl`, `tokenUrl`, `scopes`, `fetch`, and source safety limits can be overridden for compatible gateways or tests. The adapter is for Bitbucket Cloud; provider-specific types do not enter Viby's core repository interfaces. GitHub and Bitbucket can coexist without changing the calling workflow: ```ts integrations: { repository: { github: github({ /* GitHub App settings */ }), bitbucket: bitbucket({ /* OAuth consumer settings */ }), }, deployment: { vercel: vercel({ /* integration settings */ }), cloudflare: cloudflare({ /* OAuth settings */ }), }, } ``` ## Connect and handle the callback ```ts const user = viby.forUser({ tenantId, userId }); const connection = await user.integrations.repository.connect("bitbucket", { callbackUrl: "https://app.example/api/integrations/callback", returnTo: "/projects/new", }); if (connection.status === "authorization-required") { return Response.redirect(connection.url); } // In GET /api/integrations/callback: const completed = await viby.integrations.callback(request); return Response.redirect(new URL(completed.returnTo, request.url)); ``` Viby creates and verifies expiring, hashed, single-use state bound to the tenant, user, category, integration ID, and callback URL. The adapter exchanges the code server-side, loads the authorized Bitbucket identity, encrypts the rotating refresh token, and refreshes the short-lived access token before operations. Bitbucket Cloud does not document a programmatic OAuth token-revocation endpoint. Disconnect always revokes and deletes Viby's local encrypted connection, which prevents further SDK use, but the provider grant remains visible until the user removes it under Bitbucket **Personal settings → App authorizations**. Reconnecting creates a fresh local credential. ## Discover, import, push, and open a pull request ```ts const provider = user.integrations.repository.use("bitbucket"); const workspaces = await provider.owners.list(); const repositories = await provider.repositories.list({ owner: "acme" }); const chat = await user.chats.import({ source: provider.source({ repository: { owner: "acme", name: "starter" }, ref: { branch: "main" }, }), }); const version = await chat.latestVersion(); const result = await version!.push({ using: provider, repository: { owner: "acme", name: "generated-app", createIfMissing: true }, branch: { name: "feat/generated", from: "main", createIfMissing: true }, commit: { message: "feat: add generated app" }, providerOptions: { author: "Ada Lovelace " }, pullRequest: { base: "main", title: "feat: add generated app", providerOptions: { reviewers: ["{bitbucket-reviewer-uuid}"], closeSourceBranch: true, }, }, }); if (result.status === "conflict") { console.log(result.expectedHead, result.actualHead); } ``` Workspace, repository, branch, and directory pagination uses opaque cursors. Provider `next` URLs are accepted only when their origin and resource path match the configured Bitbucket API, preventing credential-bearing requests from following an untrusted cursor. Source import walks the complete commit tree with configured file, byte, and concurrency limits. It downloads bytes without text conversion, preserves executable attributes, validates every project path, and rejects symbolic links and subrepositories. Pushes use Bitbucket's [source commit endpoint](https://developer.atlassian.com/cloud/bitbucket/rest/api-group-source/). Viby compares the immutable version with the observed remote tree and sends binary-safe multipart additions, edits, executable attributes, and explicit deletions. The `parents` field prevents an observed commit from being silently replaced; an expected-head mismatch produces the portable typed conflict result. A retry with unchanged content returns the current commit without creating another effect. Pull-request creation and merge use Bitbucket's [pull request API](https://developer.atlassian.com/cloud/bitbucket/rest/api-group-pullrequests/). The portable `merge`, `squash`, and `rebase` choices map to Bitbucket's `merge_commit`, `squash`, and `fast_forward` strategies. A repeated merge first reads the pull request and returns an already-merged result without repeating the effect. All calls remain tenant/user scoped and require an active connection. Provider errors are wrapped by the connected handle as `IntegrationOperationError`; direct adapter consumers can inspect `BitbucketRepositoryError.status` and `.code`. --- ## Cloudflare deployment integration URL: /docs/integrations/cloudflare Connect Cloudflare accounts and deploy immutable prebuilt assets to Pages. # Cloudflare deployment integration `@viby/sdk/integrations/cloudflare` connects each product user to their own Cloudflare account and implements the provider-neutral deployment contract with Pages Direct Upload. The host owns the Cloudflare OAuth client, callback route, database, secret-encryption key, sandbox, artifact store, and declarative framework build command. Viby never exposes the resulting access or refresh token to the browser, a model, or a normal application record. ## Register the OAuth client Create a server-side OAuth client in Cloudflare and configure: - the Authorization Code grant; - `client_secret_basic` or `client_secret_post` token authentication; - the host application's exact integration callback URL; - the **Pages Write** permission and the account-read access needed to enumerate the accounts selected during consent. Cloudflare's client configuration uses scope identifiers. Pass those same identifiers to `scopes`; do not substitute display labels. Store the client secret only in the server environment. Set `DATABASE_URL` for the default durable connection store and `VIBY_SECRET_KEY` to a separate 32-byte encryption key for provider credentials. See Cloudflare's [OAuth client guide](https://developers.cloudflare.com/fundamentals/oauth/create-an-oauth-client/) and [Pages deployment API](https://developers.cloudflare.com/api/resources/pages/subresources/projects/subresources/deployments/methods/create/) for the current provider configuration. ## Configure Viby ```ts import { createViby } from "@viby/sdk"; import { cloudflare } from "@viby/sdk/integrations/cloudflare"; const viby = createViby({ framework: "farmjs", model, sandbox, storage: { artifacts, }, deployment: { preparation: { install: { command: "pnpm", args: ["install", "--frozen-lockfile"] }, build: { command: "pnpm", args: ["build"] }, outputDirectory: "dist", }, }, integrations: { deployment: { cloudflare: cloudflare({ clientId: env.CLOUDFLARE_CLIENT_ID, clientSecret: env.CLOUDFLARE_CLIENT_SECRET, scopes: env.CLOUDFLARE_OAUTH_SCOPES.split(" "), }), }, }, }); ``` The configuration key (`cloudflare` above) is the application-owned integration ID. It may be renamed without changing the provider. The factory name and returned `provider` identify the concrete adapter. ## Connect a user ```ts const user = viby.forUser({ tenantId, userId }); const connection = await user.integrations.deployment.connect("cloudflare", { callbackUrl: "https://app.example/api/integrations/callback", returnTo: `/projects/${projectId}`, }); if (connection.status === "authorization-required") { // Redirect or open a popup to connection.url. } // In GET /api/integrations/callback: const completed = await viby.integrations.callback(request); return Response.redirect(new URL(completed.returnTo, request.url)); ``` Viby creates and validates single-use state and binds it to the tenant, user, category, integration ID, and exact callback URL. The adapter exchanges the verified short-lived code server-side, reads the authorized Cloudflare identity, encrypts both tokens, refreshes expiring credentials, and revokes the remote authorization on disconnect. One OAuth authorization may include multiple Cloudflare accounts. Viby snapshots their safe IDs and names into connection metadata while keeping credentials encrypted, and project listing spans them. Use the typed helper to populate an account picker. Project creation uses the only accessible account automatically or requires an explicit provider option when the user selected more than one: ```ts import { cloudflareAccounts } from "@viby/sdk/integrations/cloudflare"; const hosting = user.integrations.deployment.use("cloudflare"); const connection = (await user.integrations.deployment.connections("cloudflare"))[0]; const accounts = connection ? cloudflareAccounts(connection.account) : []; await hosting.projects.create({ name: "analytics-dashboard", providerOptions: { accountId: selectedAccountId, productionBranch: "main", }, }); ``` ## Deploy prebuilt immutable assets Cloudflare Pages Direct Upload accepts prebuilt assets, so the adapter declares `{ mode: "prebuilt", outputDirectory: "dist" }`. Viby automatically materializes the raw immutable version in the configured sandbox, runs the framework build contract, stores a checked immutable ZIP through `storage.artifacts`, and then sends the built files to Cloudflare: ```ts const version = await chat.latestVersion(); const deployment = await version!.deploy({ using: hosting, project: { id: cloudflareProjectId }, environment: "preview", providerOptions: { assetsDirectory: "dist", branch: "feat/analytics", commitMessage: "Ship analytics dashboard", }, }); deployment.status; deployment.url; const [record] = await version!.deployments(); const build = record ? await version!.deploymentArtifact(record.id) : null; ``` Set `deployment.preparation.outputDirectory` and `providerOptions.assetsDirectory` to the same non-default directory when the framework does not emit `dist`. If the output contains no files, preparation fails before making provider calls. The raw version remains available through `version.download()` and is never changed or exposed as public assets. `_headers`, `_redirects`, `_routes.json`, `_worker.js`, `_worker.bundle`, and the functions routing configuration are sent through Cloudflare's dedicated multipart fields. The adapter uses Cloudflare's content-addressed upload protocol: Wrangler-compatible BLAKE3 hashes, missing-asset discovery, bounded batches, MIME metadata, and hash-cache upserts. Cloudflare's published Direct Upload limits are enforced by default: 20,000 files and 25 MiB per file. Hosts may lower those limits but cannot configure the adapter above the provider maximum. Retries are durable. A stable Viby idempotency key becomes a deterministic commit hash; the adapter looks for an existing Pages deployment with that hash before uploading or creating another effect. Production omits the branch so Cloudflare uses the project's production branch. Preview and custom environments use a provider branch. Cloudflare only reports `preview` or `production` in returned deployment records. ## Deployment lookup and cancellation ```ts const current = await hosting.deployments.get({ id: deployment.id }); ``` Cloudflare's lookup endpoint needs an account ID, project name, and deployment ID, so the adapter returns an opaque `cfp.` deployment identifier containing that routing information. Store and pass it back unchanged; do not parse it. Pages exposes deployment deletion, retry, and rollback, but not cancellation with the same semantics as the provider-neutral optional cancellation operation. The adapter therefore does not advertise `cancelDeployment` and does not mislabel deletion as cancellation. All operations are tenant/user scoped and require an active connection. Provider errors are wrapped by the connected handle as `IntegrationOperationError`; direct adapter consumers can inspect `CloudflareDeploymentError.status` and `.code`. The Direct Upload behavior follows Cloudflare's [prebuilt-assets requirement and limits](https://developers.cloudflare.com/pages/get-started/direct-upload/). Preparation stays provider-neutral: the Cloudflare adapter declares only the input shape it requires, while the host chooses the framework command, sandbox implementation, and artifact store. --- ## GitHub repository integration URL: /docs/integrations/github Connect a GitHub App installation, import source, push immutable versions, and open pull requests. # GitHub repository integration The GitHub adapter connects each Viby user to a GitHub App installation while keeping GitHub credentials inside Viby's secret-store boundary. ## GitHub App configuration Create a GitHub App owned by your product or organization and configure: - a callback URL pointing to the application route that calls `viby.integrations.callback(request)`; - **Request user authorization (OAuth) during installation**, which lets Viby verify that the signed-in GitHub user can access the returned installation; - repository **Metadata: read**; - repository **Contents: read and write**; - repository **Pull requests: read and write**; - repository **Administration: read and write** if the product will create repositories. GitHub warns that an `installation_id` received by a setup URL can be spoofed. The adapter therefore exchanges the installation OAuth code, lists installations accessible to that user, verifies the selected installation, and only then creates its one-hour installation token. The user token and installation token are encrypted by the configured Viby secret store and never appear in public connection objects. See GitHub's documentation for [sharing an app with installation state](https://docs.github.com/en/apps/sharing-github-apps/sharing-your-github-app), [setup URL security](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/about-the-setup-url), and [installation access tokens](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app). ## SDK configuration ```ts import { createViby } from "@viby/sdk"; import { github } from "@viby/sdk/integrations/github"; const viby = createViby({ framework: "farmjs", model, integrations: { repository: { github: github({ appId: env.GITHUB_APP_ID, clientId: env.GITHUB_APP_CLIENT_ID, clientSecret: env.GITHUB_APP_CLIENT_SECRET, privateKey: env.GITHUB_APP_PRIVATE_KEY, slug: "viby", }), }, }, }); ``` `privateKey` accepts a PEM string, including the common environment-variable form containing escaped `\\n` line breaks. `apiUrl`, `webUrl`, `installationUrl`, `apiVersion`, and `fetch` can be overridden for compatible enterprise or test environments. ## Connect and handle the callback ```ts const user = viby.forUser({ tenantId, userId }); const connection = await user.integrations.repository.connect("github", { callbackUrl: "https://app.example/api/integrations/callback", returnTo: "/projects/new", authorization: { account: "existing" }, }); if (connection.status === "authorization-required") { return Response.redirect(connection.url); } // In GET /api/integrations/callback: const completed = await viby.integrations.callback(request); return Response.redirect(new URL(completed.returnTo, request.url)); ``` Use `authorization.account` to make the account intent explicit: - `"existing"` runs the GitHub App OAuth flow, lists installations accessible to the authorized user, and reconnects the only available installation without asking them to reinstall the app. - `"new"` opens the GitHub App installation flow for a new account or organization. - omit `authorization` to preserve the install-first behavior used by earlier SDK versions. When the authorized user can access more than one installation, provide the selected provider identifier after an account-selection step: ```ts await user.integrations.repository.connect("github", { callbackUrl: "https://app.example/api/integrations/callback", returnTo: "/projects/new", authorization: { account: "existing", externalAccountId: "155948974", }, }); ``` The preference is provider-neutral and is also accepted by the Web API and typed web client. Products can present “Use installed app” and “Install on another account” without introducing a GitHub-specific route at their SDK boundary. The callback state is hashed, expiring, and single-use. GitHub user tokens are refreshed when GitHub enables expiration; installation tokens are renewed before their one-hour expiry. Disconnect attempts to revoke both tokens and always revokes the local connection. ## Import and push ```ts const githubHandle = user.integrations.repository.use("github"); const chat = await user.chats.import({ source: githubHandle.source({ repository: { owner: "acme", name: "starter" }, ref: { branch: "main" }, }), }); const version = await chat.latestVersion(); const result = await version!.push({ using: githubHandle, repository: { owner: "acme", name: "generated-app", createIfMissing: true }, branch: { name: "feat/generated", from: "main", createIfMissing: true }, commit: { message: "feat: add generated app" }, pullRequest: { base: "main", title: "feat: add generated app", draft: true }, }); ``` Pushes use GitHub's blob, tree, commit, and reference APIs. Each tree is built without `base_tree`, so the remote commit exactly matches the immutable Viby version: additions, changes, executable bits, binary bytes, and deletions are all represented. References are never force-updated. If the observed head changed, Viby returns a typed conflict with the actual head. Source reads reject truncated trees, symlinks, submodules, invalid paths, and configured file/byte limits. The defaults are 5,000 files and 25 MB. --- ## Vercel deployment integration URL: /docs/integrations/vercel Connect Vercel accounts and deploy immutable framework source through the provider-neutral deployment contract. # Vercel deployment integration `@viby/sdk/integrations/vercel` connects each product user to their own Vercel account and implements the provider-neutral deployment contract. The host owns the Vercel Integration registration, OAuth credentials, callback route, database, and secret-encryption key. Viby never sends the resulting Vercel access token to the browser, a model, or a normal application record. ## Register the integration Create an external integration in Vercel's Integration Console and configure: - a stable URL slug; - the host application's integration callback URL as the Redirect URL; - read/write access for the Project and Deployment scopes. Store the assigned client ID and client secret only in the server environment. Set `DATABASE_URL` for the default durable connection store and `VIBY_SECRET_KEY` to a separate 32-byte encryption key for provider credentials. ## Configure Viby ```ts import { createViby } from "@viby/sdk"; import { vercel } from "@viby/sdk/integrations/vercel"; const viby = createViby({ framework: "farmjs", model, integrations: { deployment: { vercel: vercel({ clientId: env.VERCEL_CLIENT_ID, clientSecret: env.VERCEL_CLIENT_SECRET, slug: "viby", }), }, }, }); ``` The configuration key (`vercel` above) is the application-owned integration ID. It may be renamed without changing the provider. The factory name and returned `provider` identify the concrete adapter. ## Connect a user ```ts const user = viby.forUser({ tenantId, userId }); const connection = await user.integrations.deployment.connect("vercel", { callbackUrl: "https://app.example/api/integrations/callback", returnTo: `/projects/${projectId}`, }); if (connection.status === "authorization-required") { // Redirect or open a popup to connection.url. } // In GET /api/integrations/callback: const completed = await viby.integrations.callback(request); return Response.redirect(new URL(completed.returnTo, request.url)); ``` Viby creates and validates the single-use state, binds it to the tenant, user, category, integration ID, and exact callback URL, and then gives the verified callback to the adapter. The adapter exchanges Vercel's short-lived code with a form-encoded server request. When an installation belongs to a team, its authoritative `team_id` is encrypted with the token and automatically included in every project, file, and deployment request. Disconnecting calls Vercel's token revocation endpoint before removing the local encrypted credential. If remote revocation fails, Viby still revokes and deletes the local connection and reports `providerRevoked: false`. ## Deploy an immutable version ```ts const hosting = user.integrations.deployment.use("vercel"); const version = await chat.latestVersion(); const deployment = await version!.deploy({ using: hosting, project: { name: "analytics-dashboard", createIfMissing: true, providerOptions: { framework: null, buildCommand: "pnpm build", outputDirectory: "dist", }, }, environment: "preview", providerOptions: { skipAutoDetectionConfirmation: true, meta: { product: "viby" }, }, }); deployment.status; deployment.url; ``` The adapter validates the complete snapshot, looks for a previous deployment carrying the same Viby idempotency key, uploads every text or binary file by SHA-1, and creates a deployment from those immutable references. A retry after a worker or process restart therefore recovers the existing provider deployment instead of repeating the effect. Vercel's own content deduplication also makes repeated file uploads inexpensive. `preview` omits a Vercel target, `production` uses the production target, `staging` uses the staging target, and any other typed environment value is sent as a custom environment slug or ID. Project creation remains explicit through `createIfMissing`; a missing project is never silently created. Provider-specific build settings stay inside `providerOptions`, so the core deployment contract has no Vercel framework, command, environment-variable, or project types. Farm projects can use their existing package scripts and output contract without changing the SDK's top-level `framework` value. ## Direct provider operations ```ts const projects = await hosting.projects.list({ search: "dashboard" }); const project = await hosting.projects.get({ name: "analytics-dashboard" }); const current = await hosting.deployments.get({ id: deployment.id }); if (current?.status === "queued" || current?.status === "building") { await hosting.deployments.cancel({ id: current.id, idempotencyKey: `cancel:${current.id}`, }); } ``` All operations are tenant/user scoped and require an active connection. Provider errors are wrapped by the connected handle as `IntegrationOperationError`; direct adapter consumers can inspect `VercelDeploymentError.status` and `.code`. --- ## Viby SDK URL: /docs Durable, provider-neutral infrastructure for building AI software creation products. # Viby SDK Viby is a TypeScript SDK for products that generate, iterate on, preview, and export software. It provides the durable product layer around an AI model: scoped chats, resumable generations, immutable source versions, tool approvals, artifacts, previews, repository workflows, and deployments. Your application keeps control of authentication, billing, the product UI, model credentials, infrastructure accounts, and provider policy. Viby supplies typed orchestration and persistence without requiring a Viby-hosted control plane or a Viby API key. ## Install ```bash npm install @viby/sdk ai @ai-sdk/openai ``` Continue with the [Quickstart](/docs/getting-started) to configure PostgreSQL, create the client, stream a generation, iterate from an immutable version, and download the resulting source. ## Choose a starting point - [Quickstart](/docs/getting-started) — install Viby, migrate PostgreSQL, generate a project, stream progress, iterate, and download the result. - [Core concepts](/docs/concepts) — understand identity, ownership, immutable versions, durability, adapters, and secrets. - [SDK reference](/docs/api) — browse the public client surface and its runtime behavior. - [Credentials and provider setup](/docs/credentials) — configure server credentials and securely connect user-owned providers. - [Web API host](/docs/api-host) — expose the same operations through a Web-standard `fetch(Request): Promise` handler. ## The product workflow 1. Bind the SDK to an authenticated tenant and user. 2. Create a chat or import an existing project. 3. Start a durable generation and stream its persisted events. 4. Resolve questions or permissions, then resume without losing history. 5. Inspect, preview, evaluate, or iterate on an immutable version. 6. Download raw framework source, push it to a repository, or deploy it through an adapter. Every external capability is optional. A product can begin with PostgreSQL and one model, then add artifact storage, sandboxes, browsers, tool sources, repositories, or deployment providers without changing the core chat and version contracts. ## What Viby does not own Viby does not authenticate end users, store model-provider API keys, host a managed database, run a hidden queue, or promise a global preview URL. Those boundaries remain explicit so a Viby application can run in its preferred framework, runtime, cloud, and security model. See the [shipped capability inventory](/docs/capabilities) for the exact implemented surface and the [complete v1 contract](/docs/api/v1) for normative lifecycle details. --- ## Runtime boundaries URL: /docs/runtime Use the Web-standard core and keep Node, database, filesystem, browser, and provider code behind explicit entry points. # Runtime boundaries Viby publishes a Web-standard contract separately from adapters that require Node.js or a vendor SDK. ## Portable core Import shared types and Web API helpers from `@viby/sdk/core`: ```ts import { defineGenerationEngine, defineSkillResolver, generationEventStreamResponse, type ArtifactStore, type BrowserAdapter, type SandboxAdapter, } from "@viby/sdk/core"; ``` The core entry point has no Node.js filesystem, path, process, crypto, PostgreSQL, migration, Docker, or provider-adapter dependencies. It uses Web-standard `Request`, `Response`, `Headers`, `ReadableStream`, `AbortSignal`, `Uint8Array`, and `structuredClone` APIs. ## Node application entry points Existing root imports remain supported during the 0.x compatibility line. New Node applications may make the runtime boundary explicit with `@viby/sdk/node`. Node-specific behavior lives behind dedicated entry points: | Capability | Entry point | | --- | --- | | Viby client with default PostgreSQL behavior | `@viby/sdk/node` | | PostgreSQL database factory | `@viby/sdk/storage/postgres` | | PostgreSQL migration helpers | `@viby/sdk/node/migrations` | | PostgreSQL project-environment metadata | `@viby/sdk/environment/postgres` | | local files and skills.sh/GitHub skill resolution | `@viby/sdk/node/skills` | | filesystem artifacts | `@viby/sdk/artifact/filesystem` | | S3-compatible artifacts | `@viby/sdk/storage/s3` or `@viby/sdk/artifact/s3` | | local Docker sandbox | `@viby/sdk/sandbox/docker` | | Playwright browser | `@viby/sdk/browser/playwright` | | provider integrations and sandboxes | their existing explicit provider subpaths | Node adapters require Node.js 20 or newer. The package does not declare a global Node engine because doing so would incorrectly reject portable-core consumers such as Worker applications. ## Compatibility gates CI runs the unit suite and portable-core import guard on Node.js 20, 22, and 24, then imports and exercises the published core entry point on Bun. The import guard walks the complete emitted ESM dependency graph from `@viby/sdk/core` and fails if any reachable module imports a Node built-in. It also exercises Web `Request`, `Response`, `Headers`, streams, text encoding, and Web Crypto behavior. ---