plans/multi-platform-web-and-mobile.md
Generated by swarm planning session on 2026-02-14
Turn Dyad from a desktop-only Electron app into a multi-platform product supporting web browsers and mobile devices (via PWA), while preserving the desktop app as the free, local, open-source flagship. The web version runs on server-side containers with a freemium business model. Data syncs between platforms via GitHub (code) and proprietary cloud sync (settings, chat history, metadata).
Dyad is currently limited to users who download and install a desktop app on macOS, Windows, or Linux. This creates three problems:
Target users:
| Platform | Primary Use Case | Layout |
|---|---|---|
| Desktop | Full creation (chat, code, preview, deploy, git, local models) | Three-panel: sidebar + chat + preview |
| Web | Full creation (cloud-backed, same loop minus local-only features) | Three-panel: sidebar + chat + preview (no custom titlebar) |
| Mobile/PWA | Monitoring, reviewing, lightweight chat | Single-panel with bottom tab navigation |
AICreditStatus)DyadOutput/DyadLogs components with "Cloud Build" label to indicate remote origin| Interaction | Desktop | Web | Mobile/PWA |
|---|---|---|---|
| Navigation | Sidebar + keyboard shortcuts | Sidebar (collapsible) | Bottom tab bar (< 768px) |
| File attachment | Drag-drop + native dialog | Drag-drop + <input file> | Camera/photo library/file picker |
| Context menus | Right-click | Right-click | Long-press |
| Code editing | Monaco editor | Monaco editor | Read-only code viewer |
| Panel layout | Resizable multi-panel | Resizable multi-panel | Single panel, swipe/tab to switch |
| Preview | Local dev server in side panel | Remote container in side panel | Full-screen with toggle button |
| Window controls | Custom titlebar (macOS/Windows) | None (browser chrome) | None (OS chrome) |
| Settings | Local file pickers, node path selector | Cloud storage, account management, billing | Drill-down navigation pattern |
h-7/h-8 buttons upscaled via responsive classes)env(safe-area-inset-*) for mobile notches/home indicatorsprefers-reduced-motionOption A: Backend Service Extraction (selected)
Extract the Electron main process business logic into a standalone Node.js server. The existing IPC contracts (Zod schemas) become HTTP/WebSocket endpoints. Desktop Electron becomes a thin shell that spawns the local server. Web and mobile clients talk to the same API over HTTP.
Desktop: Electron shell -> Local Node.js server -> Local SQLite + Local filesystem
Web/PWA: Browser -> Cloud Node.js server -> Cloud DB + Container filesystem
Why this approach:
PlatformProvider context for capability-based conditional renderingcreateClient() in src/ipc/contracts/core.ts is the single function to swap (IPC invoke -> HTTP fetch)Must Change (Infrastructure):
| Component | Current | Target |
|---|---|---|
src/ipc/handlers/*.ts (40+ files) | ipcMain.handle() | Extract pure service functions; thin handler wrappers for both IPC and HTTP |
src/ipc/contracts/core.ts | window.electron.ipcRenderer.invoke() | HTTP fetch adapter for web, keep IPC for desktop |
src/preload.ts | Electron contextBridge | HTTP client for web |
src/db/index.ts | better-sqlite3 (local) | Same for desktop; server-side DB for web |
src/main/settings.ts | File JSON + safeStorage | Platform-agnostic SettingsProvider (safeStorage for desktop, encrypted cloud storage for web) |
src/paths/paths.ts | electron.app.getPath() | Abstract path provider |
src/main.ts | Full Electron app lifecycle | Thin Electron shell; business logic extracted |
src/ipc/utils/git_utils.ts | dugite + isomorphic-git | isomorphic-git for web; keep dugite for desktop |
src/ipc/utils/simpleSpawn.ts | child_process.spawn() | Remote container execution for web |
electron-log (all handlers) | Electron-specific logger | Platform-agnostic logger (pino) |
Unchanged (Portable):
src/components/** -- React UI (zero Electron refs)src/atoms/** -- Jotai statesrc/pages/**, src/routes/** -- TanStack Routersrc/db/schema.ts -- Drizzle ORM schema (fully portable)src/ipc/types/** -- Zod schemas (pure validation)Schema changes:
userId column to: apps, chats, messages, versions, prompts, themes, languageModels tables (for multi-tenant web)WHERE userId = ? to all queries in multi-tenant modedrizzle-kit generate)userId is always a fixed local-user ID (no behavior change)Database driver strategy:
better-sqlite3 (keep current, fast, synchronous)better-sqlite3 per-user (simple) or PostgreSQL (scalable) -- same Drizzle schema either waySettings storage:
safeStorage encryption (unchanged)File storage:
~/dyad-apps/) -- unchangedNew HTTP/WebSocket API layer:
IPC contracts transform mechanically:
IPC: ipcMain.handle("create-app", handler) -> HTTP: POST /api/create-app
IPC: ipcRenderer.invoke("create-app", data) -> HTTP: fetch("/api/create-app", {body: data})
Stream contracts transform to SSE:
StreamContract: onChunk/onEnd/onError -> SSE: event: chunk/end/error
New endpoints (web-specific):
POST /api/auth/login -- GitHub OAuth flowPOST /api/auth/session -- Session managementPOST /api/containers/create -- Provision container for projectDELETE /api/containers/:id -- Teardown containerGET /api/containers/:id/proxy/* -- Proxy to container dev serverPOST /api/sync/upload -- Upload local DB data to cloudGET /api/sync/download -- Download cloud data to localGET /api/usage -- Current usage vs. tier limitsAuthentication middleware:
Platform recommendation: Start with E2B (purpose-built AI code execution sandboxes)
Container lifecycle:
ContainerManager.provision(projectId, userId)npm install && npm run dev inside container/api/containers/:id/proxy/*Cost controls:
Code sync (GitHub -- already built):
github_handlers.ts with push/pull/clone/fetch/rebaseapps table already stores githubOrg, githubRepo, githubBranchEverything-else sync (new proprietary service):
apps (metadata only, not path), chats, messages, prompts, themes, languageModels, settingsmessages.aiMessagesJson can be large)Extract business logic from Electron main process into pure, testable service functions. No abstract interfaces -- just separation of concerns.
chat_stream_handlers.ts, chat_handlers.ts, app_handlers.ts, github_handlers.ts, settings_handlers.tselectron-log imports with platform-agnostic logger (pino) across all handler filesPlatformProvider context and expand useSystemPlatform hook to detect desktop/web/mobileBuild the web version with the full chat + build + preview + deploy experience.
Backend:
ipcRenderer.invoke() for webuserId columns to all user-facing tables; add tenant isolation middlewaresafeStorage with server-side encrypted secret storage for API keysContainerManager: provision, proxy, idle timeout, shutdown, file persistenceFrontend:
<input type="file">shell.openExternal() with window.open()DyadOutput)Unify desktop and web backends to prevent codebase divergence.
hasFilesystem, hasShell, hasLocalModels, etc.)Make the web version excellent on mobile devices.
env(safe-area-inset-*) for notches/home indicatorsEnable seamless work across desktop and web.
aiMessagesJson for old chats)| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Container infrastructure complexity delays Phase 1 | H | H | Start with managed platform (E2B) to minimize ops; self-host only at scale |
| Container costs exceed freemium revenue | M | H | Aggressive idle timeouts, free tier time limits, monitor cost-per-user closely |
| Two-codebase divergence (desktop vs. web backend) | H | M | Timebox: Phase 2 convergence must start within 6 months of web MVP launch |
| User trust concerns storing API keys in cloud | M | H | Clear trust indicators in UI, key rotation support, consider browser-side key storage with proxy pattern |
| isomorphic-git missing critical git features for web | M | M | Audit git usage early; fall back to server-side git proxy for unsupported operations |
| Freemium conversion < 5% makes web unsustainable | M | H | Track conversion from day one; adjust free tier limits; desktop download as fallback CTA |
| Container security (arbitrary npm install, user code execution) | M | H | Use gVisor/Firecracker sandboxing; network isolation; resource limits; security audit before public launch |
| Sync conflicts cause data loss | L | H | Last-write-wins with confirmation toast for low-stakes data; explicit conflict UI for high-stakes data; never auto-delete |
| PWA limitations frustrate mobile users | M | L | Monitor PWA capability gaps; Capacitor wrapping is the escape hatch if needed |
| Desktop development slows due to web investment | M | M | Run as parallel tracks; shared component library (Storybook already set up); convergence in Phase 2 |
| Decision | Options Considered | Chosen | Reasoning |
|---|---|---|---|
| Web preview approach | WebContainers, server-side containers, deploy-only (no preview) | Server-side containers | Preserves Dyad's core value prop (instant preview). WebContainers is Chromium-only and can't run shell commands. Deploy-only is too degraded. |
| Business model | Free web, paid-only web, freemium | Freemium | Desktop stays free/local (preserves open-source identity). Web Pro is the upsell. Freemium maximizes reach while generating revenue to cover infrastructure. |
| Mobile approach | Native (Capacitor), PWA, defer entirely | PWA first | Lowest cost, instant updates, no App Store friction. Mobile use case is lightweight (review, chat) which PWA handles well. Capacitor is the escape hatch if needed. |
| Data sync model | GitHub only, proprietary sync only, hybrid, no sync | Hybrid (GitHub for code + proprietary for everything else) | GitHub for code leverages existing infrastructure and aligns with "Bridge, Don't Replace." Proprietary sync for settings/metadata fills the gap GitHub can't cover. |
| Backend architecture | Cloud-only, local-only-everywhere, backend extraction (Option A) | Backend extraction | Preserves local-first for desktop. Same business logic serves both local (Electron) and cloud (web). IPC contracts map mechanically to HTTP. |
| Phase ordering | Abstraction first, web backend first, simultaneous | Service extraction first, then web backend | PM argued abstraction-first is premature. Eng agreed to drop TransportAdapter but keep service extraction. Build working web backend, then converge. |
| Database for web | sql.js (WASM in browser), server-side SQLite, PostgreSQL | Server-side DB | Web version is inherently cloud-based. No reason to run SQLite in browser when there's a server. Same Drizzle schema works with any driver. |
| Single vs. separate frontends | Single React codebase, separate frontends per platform | Single codebase | Frontend has zero direct Electron references. Platform differences handled via PlatformProvider context and responsive CSS. Separate frontends would diverge immediately. |
Generated by dyad:swarm-to-plan