# Daytona

Sandbox lifecycle and execution

- Package: @bijectionhq/daytona
- Category: Sandboxes
- Status: Included
- License: Apache-2.0
- Bijection source (repository access required): https://github.com/bijectionhq/daytona/tree/78de84a52a9046fdf8fdd8df1f5646a32fd046e6/packages/convex
- Upstream: https://github.com/daytona/integrations/tree/4f10159a3ae5faa31792bb59abaa52aeecb5546c/packages/convex
- Documentation: https://docs.bijection.com/components/using
- Directory: https://bijection.com/components/daytona

## Get the source

Bijection repository access is required. Request access at https://bijection.com/contact.

```sh
git clone https://github.com/bijectionhq/daytona.git
cd daytona
git checkout 78de84a52a9046fdf8fdd8df1f5646a32fd046e6
cd packages/convex
```

## Before using this component

Read the package README at the recorded revision for setup, dependencies, and API examples. Upstream documentation describes the original package and may differ from the Bijection adaptation. This source is included in Bijection's workspace. Catalog inclusion does not establish npm availability or runtime qualification. Check compatibility with your SDK and deployment before adoption. External providers run their own compute.

## Upstream package documentation

Recorded 2026-10-23. This describes @daytona/convex; check the Bijection adaptation separately.

The Daytona component for Convex lets you create and manage isolated Daytona sandboxes directly from Convex actions, execute shell commands and Python/other code inside them, and read/write files -- with every sandbox and execution recorded in reactive component tables. This eliminates the need to poll external APIs for execution status: your UI can subscribe to live query results that reflect sandbox state, exit codes, and output history as they change. The component talks to the Daytona REST API via plain fetch in Convex's default runtime, so no Node.js runtime or heavy SDK bundle is required.

### Use cases

- Building an AI coding agent that needs an isolated execution environment per user session, where the UI needs to show real-time status (running, completed, failed) without polling
- Implementing a code interpreter endpoint that creates a sandbox on demand, runs submitted Python or shell code, and returns structured output including exit codes
- Running long-duration ML training jobs using runBackground so the action returns immediately and a scheduler-driven poller streams logs and exit codes into queryable Convex tables
- Scoping sandboxes to individual users with the userKey option so each user's sandbox list and execution history is isolated and queryable per identity
- Serving live preview URLs for web processes running inside a sandbox, where the port is exposed and a signed URL is returned to the client

### How it works

After installing the package and registering the component in convex.config.ts via app.use(daytona), you set a DAYTONA_API_KEY environment variable on your Convex deployment. The Daytona client is instantiated once with components.daytona and used inside actions to call methods like createSandbox, run, runCode, writeFile, and getPreviewUrl. Sandboxes can be created from a named snapshot or a Docker image, with optional resource settings (cpu, memory, disk) applying only to image-based creation.

Every sandbox lifecycle event and command execution is written to internal component tables, which means listSandboxes, getSandbox, listExecutions, and getExecution are plain Convex queries that return live data without hitting the Daytona API. This is the core architectural benefit: agent and dashboard UIs drive their state from reactive queries rather than polling.

For commands that exceed the 10-minute Convex action timeout, runBackground starts the command in a sandbox session and returns an executionId immediately. A scheduler-driven poller then checks in periodically, streams logs into the execution row, and records the final exit code when the command finishes. Synchronous run and runCode calls default to a 540-second timeout and are capped at 570 seconds.

Authorization is handled in your own functions since components cannot access ctx.auth directly. The recommended pattern is to resolve a user identity in your action or query, then pass identity.subject as the userKey when creating or listing sandboxes, which scopes all bookkeeping to that user without any additional configuration.

### Upstream README

Run [Daytona](https://www.daytona.io) sandboxes from your Convex backend: create isolated sandboxes, execute shell commands and code, read/write files, and get live preview URLs — with sandbox and execution state tracked in reactive Convex tables your UI can subscribe to.

```ts
// convex/agent.ts
import { Daytona } from "@daytona/convex";
import { v } from "convex/values";
import { components } from "./_generated/api";
import { action } from "./_generated/server";

const daytona = new Daytona(components.daytona);

export const codeInterpreter = action({
  args: { code: v.string() },
  handler: async (ctx, args) => {
    const { sandboxId } = await daytona.createSandbox(ctx, {
      autoStopInterval: 15,
    });
    const { result, exitCode } = await daytona.runCode(ctx, {
      sandboxId,
      code: args.code,
      language: "python",
    });
    return { result, exitCode };
  },
});
```

Why a component instead of calling the Daytona API directly?

- **Reactive state** — every sandbox and execution is recorded in component tables. Drive agent UIs ("running…", exit codes, output history) from live Convex queries instead of polling.
- **Isolated bookkeeping** — the component keeps its tables in its own namespace; it can't touch your app's data, and your app manages it through one typed client.
- **No SDK bundle** — the component talks to the Daytona REST API with plain `fetch` in Convex's default runtime: no `"use node"`, no heavy dependencies, fast cold starts.

## Installation

```bash
npm install @daytona/convex
```

Set your Daytona API key on your deployment (create one at [app.daytona.io/dashboard/keys](https://app.daytona.io/dashboard/keys)):

```bash
npx convex env set DAYTONA_API_KEY dtn_...
```

Then create or update `convex/convex.config.ts` in your app. Components can't read your app's env vars, so pass the key down to the component:

```ts
// convex/convex.config.ts
import { defineApp } from "convex/server";
import { v } from "convex/values";
import daytona from "@daytona/convex/convex.config.js";

const app = defineApp({
  env: { DAYTONA_API_KEY: v.string() },
});
app.use(daytona, {
  env: { DAYTONA_API_KEY: app.env.DAYTONA_API_KEY },
});

export default app;
```

Passing it by reference means the component always sees the current value, so rotating the key doesn't need a redeploy.

Optional: `DAYTONA_API_URL` (self-hosted instances; defaults to `https://app.daytona.io/api`). Set it the same way. It isn't a secret, so it doesn't need passing down: the client forwards it with each call.

## Usage

Instantiate the client with the installed component. Credentials resolve from your deployment's environment variables, or pass them explicitly:

```ts
import { Daytona } from "@daytona/convex";
import { components } from "./_generated/api";

const daytona = new Daytona(components.daytona);
// or: new Daytona(components.daytona, { apiKey: "dtn_..." })
```

`runBackground` is the exception: it always uses the key passed down to the component in `convex.config.ts`.

### Sandbox lifecycle

```ts
export const create = action({
  args: {},
  handler: async (ctx) => {
    return await daytona.createSandbox(ctx, {
      snapshot: "my-snapshot",     // optional — org default when omitted
      autoStopInterval: 15,        // pause after 15 idle minutes (fs preserved)
      autoDeleteInterval: -1,      // never auto-delete
      userKey: "user_123",         // scope sandboxes to a user/agent (see below)
    });
    // → { sandboxId: "…", state: "started" }
  },
});
```

Sandboxes are created from a **snapshot** (yours, or the org default) *or* built from a Docker **image** — pass one or the other. `cpu`/`memory`/`disk` only apply to image-based creation; snapshots define their own resources:

```ts
await daytona.createSandbox(ctx, { image: "python:3.12", cpu: 2, memory: 4 });
```

Also available: `startSandbox`, `stopSandbox`, `deleteSandbox`, and `refreshSandbox` (re-syncs remote state into the component's tables; returns `null` if the sandbox no longer exists).

### Run commands and code

```ts
const { executionId, exitCode, result } = await daytona.run(ctx, {
  sandboxId,
  command: "python train.py",
  cwd: "/home/daytona/project",
  timeoutSeconds: 300,
});

const py = await daytona.runCode(ctx, {
  sandboxId,
  code: "print(sum(range(10)))",
  language: "python",
});
```

A stopped or archived sandbox is transparently restarted first (disable with `autoStart: false`). Every call records an execution row (`running` → `completed`/`failed`) with truncated output, so history and status are queryable.

For long-running commands, `runBackground` starts the command in a sandbox session and returns immediately — the launch action isn't held open for the command's duration (a lightweight scheduler-driven poller checks in periodically instead), and the command isn't bound to the 10-minute action ceiling:

```ts
const { executionId } = await daytona.runBackground(ctx, {
  sandboxId,
  command: "python train.py",
  cwd: "/home/daytona/project",
});
// Watch it reactively: a scheduler-driven poller streams logs into the row
// while it runs and records the exit code when it finishes.
export const training = query({
  // Component table IDs cross the boundary as plain strings.
  args: { executionId: v.string() },
  handler: async (ctx, args) => daytona.getExecution(ctx, args),
});
```

To let your backend react when the command finishes (instead of polling the row), pass an `onComplete` mutation — the component invokes it on every terminal state (`completed`, `failed`, or `cancelled`) with the outcome and an optional `context` you supply:

```ts
const { executionId } = await daytona.runBackground(ctx, {
  sandboxId,
  command: "cargo build",
  onComplete: internal.build.buildFinished, // (ctx, { executionId, status, exitCode, result, error, context })
  onCompleteContext: { appId },
  minPollMs: 500,   // first poll + backoff floor (default 1000, min 250)
  maxPollMs: 5000,  // backoff ceiling (default 10000, max 120000)
});
```

Cancel a running background execution with `daytona.cancelExecution(ctx, { executionId })` — it stops the command (kills its session), marks the row `cancelled`, and fires the `onComplete` handler.

A running background command does not reset the sandbox's [idle auto-stop timer](https://www.daytona.io/docs/sandboxes#what-resets-the-timer) — create sandboxes for long jobs with `autoStopInterval: 0`.

`runBackground` needs the key passed down as shown in [Installation](#installation). Its poller runs from the scheduler, outside any of your calls, so it reads `DAYTONA_API_KEY` from the component's env on every poll instead of carrying it in scheduled function args.

### Reactive state

These read the component's tables — no Daytona API call, and they update live:

```ts
export const sandboxes = query({
  args: {},
  handler: async (ctx) => daytona.listSandboxes(ctx, { userKey: "user_123" }),
});

export const executions = query({
  args: { sandboxId: v.string() },
  handler: async (ctx, args) => daytona.listExecutions(ctx, args),
});
```

Also: `getSandbox`, `getExecution`.

### Files

```ts
await daytona.writeFile(ctx, { sandboxId, path: "/home/daytona/app.py", content });
const text = await daytona.readFile(ctx, { sandboxId, path: "/home/daytona/app.py" });
const entries = await daytona.listFiles(ctx, { sandboxId, path: "/home/daytona" });
await daytona.deleteFile(ctx, { sandboxId, path: "/home/daytona/tmp", recursive: true });
```

`readFile`/`writeFile` move content as UTF-8 strings — the right default for code, configs, and logs. For binary files (images, PDFs, archives), use the byte variants, which pass raw `ArrayBuffer`s through untouched:

```ts
const png = await daytona.readFileBytes(ctx, { sandboxId, path: "/home/daytona/chart.png" });
await ctx.storage.store(new Blob([png], { type: "image/png" })); // e.g. into Convex file storage

await daytona.writeFileBytes(ctx, { sandboxId, path: "/home/daytona/data.zip", content: zipBuffer });
```

Either way, file content crosses the Convex function boundary, so keep individual files under Convex's 16 MiB argument/return limits.

### Preview URLs

```ts
const { url } = await daytona.getPreviewUrl(ctx, { sandboxId, port: 3000 });
```

Signed URLs (default) embed a short-lived token; pass `signed: false` for a standard URL + token pair.

### Authorization

Components can't see your app's `ctx.auth` — authenticate callers in your own functions and scope sandboxes with `userKey`:

```ts
export const mySandboxes = query({
  args: {},
  handler: async (ctx) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error("Unauthorized");
    return await daytona.listSandboxes(ctx, { userKey: identity.subject });
  },
});
```

### Limits & long-running work

- Convex actions time out after 10 minutes, so synchronous `run`/`runCode` commands are always bounded below that ceiling: `timeoutSeconds` defaults to 540 and is capped at 570. For longer jobs, use `runBackground` — it has no duration bound and holds no action open.
- Sandbox and execution rows are kept as audit history. They never clean themselves up — prune them in batches with `daytona.purgeSandboxes(ctx, { olderThanMs })` and `daytona.purgeExecutions(ctx, { olderThanMs })` (each call returns `hasMore`; terminal rows only, remote sandboxes untouched). `listSandboxes` accepts a `state` filter (e.g. `"started"` for a live count) and `listSandboxesPaginated` provides cursor pagination.
- Stored execution output is truncated (64 KB); the action's return value carries up to 4 MB.
- Sandboxes cost money while running: set `autoStopInterval`, and delete sandboxes you're done with. `refreshSandbox` reconciles records whose remote sandbox was removed out-of-band.

See [example/convex/example.ts](./example/convex/example.ts) for a complete example app.

Found a bug? Feature request? [File it here](https://github.com/daytona/integrations/issues).