plans/nitro-integration.md
Generated by swarm planning session on 2026-03-27
Add a server layer (Nitro) to Vite/React apps in Dyad through an AI-callable tool that the agent invokes automatically when the user's prompt indicates server-side needs (database access, secrets, API routes, webhooks). 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. Nitro-specific coding conventions are appended to the project's AI_RULES.md so all future chats know the patterns.
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.
enable_nitro AI tool — available in local-agent mode. The agent calls it when the user's prompt requires server-side capabilities (database access, secrets, API routes, webhooks, server logic).nitro to package.json, creates server/routes/api/ directory, appends Nitro rules to AI_RULES.md, and triggers install. The agent handles vite.config.ts modification in the same turn (guided by the newly-injected AI rules).AI_RULES.md modification — the tool appends a "Nitro Server Layer" section to the project's AI_RULES.md (or creates it if missing). This keeps all project-level coding conventions in a single, user-visible file.preset: "vercel") generates .vercel/output/ at build time, which Vercel understands nativelyenable_nitro → agent generates route → vite build produces .vercel/output/ → deploy to Vercel → verify API route respondsAI_RULES.md diff)vercel.json modification (deferred — Nitro's Build Output API takes precedence)As a Vite app developer who asks for server-side logic, I want the AI assistant to recognize my intent (e.g., "save form submissions to Postgres", "add a Stripe webhook") and automatically scaffold the server layer in the same turn — so I never have to know that "Nitro" is the underlying tool.
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 agent to skip Nitro when Supabase already covers my use case (anon-key fetch, RLS-protected queries) and only scaffold it when I explicitly need custom server logic — so I don't get unnecessary dependencies.
As a user who wants visibility into what the agent did, I want the appended AI_RULES.md section and the tool-call message in chat to clearly explain that a server layer was added — so the change isn't hidden behind a black-box tool call.
As a user who wants to remove Nitro, 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.
(Follow-up) As a Vite+Neon developer, I want the system to automatically call the enable_nitro tool when I connect Neon — so the AI agent can generate secure server-side database queries using DATABASE_URL.
/api/me endpoint that reads the session cookie"enable_nitro tool<dyad-enable-nitro />) similar to <dyad-add-integration>, so the user sees the scaffolding action in the message streampackage.json, creates server/routes/api/, appends a "Nitro Server Layer" section to AI_RULES.md, triggers installAI_RULES.md (already loaded as part of the local-agent context for the next tool-loop step) for the ongoing route/security conventions, and follows the post-call setup steps from the tool description for the one-time vite.config.ts editvite.config.ts with the Nitro plugin and writes the requested API route(s)/api/signup that writes to Postgres."enable_nitro is not registered in legacy build mode. Build mode uses the text-stream + dyad-write XML-tag pipeline (chat_stream_handlers.ts:1330), not real model tool calls. Two options for build-mode users in v1:
system_prompt.ts, not new code.<dyad-enable-nitro /> XML tag the way dyad-add-integration already works. Skipped because it duplicates logic and build mode is being phased toward local-agent.Encoded in the local-agent tool's description (the tool-selection logic the model reads), with a parallel hint in system_prompt.ts:
Trigger condition (Vite app + nitroEnabled === false) | Action |
|---|---|
Prompt requires server-side secrets (DATABASE_URL, API keys) | Call the tool |
| Prompt asks for an API route, webhook, or server endpoint | Call the tool |
| Prompt requests a database connection (Neon, Postgres, Mongo) | Call the tool |
| Prompt asks for server-side compute (cron, email, payments) | Call the tool |
Prompt only needs client-side fetch to a public 3rd-party API | Do NOT call |
| Use case fully covered by Supabase (anon key + RLS) | Do NOT call |
| User explicitly says "static only" or "no backend" | Do NOT call |
| App template is Next.js | Tool's isEnabled returns false |
nitroEnabled === true already | Tool's isEnabled returns false |
dyad-write / MCP tool UX) so the user sees what's being scaffolded.AI_RULES.md diff — User can review the appended section in their editor; this is the canonical record of what changed.disable-nitro tool in v1).Nitro-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 creation, AI_RULES.md patching, install triggervite.config.ts modification + writing the user-requested API route(s)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 (still needed for idempotency + future Neon auto-call) |
| AI Tool | src/pro/main/ipc/handlers/local_agent/tools/enable_nitro.ts (new) | enable_nitro tool definition matching the existing ToolDefinition shape (mirrors add_integration.ts for signaling, add_dependency.ts for the install flow). Fields: name, description (carries post-call setup steps), inputSchema, defaultConsent: "always", modifiesState: true, isEnabled: (ctx) => ctx.frameworkType === "vite" && !ctx.nitroEnabled, getConsentPreview, buildXml (renders <dyad-enable-nitro />), execute (the scaffolding logic) |
| Tool Registration | src/pro/main/ipc/handlers/local_agent/tool_definitions.ts | Add enable_nitro to TOOL_DEFINITIONS and to buildAgentToolSet. Pass nitroEnabled into the tool-execution ctx so isEnabled can evaluate (app template is already available via existing frameworkType field). |
| Tool Context | src/pro/main/ipc/handlers/local_agent/tools/types.ts | Extend the tool execution context type with nitroEnabled: boolean. Reuse the existing frameworkType: "nextjs" | "vite" | "other" | null field (already populated via detectFrameworkType(appPath) in local_agent_handler.ts) — do NOT add a parallel appTemplate field. |
| Chat UI renderer | src/components/chat/DyadMarkdownParser.tsx + src/components/chat/DyadEnableNitro.tsx (new) | Add dyad-enable-nitro to the DYAD_CUSTOM_TAGS allowlist and a case "dyad-enable-nitro": in renderCustomTag (mirror the dyad-add-integration case). Render a minimal status card — no interactive UI; the tool's execute already did the scaffolding. |
| System Prompt | src/prompts/system_prompt.ts + src/prompts/local_agent_prompt.ts | Local-agent prompt: reinforce the trigger table for enable_nitro selection. Build-mode prompt (system_prompt.ts): add the "switch to local-agent mode for backend" nudge for Vite apps. No new prompt file. |
| AI Rules (scaffold) | scaffold/AI_RULES.md | Unchanged at scaffold time. Patched per-app by the tool when called. |
| AI Rules (per-app) | <app>/AI_RULES.md | Tool appends a "## Nitro Server Layer" section (or creates the file if missing) with vite.config.ts setup, route conventions, security rules |
| AI Rules patcher | src/ipc/utils/ai_rules_patcher.ts (new) | Idempotent helper: read AI_RULES.md, check for marker comment, append Nitro section between <!-- nitro:start --> / <!-- nitro:end --> markers |
New column on apps table:
nitroEnabled: integer("nitro_enabled", { mode: "boolean" })
.notNull()
.default(sql`0`);
Simple boolean, backward-compatible. Defaults to false.
New AI tool (registered in the local-agent TOOL_DEFINITIONS):
Matches the existing ToolDefinition shape used by add_integration.ts. The tool description carries the one-time post-call setup steps the agent must execute in the same turn (modifying vite.config.ts). Ongoing conventions (route patterns, security rules) live in AI_RULES.md instead, because they apply to every future turn — not just the call that flipped Nitro on.
// src/pro/main/ipc/handlers/local_agent/tools/enable_nitro.ts
import { z } from "zod";
import { ToolDefinition } from "./types";
const enableNitroSchema = z.object({
reason: z
.string()
.describe(
"One sentence explaining why server-side code is needed for this prompt.",
),
});
export const enableNitroTool: ToolDefinition<
z.infer<typeof enableNitroSchema>
> = {
name: "enable_nitro",
description: `
Add a Nitro server layer to this Vite app so it can run secure server-side code
(API routes, database clients, secrets, webhooks).
WHEN TO CALL: Before writing any code under server/, before referencing DATABASE_URL
or any server-only env var, or when the user asks for an API route, webhook, or
server-side compute. Skip for client-side fetch with public/anon keys, for use
cases fully covered by Supabase (anon key + RLS), or when the user explicitly
says "static only" / "no backend".
This tool is auto-disabled (via isEnabled) on non-Vite apps and once Nitro is
already enabled — if it appears in your toolset, it is safe and appropriate to call.
==== POST-CALL SETUP STEPS (you MUST perform these in the same turn) ====
After this tool returns successfully, you MUST update vite.config.ts to register
the Nitro plugin. The tool itself does NOT touch vite.config.ts because TS config
files are fragile to edit programmatically.
1. Add the import:
import { nitro } from "nitro/vite";
2. Add nitro() to the plugins array. Place it AFTER react() so Vite's
module-transform middleware runs first — otherwise Nitro's SPA fallback
returns index.html for Vite internal URLs (/src/*.tsx, /@vite/client,
/@react-refresh, /@fs/*) and the preview iframe fails to load modules
with a "text/html MIME type" error.
Example final vite.config.ts:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { nitro } from "nitro/vite";
import { dyadComponentTagger } from "@dyad-sh/react-vite-component-tagger";
export default defineConfig({
plugins: [dyadComponentTagger(), react(), nitro()],
});
3. Then write the user-requested API route(s) following the conventions documented
in AI_RULES.md — the tool appended a "Nitro Server Layer" section with route
filesystem conventions, defineHandler usage, useRuntimeConfig patterns, and
security rules. That is the source of truth for ongoing code.
`.trim(),
inputSchema: enableNitroSchema,
defaultConsent: "always",
modifiesState: true,
isEnabled: (ctx) => ctx.frameworkType === "vite" && !ctx.nitroEnabled,
getConsentPreview: (args) => `Add Nitro server layer (${args.reason})`,
buildXml: (_args, _isComplete) => `<dyad-enable-nitro />`,
execute: async (_args, ctx) => {
/* see logic below */
},
};
Why isEnabled instead of a runtime guard inside execute: Mirrors the add_integration pattern — isEnabled removes the tool from the model's toolset entirely when it shouldn't be called, which is cheaper and clearer than a no-op execute path.
Tool execution logic (idempotent, enable-only):
nitroEnabled === true → return early "already enabled" message (no filesystem writes)AI_RULES.md in memory, then patch it via ai_rules_patcher.ts — append Nitro section between marker comments (idempotent on re-call)server/routes/api/.gitkeepnitro and vite packages (both bare — matches npm install nitro vite from the quick-start) by delegating to executeAddDependency({ packages: ["nitro", "vite"], message, appPath }) from src/ipc/processors/executeAddDependency.ts — same pattern as add_dependency.ts. This handles package.json modification + install atomically and surfaces warnings via ctx.onWarningMessage.nitro.config.ts at the app root with serverDir: "./server" if it does not already exist (matches the upstream quick-start).nitroEnabled = true in DB (commit step — only after file ops succeed)AI_RULES.md will be re-read on subsequent turns.Rollback on failure: Because executeAddDependency owns the package.json mutation atomically, the only in-memory backup we need is AI_RULES.md. If any step fails after the patch, restore the original AI_RULES.md from the backup and rethrow. DB write is last, so nitroEnabled is never true if file setup failed.
Tool registration logic (in local_agent/tool_definitions.ts):
Add enable_nitro to TOOL_DEFINITIONS and buildAgentToolSet. Visibility/availability is controlled by the tool's own isEnabled(ctx) (not by call-site filtering), matching the precedent set by add_integration and execute_sql. The tool execution context must include nitroEnabled: boolean; the existing frameworkType field (already populated for every turn) supplies the Vite vs. Next.js check. Once nitroEnabled flips true, isEnabled returns false and the tool drops out of the agent's toolset on the next turn — no second-guessing.
Build mode: not applicable. The legacy build-mode path (chat_stream_handlers.ts:1330) doesn't dispatch real tool calls; instead, system_prompt.ts adds a nudge instructing build-mode users to switch to local-agent mode when their prompt requires backend code. (See Build mode constraint in UX Design.)
nitro_prompt.ts)The tool appends this section to the project's AI_RULES.md. Setup steps are NOT here — those live in the tool description and run once. This section holds the conventions and security rules that govern every future turn (writing new routes, reviewing existing ones, refactoring, etc.):
<!-- nitro:start -->
## Nitro Server Layer
This project has a Nitro server layer for backend API routes. (Initial setup of `vite.config.ts` was performed when the server layer was added.)
A `nitro.config.ts` at the app root sets `serverDir: "./server"` — do not move or remove it.
### 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.
<!-- nitro:end -->
Why this split (setup in tool description, conventions in AI_RULES.md):
vite.config.ts instructions in the turn that calls the tool. Putting them in the tool description gives the model the steps exactly when it needs them, and avoids polluting future turns with stale setup instructions.AI_RULES.md is the right home: user-visible, already loaded by system_prompt.ts via readAiRules(), and survives across chats.readAiRules() flow picks up the appended section.Tool-selection rules are encoded primarily in the tool's own description field (the WHEN TO CALL block — see the tool definition above), which the local-agent model reads as part of the toolset. A short reinforcement is added to local_agent_prompt.ts for cross-tool prioritization context, but the canonical "when to call" guidance lives with the tool itself, since description is the most reliable place a model attends to before deciding to invoke. Build mode (legacy text-stream path) instead gets a "switch to local-agent mode for backend code" nudge in system_prompt.ts.
AI_RULES.md section hardcodes nitro({ preset: "vercel" }) in the plugin example.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 tool's execute functionnitroEnabled column to apps table in src/db/schema.ts (mirror the isFavorite pattern: integer("nitro_enabled", { mode: "boolean" }).notNull().default(sql0))pnpm drizzle-kit generate to auto-generate the next drizzle/NNNN_*.sql migration file — do NOT hand-write migrations; drizzle-kit owns this directorysrc/ipc/utils/ai_rules_patcher.ts — read <app>/AI_RULES.md, idempotently append the Nitro section between <!-- nitro:start --> / <!-- nitro:end --> markers, create the file if missing. Expose appendNitroRules(appPath) returning the original contents (for rollback) and restoreAiRules(appPath, backup) for failure recoverysrc/pro/main/ipc/handlers/local_agent/tools/enable_nitro.ts exporting enableNitroTool matching the existing ToolDefinition shape (mirror add_integration.ts)vite.config.ts setup steps with worked exampleisEnabled: (ctx) => ctx.frameworkType === "vite" && !ctx.nitroEnabled so the tool drops out of the toolset on non-Vite apps and after enablebuildXml: () => "<dyad-enable-nitro />" so the action renders in the chat streamexecute: patch AI_RULES.md via ai_rules_patcher (with in-memory backup), create server/routes/api/.gitkeep, install nitro via executeAddDependency({ packages: ["nitro"], message, appPath }), set nitroEnabled=true; on failure restore AI_RULES.md from backup (executeAddDependency owns package.json atomicity)local_agent/tools/types.ts with nitroEnabled: boolean (reuse existing frameworkType); wire nitroEnabled: chat.app.nitroEnabled in local_agent_handler.ts where ctx is constructedenable_nitro in local_agent/tool_definitions.ts (TOOL_DEFINITIONS + buildAgentToolSet)dyad-enable-nitro to the DYAD_CUSTOM_TAGS allowlist in src/components/chat/DyadMarkdownParser.tsx and a case "dyad-enable-nitro": in renderCustomTag (mirror the dyad-add-integration case); create src/components/chat/DyadEnableNitro.tsx for the minimal status-card renderenable_nitro selection priorities in src/prompts/local_agent_prompt.ts (canonical rules stay in the tool's description)src/prompts/system_prompt.ts: when the user prompt requires backend code, instruct the agent to ask the user to switch to local-agent mode (since legacy build mode can't dispatch the tool)nitro_prompt.ts.readAiRules() path picks up the appended Nitro section after the tool runs (it should — AI_RULES.md is re-read on the next tool-loop step / next chat invocation)enable_nitro → agent updates vite.config.ts and writes route → vite 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 plugin (rules dictate Nitro-last ordering so Vite's module-transform middleware runs before Nitro's SPA fallback)enable_nitro twice in one turn (e.g., due to a re-prompt) produces no duplicate editsenable_nitro (server-side, not via the agent) so the next chat starts with the server layer readyDATABASE_URL-based server routes via Nitroplans/neondb-integration.md to replace Data API approach with Nitro for Vite appsenable_nitro tool execute — verify package.json modification, directory creation, AI_RULES.md patch, DB statenitroEnabled === true returns early without filesystem writesai_rules_patcher — idempotent (run twice → identical file), preserves user content above/below markers, creates file if missingnitroEnabled=false, absent for Next.js apps and Vite apps with nitroEnabled=truelocalhost:8080/api/*| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
Agent fails to call enable_nitro when it should (false negative) | Medium | High | Detailed trigger table in system_prompt.ts; tool-selection eval catches regressions; user can re-prompt and the tool is still registered |
Agent calls enable_nitro when it shouldn't (false positive — adds dependency noise) | Medium | Medium | Negative cases in eval suite; tool description explicitly excludes Supabase/anon-key flows; user can revert via chat |
Agent forgets to update vite.config.ts after tool returns | Medium | Medium | Tool description carries the explicit vite.config.ts setup steps with a worked example, framed as a mandatory post-call action; eval covers the "tool called → vite.config.ts edited in same turn" case |
AI_RULES.md patcher corrupts user-written rules | Low | High | Marker-comment-bounded section; patcher only edits between markers; idempotency unit test; in-memory backup before write |
vercel.json catch-all rewrite interferes with Nitro's Build Output API | Medium | High | Integration test verifies; fallback: delete/modify vercel.json in tool's execute |
Vite 6.x incompatible with nitro/vite plugin | Low | High | Pin compatible Nitro version; verify in integration test |
| pnpm install not triggered after package.json modification | Medium | Medium | Hook into existing install lifecycle; tool-call message instructs user if needed |
dyadComponentTagger plugin conflicts with Nitro plugin | Low | Low | AI rules dictate Nitro-last ordering in the plugins array so Vite's module-transform middleware handles /src/*, /@vite/*, /@fs/* before Nitro's SPA fallback |
| Dev server port conflict (Nitro vs Vite port 8080) | Low | Low | nitro/vite integrates into Vite's dev middleware; verify in integration test |
| Nitro version drift causes breaking changes | Low | Medium | Pin specific tested version in package.json modification |
| User loses visibility (no UI surface) | Medium | Medium | Tool-call renders in chat like other agent tools; appended AI_RULES.md is user-visible and diffable; final agent message summarizes what changed |
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 |
|---|---|
| Agent-callable tool, not a UI toggle | Users describe goals ("save signups to Postgres"), not tools. The agent has full prompt context to make the right call. Removes a UI step the user shouldn't have to think about. UI toggle deferred — can be added later if eval shows the agent under-triggers. |
| Local-agent mode only (build mode gets a switch-mode nudge) | Real model-dispatched tool calls only flow through handleLocalAgentStream; legacy build mode uses text-stream + dyad-write XML tags. Adding a parallel build-mode XML scaffolder duplicates logic for a code path that's being phased out. A prompt-level nudge in build mode keeps users informed without forking the implementation. |
Tool gating via isEnabled(ctx), not call-site filtering | Matches the established add_integration / execute_sql precedent in the local-agent toolset. Removes the tool from the model's view entirely when not applicable, which is cheaper and clearer than a runtime execute no-op. |
Modify AI_RULES.md, not a separate nitro_prompt.ts | AI_RULES.md is already the canonical per-project rules surface, already loaded by system_prompt.ts via readAiRules(). User-visible and diffable. Avoids a parallel injection path that could drift. |
Marker-bounded section in AI_RULES.md | <!-- nitro:start --> / <!-- nitro:end --> makes the patcher idempotent and lets users edit around the section without conflict. |
Tool-selection rules live in system_prompt.ts, not AI_RULES.md | Per-project rules describe code conventions; tool-selection is a runtime behavior of the agent across all projects. Keeps the two layers conceptually clean. |
Setup steps in tool description, conventions in AI_RULES.md | Setup is a one-time post-call action (the model needs vite.config.ts instructions exactly when it calls the tool); conventions apply to every future turn (writing/reviewing routes). Splitting puts each piece where it's most reliably available without duplication. |
| 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. |
| Idempotent / enable-only | Disabling requires file cleanup with no codebase precedent. Avoids destructive edge cases. Removal via AI assistant in chat is sufficient escape hatch. |
nitroEnabled boolean, not enum | YAGNI. Nitro is the only server layer for Vite. Migration from boolean to enum is trivial if ever needed. Still required (not derived from filesystem) so the tool can be cheaply gated/un-registered. |
| Auto-deregister tool once enabled | Stops the model from second-guessing or re-calling. Cheaper than a no-op call. |
| 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