plans/nitro-integration.md
Generated by swarm planning session on 2026-03-27
Add an opt-in server layer (Nitro) to Vite/React apps in Dyad, giving them filesystem-based API routes that deploy as Vercel Functions. This closes the architectural gap between Next.js (which has built-in API routes) and Vite for database-backed apps — specifically unblocking Neon database integration for Vite projects, which requires server-side access to DATABASE_URL.
Dyad users building React/Vite apps have no server layer. This creates three concrete problems:
DATABASE_URL or other server-side credentials. The AI agent cannot generate server-side database code for Vite apps.DATABASE_URL with @neondatabase/serverless, which requires server-side code. The previously proposed workaround (Neon Data API + RLS) is non-standard and may require extra configuration from the users.Since Vite is the default template, this affects the majority of Dyad apps.
nitro to package.json and create server/routes/api/ directory; AI agent handles vite.config.ts modification via system promptnitro_prompt.ts with Nitro-specific coding patterns (filesystem routing, defineHandler, useRuntimeConfig(), server/routes/api/ convention)preset: "vercel") generates .vercel/output/ at build time, which Vercel understands nativelyvercel.json modification (deferred — Nitro's Build Output API takes precedence)As a Vite app developer who wants server-side logic, I want to enable backend API routes by flipping a switch and confirming, then have the AI assistant complete the Vite config setup on my next chat — so that I get a working server layer without manually configuring build tools.
As a Vite app developer deploying to Vercel, I want my API routes to automatically become Vercel Functions — so that deployment just works without extra configuration.
As a Vite+Supabase developer, I want the option to enable a server layer for custom API routes if I need it — but I don't want it forced on me since Supabase handles most server-side needs.
As a user who enabled Nitro and wants to remove it, I can ask the AI assistant to undo the setup in chat — so I'm not stuck with an irreversible action, even though the uncommon path is handled conversationally rather than with dedicated UI.
(Follow-up) As a Vite+Neon developer, I want the system to automatically enable the server layer when I connect Neon — so the AI agent can generate secure server-side database queries using DATABASE_URL.
vite.config.ts not configured → agent proactively adds Nitro plugin to vite.config.ts| State | Visual | Switch | Description |
|---|---|---|---|
| OFF | Card with description | Toggleable | "Add server-side API routes to your project." |
| ENABLING | Loader2 spinner | Disabled | "Setting up..." — scaffolding in progress |
| ON | Green "Active" badge | ON + disabled (permanent) | Shows "Open chat to finish setup" nudge, tip for first API route. Muted note: "To remove, ask the AI assistant in chat." |
| ERROR | Red alert + Retry button | Reverts to OFF | Error message with actionable retry CTA |
The LOCKED_ON state (Neon requires Nitro) is deferred to the follow-up PR when Neon auto-enable lands.
DialogContent max-w-sm with DialogHeader, DialogDescription, DialogFooter with Cancel/Confirm buttons)Switch from src/components/ui/switch.tsx, consistent with ~10 existing switches in codebasedisabled — no off affordance in UI. Note: "To remove, ask the AI assistant in chat"On the app-details page, positioned between SupabaseConnector and CapacitorControls:
GitHub Connector
Supabase Connector
→ Server Layer (Nitro) ← NEW
Capacitor Controls
Only rendered for Vite/React template apps (not Next.js, which already has a server layer).
aria-label="Enable backend API routes"aria-describedby pointing to explanation text ("To remove, ask the AI assistant")aria-busy="true" on the Cardprefers-reduced-motionNitro-enabled Vite app structure:
┌──────────────────────────────────────────┐
│ React SPA (client) │
│ fetch("/api/todos") → Nitro route │
├──────────────────────────────────────────┤
│ Nitro Server Layer │
│ server/routes/api/*.ts │
│ defineHandler from "nitro" │
│ useRuntimeConfig() for env vars │
├──────────────────────────────────────────┤
│ Vercel Functions (production) │
│ .vercel/output/ (Build Output API v3) │
└──────────────────────────────────────────┘
Hybrid setup approach:
package.json modification, server/routes/api/ directory creationvite.config.ts modification (adding Nitro plugin import + config)vercel.json — Nitro's Vercel preset generates .vercel/output/ which takes precedence| Component | File(s) | Change |
|---|---|---|
| DB Schema | src/db/schema.ts | Add nitroEnabled boolean column to apps table |
| IPC Handler | src/ipc/handlers/nitro_handlers.ts (new) | app:set-nitro-enabled — modify package.json, create server dir, update DB |
| IPC Types | src/ipc/types/nitro.ts (new) | Contract schema for app:set-nitro-enabled |
| App Details Page | src/pages/app-details.tsx | Add NitroToggle component (Vite apps only) |
| Nitro Toggle | src/components/NitroToggle.tsx (new) | Card + Switch + confirmation dialog + states |
| System Prompt | src/prompts/nitro_prompt.ts (new) | Nitro setup instructions + API route patterns |
| Chat Stream | src/ipc/handlers/chat_stream_handlers.ts | Inject Nitro prompt when nitroEnabled === true |
New column on apps table:
nitroEnabled: integer("nitro_enabled", { mode: "boolean" })
.notNull()
.default(sql`0`);
Simple boolean, backward-compatible. Defaults to false.
New IPC contract:
| Contract | Input | Output |
|---|---|---|
app:set-nitro-enabled | { appId: number, enabled: true } | { success: boolean } |
Handler logic (enable only):
package.json → add nitro (pinned version) to dependencies → write backserver/routes/api/.gitkeepnitroEnabled = true in DB (commit step — only after file ops succeed)installCommand)Rollback on failure: If any file operation fails, revert previously written files (read originals into memory before mutations). DB write is last, so nitroEnabled is never true if file setup failed.
nitro_prompt.ts)## Nitro Server Layer
The user has enabled backend API routes (Nitro) for this project.
### Setup Check
Check if `vite.config.ts` imports and uses the Nitro plugin.
If NOT present, update vite.config.ts to include:
1. Add import: `import { nitro } from "nitro/vite";`
2. Add `nitro({ preset: "vercel" })` to the plugins array
Example vite.config.ts:
[full example with nitro plugin in correct position]
### API Route Conventions
- Write routes in `server/routes/api/` (NEVER top-level `/api/`)
- Use `defineHandler` from "nitro" for handlers
- Dynamic routes: `[param].ts`
- Method-specific: `hello.get.ts`, `hello.post.ts`
- Runtime config: `useRuntimeConfig()` (env vars prefixed with `NITRO_`)
### Security Rules
NEVER import server-side code (database clients, secrets, env vars)
in client-side React components. Server code lives in `server/` only.
Critical: The prompt must detect the half-configured state (Nitro enabled in DB but plugin not in vite.config.ts) and proactively complete the setup on first chat interaction.
nitro({ preset: "vercel" }) in the plugin config.vercel/output/ (Build Output API v3) which Vercel understands nativelyvercel.json SPA rewrite should be harmless because Build Output API takes precedence when .vercel/output/ existsdetectFramework() in vercel_handlers.ts returns "vite" (correct — Vercel recognizes Vite+Nitro)vercel.json interferes, add vercel.json deletion/modification to the toggle handlernitroEnabled column to apps table in src/db/schema.tssrc/ipc/types/nitro.ts with contract schemasrc/ipc/handlers/nitro_handlers.ts with app:set-nitro-enabled handlersrc/components/NitroToggle.tsx — Card with Switch, confirmation dialog, loading/error/enabled statessrc/pages/app-details.tsx (between Supabase and Vercel connectors, Vite apps only)disabled once ON, with "To remove, ask AI" notesrc/prompts/nitro_prompt.ts with vite.config.ts setup check + API route conventions + security rulessrc/ipc/handlers/chat_stream_handlers.ts — inject when nitroEnabled === truevite build produces .vercel/output/vercel.json catch-all rewrite doesn't interfere with Nitro's Build Output APInitro/vite compatibilitydyadComponentTagger plugin doesn't conflict with Nitro pluginDATABASE_URL-based server routes via Nitroplans/neondb-integration.md to replace Data API approach with Nitro for Vite appsapp:set-nitro-enabled IPC handler — verify package.json modification, directory creation, DB statelocalhost:8080/api/*server/routes/api/*.ts with defineHandler pattern| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
vercel.json catch-all rewrite interferes with Nitro's Build Output API | Medium | High | Integration test verifies; fallback: delete/modify vercel.json in toggle handler |
Vite 6.x incompatible with nitro/vite plugin | Low | High | Pin compatible Nitro version; verify in integration test |
| AI agent fails to modify vite.config.ts correctly | Medium | Medium | System prompt includes exact example; half-configured state is recoverable via re-chat |
| pnpm install not triggered after package.json modification | Medium | Medium | Hook into existing install lifecycle; toast instructs user if needed |
dyadComponentTagger plugin conflicts with Nitro plugin | Low | Low | Test plugin ordering; Nitro should be first in plugins array |
| Dev server port conflict (Nitro vs Vite port 8080) | Low | Low | nitro/vite integrates into Vite's dev middleware; verify in integration test |
| Half-configured state confuses users | Medium | Medium | Card shows "Open chat to finish setup" nudge; prompt auto-completes config |
| Nitro version drift causes breaking changes | Low | Medium | Pin specific tested version in package.json modification |
nitro package auto-generate types in .nitro/? If not, may need server/tsconfig.json. Verify during Phase 4.DATABASE_URL injection during local dev (follow-up): When Neon is connected, how does the dev server get the connection string? Options: Dyad writes .env file, Dyad sets env var on process, or system prompt instructs agent. Resolve before Phase 5.| Decision | Reasoning |
|---|---|
| Hybrid setup (programmatic JSON + AI vite.config.ts) | JSON files are trivially safe to modify programmatically; TypeScript config is fragile and better handled contextually by AI agent. Mirrors Supabase client file pattern. |
| One-directional toggle (enable only, v1) | Disabling requires file cleanup with no codebase precedent. Avoids destructive edge cases. Removal via AI assistant is sufficient escape hatch. |
| Confirmation dialog on enable | Toggle modifies project files — heavier than a preference switch. Transparency principle requires user consent for file changes. |
| "Server Layer" label, not "Nitro" | Users think in capabilities ("I need API routes"), not tools ("I need Nitro"). "Powered by Nitro" as subtitle for power users. |
| Show on ALL Vite apps | Nitro is useful beyond databases (webhooks, API proxies, server logic). Gating behind database connection would artificially limit a general-purpose capability. |
| Standalone card, not nested under DB connector | Nitro is a server layer feature, not a database feature. Avoids false dependency. Follow-up PR adds visual link ("Required by Neon" badge). |
nitroEnabled boolean, not enum | YAGNI. Nitro is the only server layer for Vite. Migration from boolean to enum is trivial if ever needed. |
| Defer vercel.json modification | Nitro's Vercel preset generates .vercel/output/ which Build Output API understands natively. No need to touch vercel.json unless proven necessary. |
| Replace Data API approach in Neon plan | Nitro gives Vite apps conventional server-side access identical to Next.js. Data API + RLS is non-standard and harder for the AI agent to reason about. Update neondb-integration.md in follow-up. |
Generated by dyad:swarm-to-plan