plans/sandbox-engine-implementation-plan.md
Drafted on 2026-03-13
This document scopes the Dyad Engine work needed to support the new cloud sandbox runtime mode in the desktop app.
The desktop app now has a client-side cloud execution path:
runtimeMode2: "cloud"editAppFileWhat remains is the real backend implementation: authenticated sandbox lifecycle management, file upload, log streaming, usage limits, and cleanup.
The desktop app currently expects these Dyad Engine endpoints under DYAD_ENGINE_URL:
POST /sandboxesDELETE /sandboxes/:sandboxIdPOST /sandboxes/:sandboxId/filesGET /sandboxes/:sandboxId/logsCurrent response expectations:
POST /sandboxesRequest body:
{
"appId": 123,
"appPath": "/abs/path/to/app",
"installCommand": "pnpm install",
"startCommand": "pnpm run dev --port 4123"
}
Response body:
{
"sandboxId": "sbx_123",
"previewUrl": "https://sandbox-preview.example.com/sbx_123"
}
POST /sandboxes/:sandboxId/filesRequest body:
{
"files": {
"src/App.tsx": "export default function App() { return <div>Hello</div>; }"
}
}
Response body:
{
"previewUrl": "https://sandbox-preview.example.com/sbx_123"
}
GET /sandboxes/:sandboxId/logsdata: {"message":"..."} eventsdata: [DONE]Add an engine-side sandbox service with a narrow interface:
interface SandboxService {
create(input: CreateSandboxInput): Promise<CreateSandboxResult>;
uploadFiles(
input: UploadSandboxFilesInput,
): Promise<UploadSandboxFilesResult>;
streamLogs(sandboxId: string): AsyncIterable<SandboxLogEvent>;
destroy(sandboxId: string): Promise<void>;
reconcileForUser(userId: string): Promise<ReconcileResult>;
}
This service should own:
Start with a single Vercel-backed adapter behind the service:
interface SandboxProvider {
createSandbox(...): Promise<...>;
uploadFiles(...): Promise<...>;
streamLogs(...): AsyncIterable<...>;
destroySandbox(...): Promise<void>;
}
Even with one provider, keep this boundary. It aligns with Dyad’s backend-flexible principle and avoids leaking Vercel specifics into route handlers.
Store minimal sandbox metadata in the engine:
sandboxIdproviderSandboxIduserIdappIdstatuspreviewUrlcreatedAtlastActiveAtexpiresAtThis can live in the engine database or another lightweight persistent store. Persistence is needed for:
Implement:
POST /sandboxesDELETE /sandboxes/:sandboxIdPOST /sandboxes/:sandboxId/filesGET /sandboxes/:sandboxId/logsRequirements:
Implement:
GET /sandboxes/:sandboxId/statusPOST /sandboxes/reconcilereconcile should:
Enforce:
Server-side validation should include:
appId must be numericFor file uploads, normalize and reject:
../foo/etc/passwdUse stable structured errors so the desktop app can classify them later:
{
"code": "sandbox_limit_reached",
"message": "You already have an active cloud sandbox."
}
Suggested codes:
sandbox_auth_requiredsandbox_pro_requiredsandbox_limit_reachedsandbox_not_foundsandbox_not_ownedsandbox_provider_unavailablesandbox_create_failedsandbox_upload_failedsandbox_log_stream_failedsandbox_timeoutRecord at minimum:
Add correlation fields:
userIdsandboxIdproviderSandboxIdappIdShip engine endpoints behind a feature flag or allowlist.
Connect a staging desktop build to staging engine and validate:
Turn on for internal users first, then a small Dyad Pro cohort.
POST /sandboxes to accept an initial file batch to reduce round trips?logs remain SSE, or is WebSocket materially better for the provider integration?Implement the engine routes with a single provider adapter and a persisted sandbox metadata table, then point a staging desktop build at that environment for end-to-end validation.