Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions apps/web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,11 @@ VITE_FASTAPI_URL=http://localhost:8000

# Arcjet (server-only rate limiting / shield — copy from Arcjet dashboard)
ARCJET_KEY=

# Daytona (server-only — used by TanStack Start backend for browser-initiated sandbox CRUD)
DAYTONA_API_KEY=
DAYTONA_API_URL=https://app.daytona.io/api
DAYTONA_TARGET=us

# Convex deploy key (server-only — used by TanStack Start to call internal mutations)
CONVEX_DEPLOY_KEY=
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@arcjet/transport": "^1.3.1",
"@clerk/tanstack-react-start": "^0.29.1",
"@convex-dev/react-query": "^0.1.0",
"@daytonaio/sdk": "^0.171.0",
"@harness/convex-backend": "workspace:*",
"@t3-oss/env-core": "^0.13.8",
"@tailwindcss/typography": "^0.5.19",
Expand Down
16 changes: 15 additions & 1 deletion apps/web/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ export const env = createEnv({
server: {
SERVER_URL: z.string().url().optional(),
ARCJET_KEY: z.string().min(1),
// Optional at boot so dev still starts without these set. The
// sandbox lifecycle server functions validate at call time and
// return a clear "missing X" error if they're not configured.
DAYTONA_API_KEY: z.string().min(1).optional(),
DAYTONA_API_URL: z.string().url().default("https://app.daytona.io/api"),
DAYTONA_TARGET: z.string().default("us"),
// CONVEX_DEPLOY_KEY is deliberately NOT declared here. It is an admin
// credential that must never be reachable through any module that the
// client bundle might import. It is read via `process.env` directly
// inside server-only handlers (see `lib/sandbox-server.ts`).
},

/**
Expand All @@ -27,8 +37,12 @@ export const env = createEnv({
runtimeEnv: {
...import.meta.env,
SERVER_URL: process.env.SERVER_URL,
// Inlined by Vite's `define` in vite.config.ts at build time.
// Inlined by Vite's `define` in vite.config.ts at build time
// (process.env is empty in Cloudflare Workers).
ARCJET_KEY: process.env.ARCJET_KEY,
DAYTONA_API_KEY: process.env.DAYTONA_API_KEY,
DAYTONA_API_URL: process.env.DAYTONA_API_URL,
DAYTONA_TARGET: process.env.DAYTONA_TARGET,
},

/**
Expand Down
49 changes: 39 additions & 10 deletions apps/web/src/lib/sandbox-api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { env } from "../env";
import {
archiveSandbox as archiveSandboxFn,
deleteSandbox as deleteSandboxFn,
reconcileSandboxStatuses as reconcileSandboxStatusesFn,
startSandbox as startSandboxFn,
stopSandbox as stopSandboxFn,
syncSandbox as syncSandboxFn,
updateSandboxMetadata as updateSandboxMetadataFn,
} from "./sandbox-server";

const API_URL = env.VITE_FASTAPI_URL ?? "http://localhost:8000";

Expand Down Expand Up @@ -118,20 +127,40 @@ export function createSandboxApi(getToken: () => Promise<string | null>) {
});
},

// Lifecycle ops route through TanStack Start server functions so they
// hit Daytona via @daytonaio/sdk (Node) instead of FastAPI (Python).
// FastAPI is reserved for inference-time agent tool calls.
startSandbox(sandboxId: string) {
return sandboxFetch<SandboxLifecycleResponse>(
`/api/sandbox/${sandboxId}/start`,
getToken,
{ method: "POST" },
);
return startSandboxFn({ data: { sandboxId } });
},

stopSandbox(sandboxId: string) {
return sandboxFetch<SandboxLifecycleResponse>(
`/api/sandbox/${sandboxId}/stop`,
getToken,
{ method: "POST" },
);
return stopSandboxFn({ data: { sandboxId } });
},

archiveSandbox(sandboxId: string) {
return archiveSandboxFn({ data: { sandboxId } });
},

deleteSandbox(sandboxId: string) {
return deleteSandboxFn({ data: { sandboxId } });
},

updateSandbox(sandboxId: string, updates: { name?: string }) {
return updateSandboxMetadataFn({
data: { sandboxId, ...updates },
});
},

syncSandbox(sandboxId: string) {
return syncSandboxFn({ data: { sandboxId } });
},

// Reconciles every sandbox the caller owns against Daytona truth in
// one round-trip. Called on dashboard mount to catch drift caused by
// Daytona's idle auto-stop, LRU evictions, and admin actions.
reconcileSandboxStatuses() {
return reconcileSandboxStatusesFn();
},

listFiles(sandboxId: string, path = "/home/daytona") {
Expand Down
Loading
Loading