@daytona/convex

中文文档
2.2.0发布于 2026/11/03Apache-2.0TypeScriptES Module零依赖构建来源已验证近 30 天下载 4220

Convex component for Daytona — create sandboxes, run commands and code, and manage sandbox files from your Convex backend, with sandbox and execution state tracked in reactive Convex tables

原始使用文档
文档目录13 节
  1. Installation
  2. Usage
  3. Sandbox lifecycle
  4. Run commands and code
  5. Reactive state
  6. Files
  7. Preview URLs
  8. Authorization
  9. Keeping sandbox state in sync (webhooks)
  10. Limits & long-running work
  11. Development
  12. Publishing
  13. License

Daytona Component for Convex

npm version

Run Daytona 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.

// 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

npm install @daytona/convex

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

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:

// 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:

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

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:

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

To share or persist files across sandboxes, mount Daytona volumes. Volumes can only be mounted when a sandbox is created; create the volume itself with the Daytona SDK, CLI, or dashboard, and wait until it's ready:

await daytona.createSandbox(ctx, {
  volumes: [
    { volumeId: "shared-models", mountPath: "/home/daytona/models" }, // volume ID or name
    { volumeId: "tenants", mountPath: "/home/daytona/data", subpath: "users/alice" }, // only this prefix
  ],
});

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

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:

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:

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 — create sandboxes for long jobs with autoStopInterval: 0.

runBackground needs the key passed down as shown in 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:

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

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 ArrayBuffers through untouched:

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

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:

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 });
  },
});

Keeping sandbox state in sync (webhooks)

Sandbox records reflect what the component last observed. When Daytona changes a sandbox on its own (auto-stop, auto-archive, auto-delete), the record lags until you call refreshSandbox. To keep the sandboxes table in sync in real time, connect a Daytona webhook. It's opt-in, and it takes three steps:

1. Mount the component's webhook route and pass the secret down in convex/convex.config.ts:

const app = defineApp({
  env: {
    DAYTONA_API_KEY: v.string(),
    DAYTONA_WEBHOOK_SECRET: v.optional(v.string()),
  },
});
app.use(daytona, {
  httpPrefix: "/daytona/",
  env: {
    DAYTONA_API_KEY: app.env.DAYTONA_API_KEY,
    DAYTONA_WEBHOOK_SECRET: app.env.DAYTONA_WEBHOOK_SECRET,
  },
});

2. Create a webhook endpoint in the Daytona Dashboard (how to create an endpoint):

  • Endpoint URL: https://<your-deployment>.convex.site/daytona/webhook (your deployment's HTTP actions URL, plus the prefix above)
  • Events: subscribe to sandbox.state.updated. The component ignores every other event, so subscribing to anything else only creates extra deliveries.

Create the endpoint in the same Daytona organization as your DAYTONA_API_KEY: webhooks are per organization, so an endpoint in another org never receives your sandboxes' events.

3. Copy the endpoint's signing secret into your deployment. Daytona generates a signing secret (starting with whsec_) for each endpoint you create and signs every delivery with it. In the Daytona Dashboard's Webhooks page, click your endpoint in the endpoints table; its details show the signing secret, ready to copy. Set it on your Convex deployment:

npx convex env set DAYTONA_WEBHOOK_SECRET whsec_...

From then on, every state change Daytona makes shows up in getSandbox/listSandboxes as it happens. For example, listSandboxes(ctx, { state: "started" }) becomes an accurate live count. Every delivery's signature is verified (deliveries older than 5 minutes are rejected as possible replays). Duplicate and out-of-order deliveries are handled automatically. Events for sandboxes this deployment doesn't track are acknowledged and ignored, since an endpoint receives every sandbox in your organization. If the secret isn't set, the route refuses all deliveries.

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 a record on demand; webhooks keep all records in sync automatically.

See example/convex/example.ts for a complete example app.

Found a bug? Feature request? File it here.

Development

This package lives in the daytona/integrations monorepo under packages/convex and is self-contained (own package.json, lockfile, no workspace tooling).

git clone https://github.com/daytona/integrations
cd integrations/packages/convex
npm install

The component's src/component/_generated code is committed; regenerate it after changing component functions:

npx convex init           # one-time: bootstrap a local anonymous deployment
npm run codegen           # component codegen (src/component/_generated)
npx convex codegen        # example app codegen (example/convex/_generated)

Checks:

npm run build             # compile to dist/
npm run typecheck         # package sources
npm run typecheck:example # example app (needs codegen above)
npm test                  # offline unit tests (mocked Daytona API)
npm run test:live         # live E2E: local Convex deployment + real Daytona (needs DAYTONA_API_KEY)

npm run dev starts a Convex dev deployment serving the example app — set DAYTONA_API_KEY on it to exercise the component against real sandboxes.

Publishing

Releases are automated with release-please: merging this package's Release PR tags it and publishes to npm (public, with provenance) from the repo's release workflow.

License

Apache-2.0

版本与发布记录

@daytona/convex 共发布 6 个版本,首次发布于 2026/09/24;过去 12 个月发布了 6 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
2.2.0latest2026/11/038 天前
2.1.02026/10/101 个月前
2.0.02026/10/021 个月前
1.1.02026/09/301 个月前
1.0.02026/09/251 个月前
0.0.02026/09/241 个月前

关于 @daytona/convex 的常见问题

@daytona/convex 是什么?

Convex component for Daytona — create sandboxes, run commands and code, and manage sandbox files from your Convex backend, with sandbox and execution state tracked in reactive Convex tables

如何安装 @daytona/convex?

在项目目录运行 npm install @daytona/convex,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:convex(^1.43.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@daytona/convex 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@daytona/convex 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@daytona/convex 可以商用吗?

@daytona/convex 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@daytona/convex 的下载量有多少?

根据 npm 官方下载统计,@daytona/convex 在 2026/10/12 – 2026/11/10 这 30 天内被下载 4,220 次,日均约 141 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@daytona/convex 还在维护吗?

最新版本 2.2.0 发布于 2026/11/03(8 天前);至今共发布 6 个版本,其中过去 12 个月发布了 6 个;首次发布于 2026/09/24。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。