Back to Dyad

Nitro Server Layer Support for Vite Projects

plans/nitro-integration.md

0.44.021.2 KB
Original Source

Nitro Server Layer Support for Vite Projects

Generated by swarm planning session on 2026-03-27

Summary

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.

Problem Statement

Dyad users building React/Vite apps have no server layer. This creates three concrete problems:

  1. No secure secrets management — Vite apps can't safely hold DATABASE_URL or other server-side credentials. The AI agent cannot generate server-side database code for Vite apps.
  2. Neon integration is blocked for Vite — Neon's standard connection pattern uses 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.
  3. No Vercel Functions — Without a server layer, Vite apps deployed to Vercel are static-only. Users who need webhook handlers, API proxies, or any server logic must switch to Next.js.

Since Vite is the default template, this affects the majority of Dyad apps.

Scope

In Scope (MVP — This PR)

  • Nitro toggle on app-details page — A one-directional switch (enable only) with confirmation dialog, shown only for Vite/React apps
  • Hybrid scaffolding — Programmatically add nitro to package.json and create server/routes/api/ directory; AI agent handles vite.config.ts modification via system prompt
  • System promptnitro_prompt.ts with Nitro-specific coding patterns (filesystem routing, defineHandler, useRuntimeConfig(), server/routes/api/ convention)
  • Vercel compatibility — Nitro's Vercel preset (preset: "vercel") generates .vercel/output/ at build time, which Vercel understands natively
  • Integration test — End-to-end: create Vite app → enable Nitro → chat to generate route → deploy to Vercel → verify API route responds

Out of Scope (Follow-up)

  • Making Nitro mandatory for Neon users (follow-up PR — auto-enable Nitro when Neon is connected to a Vite app)
  • Neon-specific agent tools or system prompts (separate Neon integration work)
  • Supabase-specific Nitro patterns (Supabase has its own Edge Functions)
  • Disabling Nitro from UI (v1 is enable-only; removal via AI assistant in chat)
  • vercel.json modification (deferred — Nitro's Build Output API takes precedence)
  • Multi-platform deployment presets (MVP hardcodes Vercel preset)
  • Modifying the default scaffold template (Nitro is added dynamically, not baked in)

User Stories

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. (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.

UX Design

User Flow

  1. User navigates to App Details page for their Vite project
  2. User sees "Server Layer" card in the integrations section (between Supabase and Vercel connectors)
  3. Card shows a Switch (OFF) with label "Enable backend API routes" and subtitle "Powered by Nitro"
  4. User flips switch ON
  5. Confirmation dialog appears:
    • Title: "Enable Server Layer?"
    • Body: "This will enable backend API routes for your app, adding dependencies and creating a server/routes/api/ directory. Your Vite config will be updated by the AI assistant on your next chat message."
    • Buttons: "Cancel" / "Enable"
  6. On confirm → scaffolding runs (package.json modification, directory creation, pnpm install)
  7. Switch shows loading state (disabled + Loader2 spinner, "Setting up...")
  8. On success → toast: "Server layer enabled"
  9. Card updates to enabled state with nudge: "Open chat to finish setup" button + tip: "Try asking: 'Create a /api/users endpoint'"
  10. User opens chat → AI agent detects Nitro enabled but vite.config.ts not configured → agent proactively adds Nitro plugin to vite.config.ts
  11. Server layer is fully operational

Key States

StateVisualSwitchDescription
OFFCard with descriptionToggleable"Add server-side API routes to your project."
ENABLINGLoader2 spinnerDisabled"Setting up..." — scaffolding in progress
ONGreen "Active" badgeON + disabled (permanent)Shows "Open chat to finish setup" nudge, tip for first API route. Muted note: "To remove, ask the AI assistant in chat."
ERRORRed alert + Retry buttonReverts to OFFError message with actionable retry CTA

The LOCKED_ON state (Neon requires Nitro) is deferred to the follow-up PR when Neon auto-enable lands.

Interaction Details

  • Confirmation dialog: Follows the existing Rename dialog pattern (DialogContent max-w-sm with DialogHeader, DialogDescription, DialogFooter with Cancel/Confirm buttons)
  • Switch component: Uses existing Switch from src/components/ui/switch.tsx, consistent with ~10 existing switches in codebase
  • One-directional: Once ON, the switch is disabled — no off affordance in UI. Note: "To remove, ask the AI assistant in chat"
  • Toast: Success/error feedback via sonner (consistent with SupabaseConnector)
  • "Open chat" button: Secondary button in the Card that navigates to chat, similar to existing "Open in Chat" pattern

Placement

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).

Accessibility

  • Switch: aria-label="Enable backend API routes"
  • Disabled state (when ON): aria-describedby pointing to explanation text ("To remove, ask the AI assistant")
  • Loading state: aria-busy="true" on the Card
  • Status badge: text label alongside color (not color-only)
  • All interactive elements keyboard-focusable
  • Loading spinner respects prefers-reduced-motion

Technical Design

Architecture

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:

  • Programmatic (IPC handler): package.json modification, server/routes/api/ directory creation
  • AI agent (system prompt): vite.config.ts modification (adding Nitro plugin import + config)
  • Deferred: vercel.json — Nitro's Vercel preset generates .vercel/output/ which takes precedence

Components Affected

ComponentFile(s)Change
DB Schemasrc/db/schema.tsAdd nitroEnabled boolean column to apps table
IPC Handlersrc/ipc/handlers/nitro_handlers.ts (new)app:set-nitro-enabled — modify package.json, create server dir, update DB
IPC Typessrc/ipc/types/nitro.ts (new)Contract schema for app:set-nitro-enabled
App Details Pagesrc/pages/app-details.tsxAdd NitroToggle component (Vite apps only)
Nitro Togglesrc/components/NitroToggle.tsx (new)Card + Switch + confirmation dialog + states
System Promptsrc/prompts/nitro_prompt.ts (new)Nitro setup instructions + API route patterns
Chat Streamsrc/ipc/handlers/chat_stream_handlers.tsInject Nitro prompt when nitroEnabled === true

Data Model Changes

New column on apps table:

typescript
nitroEnabled: integer("nitro_enabled", { mode: "boolean" })
  .notNull()
  .default(sql`0`);

Simple boolean, backward-compatible. Defaults to false.

API Changes

New IPC contract:

ContractInputOutput
app:set-nitro-enabled{ appId: number, enabled: true }{ success: boolean }

Handler logic (enable only):

  1. Read app's package.json → add nitro (pinned version) to dependencies → write back
  2. Create server/routes/api/.gitkeep
  3. Set nitroEnabled = true in DB (commit step — only after file ops succeed)
  4. Trigger install (hook into existing install mechanism via 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.

System Prompt Design (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.

Vercel Compatibility Strategy

  • System prompt hardcodes nitro({ preset: "vercel" }) in the plugin config
  • At build time, Nitro produces .vercel/output/ (Build Output API v3) which Vercel understands natively
  • The existing vercel.json SPA rewrite should be harmless because Build Output API takes precedence when .vercel/output/ exists
  • detectFramework() in vercel_handlers.ts returns "vite" (correct — Vercel recognizes Vite+Nitro)
  • Fallback: If integration test reveals vercel.json interferes, add vercel.json deletion/modification to the toggle handler

Implementation Plan

Phase 1: DB + IPC (Foundation)

  • Add nitroEnabled column to apps table in src/db/schema.ts
  • Create src/ipc/types/nitro.ts with contract schema
  • Create src/ipc/handlers/nitro_handlers.ts with app:set-nitro-enabled handler
  • Handler: read/modify package.json, create server/routes/api/, write DB, trigger install
  • Implement rollback logic (revert file changes if any step fails)

Phase 2: UI

  • Create src/components/NitroToggle.tsx — Card with Switch, confirmation dialog, loading/error/enabled states
  • Integrate NitroToggle into src/pages/app-details.tsx (between Supabase and Vercel connectors, Vite apps only)
  • One-directional: switch is disabled once ON, with "To remove, ask AI" note
  • "Open chat to finish setup" nudge button in enabled state

Phase 3: System Prompt

  • Create src/prompts/nitro_prompt.ts with vite.config.ts setup check + API route conventions + security rules
  • Integrate into src/ipc/handlers/chat_stream_handlers.ts — inject when nitroEnabled === true
  • Ensure prompt detects half-configured state and proactively completes vite.config.ts setup

Phase 4: Verification

  • Integration test: create Vite app → enable Nitro → chat to trigger vite.config.ts setup → generate sample API route → vite build produces .vercel/output/
  • Integration test: deploy Nitro-enabled Vite app to Vercel → verify API route responds
  • Verify vercel.json catch-all rewrite doesn't interfere with Nitro's Build Output API
  • Verify Vite 6.x + nitro/vite compatibility
  • Verify dyadComponentTagger plugin doesn't conflict with Nitro plugin
  • Verify dev server serves both React app and API routes on same port (8080)
  • Regression test: existing Vite apps without Nitro still work unchanged

Phase 5: Follow-up PR (Neon Auto-Enable)

  • Auto-enable Nitro when Neon is connected to a Vite app
  • Add LOCKED_ON state to NitroToggle (switch disabled with "Required by Neon" tooltip)
  • Update Neon system prompt to generate DATABASE_URL-based server routes via Nitro
  • Update plans/neondb-integration.md to replace Data API approach with Nitro for Vite apps
  • Per-app Neon connector on app-details page (must ship alongside locked state)

Testing Strategy

  • Unit test: app:set-nitro-enabled IPC handler — verify package.json modification, directory creation, DB state
  • Unit test: rollback on partial failure — simulate file write failure, verify clean state
  • Unit test: NitroToggle component — confirmation dialog, loading states, one-directional behavior
  • Integration test: full Nitro scaffolding + AI config + build + deploy pipeline
  • Integration test: dev server with Nitro — API routes accessible at localhost:8080/api/*
  • Regression test: Vite app without Nitro — toggle not shown for Next.js apps, existing apps unaffected
  • System prompt test: AI generates correct server/routes/api/*.ts with defineHandler pattern

Risks & Mitigations

RiskLikelihoodImpactMitigation
vercel.json catch-all rewrite interferes with Nitro's Build Output APIMediumHighIntegration test verifies; fallback: delete/modify vercel.json in toggle handler
Vite 6.x incompatible with nitro/vite pluginLowHighPin compatible Nitro version; verify in integration test
AI agent fails to modify vite.config.ts correctlyMediumMediumSystem prompt includes exact example; half-configured state is recoverable via re-chat
pnpm install not triggered after package.json modificationMediumMediumHook into existing install lifecycle; toast instructs user if needed
dyadComponentTagger plugin conflicts with Nitro pluginLowLowTest plugin ordering; Nitro should be first in plugins array
Dev server port conflict (Nitro vs Vite port 8080)LowLownitro/vite integrates into Vite's dev middleware; verify in integration test
Half-configured state confuses usersMediumMediumCard shows "Open chat to finish setup" nudge; prompt auto-completes config
Nitro version drift causes breaking changesLowMediumPin specific tested version in package.json modification

Open Questions

  • Nitro version: Which specific version to pin? Must be tested against Vite 6.x and the Vercel preset. Determine during Phase 1.
  • TypeScript types for server/: Does the 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 Log

DecisionReasoning
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 enableToggle 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 appsNitro 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 connectorNitro 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 enumYAGNI. Nitro is the only server layer for Vite. Migration from boolean to enum is trivial if ever needed.
Defer vercel.json modificationNitro'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 planNitro 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