# 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 prompt** — `nitro_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

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

### 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

| 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`                            |

### 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:**

| Contract                | Input                              | Output                 |
| ----------------------- | ---------------------------------- | ---------------------- |
| `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

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

## 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

| 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_
