Back to Dyad

NeonDB Integration: First-Class Support for Next.js

plans/neondb-integration.md

0.44.033.4 KB
Original Source

NeonDB Integration: First-Class Support for Next.js

Generated by swarm planning session on 2026-02-17

Summary

Elevate Neon from a Portal-template-only experiment to a first-class database integration on par with Supabase. Users will be able to connect Neon to any Next.js app, with full AI agent support for schema introspection, SQL execution, and code generation — all without requiring the Portal template. Next.js apps use DATABASE_URL with server-side code (API routes, Server Actions).

Problem Statement

Today, Neon in Dyad is a second-class citizen:

  • Locked to the Portal template: Only portal-mini-store has requiresNeon: true — users cannot use Neon with Next.js templates
  • No agent tools: The AI agent has no execute_sql, get_table_schema, or get_project_info equivalents for Neon
  • No schema introspection: The agent cannot read Neon table schemas, so it cannot generate correct queries
  • Hub-page-only connector: NeonConnector lives on the hub page, not per-app like SupabaseConnector
  • Only creates projects: No way to link existing Neon projects (only neon:create-project exists)

This violates the Backend-Flexible design principle — users who want serverless Postgres without a full BaaS (Supabase) have no viable path in Dyad.

Scope

In Scope (MVP / v1)

  • Decouple Neon from Portal template — works with Next.js templates
  • Per-app Neon project selector on the app-details page (matching Supabase's pattern)
  • List existing Neon projects + create new projects
  • Full branch selector with color-coded badges (production/development/preview)
  • Agent tools: get_neon_project_info, get_neon_table_schema, execute_neon_sql
  • Neon context in agent prompt with schema awareness
  • add_integration tool support for provider: "neon"
  • System prompt covering Drizzle ORM setup, API routes (Next.js), connection patterns
  • Auth guidance: recommend Neon Auth — built-in auth powered by Better Auth, no external providers needed (no homegrown JWT+bcrypt)
  • <dyad-execute-sql> tag support routed to Neon when Neon is the active provider
  • Neon and Supabase mutually exclusive per app
  • Full stack example code in system prompt

Out of Scope (Follow-up / v2)

  • Multi-account Neon support (currently single-account; multi-org like Supabase can come later)
  • Migration file writing with enableNeonWriteSqlMigration flag
  • Agent-driven branch creation/deletion
  • Connection URI caching (fetch on-demand for v1)

User Stories

  1. As a user creating a new Next.js app, I want to connect my Neon account and have the AI agent set up my database schema, so that I get a working database-backed app without manual SQL or connection configuration.

  2. As a user with an existing Neon project, I want to link it to my Dyad app by selecting it from a dropdown, so that I don't have to create a new project or copy connection strings.

  3. As a user building a CRUD app, I want to tell the agent "add a tasks table and build a task list page" and have it execute the SQL on Neon and generate the React components, so that I get an end-to-end working feature in one step.

  4. As a user who chose Neon over Supabase, I want the AI to recommend Neon Auth for authentication, so that I get a secure, production-ready auth solution that lives in my Neon database and branches with it.

  5. As a user working on a feature branch, I want to select my Neon development branch and have the agent execute SQL against it, so that I can iterate safely without affecting production data.

UX Design

User Flow

First-Time Connection Flow

  1. User creates or opens a Next.js app
  2. In the app-details page, user sees "Connect Neon" card in the integrations section (same position as SupabaseConnector)
  3. User clicks "Connect Neon" → OAuth popup opens in browser
  4. User authorizes Dyad → popup closes, tokens stored via deep link
  5. Integration card updates to show project selector dropdown (grouped by organization)
    • Existing projects listed via neon:list-projects
    • "Create New Project" option at the top
  6. User selects or creates a project → project linked to app
  7. Branch selector appears with color-coded badges:
    • 🟢 Production (green)
    • 🔵 Development (blue, default selected)
    • 🟡 Preview (yellow)
  8. AI agent now has full Neon context — can execute SQL, read schemas, generate code

AI Agent Interaction Flow

  1. User asks "I need a database for my todo app"
  2. If Neon is NOT connected: agent renders <dyad-add-integration provider="neon"> prompt
  3. If Neon IS connected: agent uses get_neon_project_info to check existing tables
  4. Agent generates Drizzle schema, executes SQL via <dyad-execute-sql> tag
  5. Agent generates DB client + API routes / Server Actions (Next.js) + React components
  6. User sees SQL execution results in chat, previews updated app

Key States

StateDescriptionVisual
Not Connected"Connect to Neon" card with Neon logo and brief descriptionCard with connect button
Connected, No ProjectProject selector dropdown visible, loading projectsSelect component with skeleton
Connected, Project SelectedProject name in Badge, branch selector visible, "Open in Console" linkFull integration card
LoadingSkeleton placeholders for project/branch listsSkeleton components
ErrorRed text with retry button and actionable messageError state with CTA

Interaction Details

  • Branch selection: Changing the branch updates the agent's SQL target (connection string). Toast notification confirms: "Switched to development branch"
  • Project disconnect: Destructive button with confirmation. Clears neonProjectId from app record
  • External link: "Open in Neon Console" button opens the project in browser
  • Auth context: When user asks for auth, agent recommends Neon Auth — auth data stored in the Neon database, branches with the database

Accessibility

  • All interactive elements keyboard-focusable (connect button, Select dropdowns, branch selector)
  • Branch type badges have aria-label (e.g., "production branch", not just color)
  • Error messages associated with controls via aria-describedby
  • Status changes announced via aria-live region
  • Spinner animations respect prefers-reduced-motion

Technical Design

Architecture

Architecture pattern for generated Next.js apps:

Next.js (server-side access via DATABASE_URL):

┌─────────────────────────────────────────────┐
│ Layer 3: Auth (Neon Auth)                    │
├─────────────────────────────────────────────┤
│ Layer 2: Backend                             │
│  Next.js: API Routes / Server Actions        │
├─────────────────────────────────────────────┤
│ Layer 1: Database                            │
│  @neondatabase/serverless + Drizzle ORM      │
│  Connection via DATABASE_URL env var         │
└─────────────────────────────────────────────┘

Within Dyad itself (management plane):

┌─────────────────────────────────────────┐
│ Agent Tools (get_neon_*, execute_sql)    │
├─────────────────────────────────────────┤
│ Neon Context Module (neon_context.ts)    │
├─────────────────────────────────────────┤
│ Neon Management Client (existing)        │
│ @neondatabase/serverless (SQL execution) │
│ @neondatabase/api-client (project CRUD)  │
├─────────────────────────────────────────┤
│ IPC Handlers (neon_handlers.ts)          │
├─────────────────────────────────────────┤
│ Settings Storage (encrypted credentials) │
└─────────────────────────────────────────┘

Components Affected

ComponentFile(s)Change Type
Agent Contextsrc/pro/main/ipc/handlers/local_agent/tools/types.tsModify — add neonProjectId, neonDevelopmentBranchId, neonActiveBranchId, frameworkType
Agent Handlersrc/pro/main/ipc/handlers/local_agent/local_agent_handler.tsModify — populate Neon context from chat.app.*
Tool Definitionssrc/pro/main/ipc/handlers/local_agent/tool_definitions.tsModify — register 3 new Neon tools
Add Integrationsrc/pro/main/ipc/handlers/local_agent/tools/add_integration.tsModify — add "neon" to supported providers (Next.js)
Neon Contextsrc/neon_admin/neon_context.tsNewgetNeonClientCode(), getNeonContext(), getNeonProjectInfo(), getNeonTableSchema(), executeNeonSql() (extract SQL execution from src/ipc/utils/neon_timestamp_utils.ts)
Neon Project Info Toolsrc/pro/main/ipc/handlers/local_agent/tools/get_neon_project_info.tsNew — mirrors get_supabase_project_info.ts
Neon Table Schema Toolsrc/pro/main/ipc/handlers/local_agent/tools/get_neon_table_schema.tsNew — mirrors get_supabase_table_schema.ts
Execute Neon SQL Toolsrc/pro/main/ipc/handlers/local_agent/tools/execute_neon_sql.tsNew — mirrors execute_sql.ts with @neondatabase/serverless
Neon System Promptsrc/prompts/neon_prompt.tsNew — DB setup, API routes (Next.js), auth guidance, security rules
Chat Stream Handlerssrc/ipc/handlers/chat_stream_handlers.tsModify — inject Neon prompt when neonProjectId present
Response Processorsrc/ipc/processors/response_processor.tsModify — route <dyad-execute-sql> to Neon executor when Neon is active
Neon IPC Handlerssrc/ipc/handlers/neon_handlers.tsModify — add neon:list-projects, neon:set-app-project, neon:get-connection-uri, neon:execute-sql
Neon IPC Typessrc/ipc/types/neon.tsModify — add new contract schemas
App Details Pagesrc/pages/app-details.tsxModify — add NeonProjectSelector component
Neon Connectorsrc/components/NeonConnector.tsxRefactor — use Card components, add per-app project/branch selection
Neon Integrationsrc/components/NeonIntegration.tsxEnhance — show project info like SupabaseIntegration
DB Schemasrc/db/schema.tsModify — add neonActiveBranchId column

Data Model Changes

Existing columns (already in apps table — no migration needed):

  • neonProjectId: string | null
  • neonDevelopmentBranchId: string | null
  • neonPreviewBranchId: string | null

New column needed:

  • neonActiveBranchId: string | null — tracks which branch the agent's SQL execution targets (defaults to development branch)

Agent context additions:

typescript
interface AgentContext {
  // ... existing fields
  neonProjectId: string | null;
  neonDevelopmentBranchId: string | null;
  neonActiveBranchId: string | null;
  frameworkType: "nextjs" | "vite" | "other" | null; // Detected from appPath (e.g., check for next.config.*)
}

Dependencies for generated Next.js apps:

  • @neondatabase/auth (Neon Auth server SDK for Next.js)
  • @neondatabase/neon-js (Neon Auth client SDK — provides auth, auth/react/ui)
  • @neondatabase/serverless (serverless Postgres driver)
  • drizzle-orm + drizzle-kit (ORM and migrations)

API Changes

New IPC contracts in src/ipc/types/neon.ts:

ContractInputOutput
neon:list-projects{}{ projects: NeonProject[] }
neon:set-app-project{ appId, projectId, branchIds }{ success: boolean }
neon:unset-app-project{ appId }{ success: boolean }
neon:execute-sql{ appId, query }{ result: string }
neon:get-connection-uri{ appId }{ connectionUri: string }
neon:get-table-schema{ appId, tableName? }{ schema: string }
neon:set-active-branch{ appId, branchId }{ success: boolean }

Security Considerations

Next.js apps: Connection string is a full-access credential

Neon's DATABASE_URL connection string gives full read/write database access. The system prompt MUST:

  1. NEVER place DATABASE_URL in client-side code
  2. NEVER import @neondatabase/serverless in React components or browser code
  3. ONLY use the connection string in:
    • Next.js API routes (app/api/)
    • Next.js Server Actions
    • Next.js Server Components
    • Environment variables (.env.local, not .env)

Example Code: Next.js Full Stack Integration

Layer 1: Database Client (src/db/index.ts)

typescript
import { neon } from "@neondatabase/serverless";
import { drizzle } from "drizzle-orm/neon-http";
import * as schema from "./schema";

const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });

Layer 1: Schema Definition (src/db/schema.ts)

typescript
import { pgTable, uuid, text, timestamp, boolean } from "drizzle-orm/pg-core";

export const users = pgTable("users", {
  id: uuid("id").defaultRandom().primaryKey(),
  email: text("email").notNull().unique(),
  name: text("name"),
  createdAt: timestamp("created_at").defaultNow().notNull(),
});

export const todos = pgTable("todos", {
  id: uuid("id").defaultRandom().primaryKey(),
  title: text("title").notNull(),
  completed: boolean("completed").default(false).notNull(),
  userId: uuid("user_id")
    .references(() => users.id)
    .notNull(),
  createdAt: timestamp("created_at").defaultNow().notNull(),
});

Layer 2: Next.js API Route (app/api/todos/route.ts)

typescript
import { db } from "@/db";
import { todos } from "@/db/schema";
import { eq } from "drizzle-orm";
import { auth } from "@/lib/auth/server";
import { headers } from "next/headers";

export async function GET() {
  const session = await auth.getSession({ headers: await headers() });
  if (!session?.user?.id) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }

  const userTodos = await db
    .select()
    .from(todos)
    .where(eq(todos.userId, session.user.id));

  return Response.json(userTodos);
}

export async function POST(request: Request) {
  const session = await auth.getSession({ headers: await headers() });
  if (!session?.user?.id) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }

  const { title } = await request.json();
  const [newTodo] = await db
    .insert(todos)
    .values({ title, userId: session.user.id })
    .returning();

  return Response.json(newTodo, { status: 201 });
}

Layer 3: Auth Server Configuration (lib/auth/server.ts)

Neon Auth is a managed auth service powered by Better Auth. Auth data is stored in your Neon database and branches automatically with database branches.

typescript
import { createNeonAuth } from "@neondatabase/auth/next/server";

export const auth = createNeonAuth({
  baseUrl: process.env.NEON_AUTH_BASE_URL!,
  cookies: { secret: process.env.NEON_AUTH_COOKIE_SECRET! },
});

Layer 3: Auth Route Handler (app/api/auth/[...path]/route.ts)

typescript
import { auth } from "@/lib/auth/server";

export const { GET, POST } = auth.handler();

Layer 3: Client-Side Auth (src/lib/auth-client.ts)

typescript
import { createAuthClient } from "@neondatabase/neon-js/auth";

export const authClient = createAuthClient("/api/auth");
// Provides: useSession(), signIn.email(), signOut(), etc.

Layer 3: Auth UI Components (optional)

Neon Auth provides pre-built UI components for sign-in/sign-up:

tsx
import {
  NeonAuthUIProvider,
  AuthView,
} from "@neondatabase/neon-js/auth/react/ui";
import { authClient } from "@/lib/auth-client";

export function AuthPage() {
  return (
    <NeonAuthUIProvider authClient={authClient}>
      <AuthView pathname="sign-in" />
    </NeonAuthUIProvider>
  );
}

Frontend: React Component (src/components/TodoList.tsx)

typescript
"use client";

import { useEffect, useState } from "react";

interface Todo {
  id: string;
  title: string;
  completed: boolean;
}

export function TodoList() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [newTitle, setNewTitle] = useState("");

  useEffect(() => {
    fetch("/api/todos")
      .then((r) => r.json())
      .then(setTodos);
  }, []);

  async function addTodo(e: React.FormEvent) {
    e.preventDefault();
    const res = await fetch("/api/todos", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ title: newTitle }),
    });
    const todo = await res.json();
    setTodos([...todos, todo]);
    setNewTitle("");
  }

  return (
    <div>
      <form onSubmit={addTodo}>
        <input
          value={newTitle}
          onChange={(e) => setNewTitle(e.target.value)}
          placeholder="Add a todo..."
        />
        <button type="submit">Add</button>
      </form>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            {todo.title} {todo.completed ? "(done)" : ""}
          </li>
        ))}
      </ul>
    </div>
  );
}

Next.js Environment Variables (.env.local)

bash
# Neon Database (injected by Dyad)
DATABASE_URL=postgresql://user:[email protected]/dbname?sslmode=require

# Neon Auth (managed by Neon, values from Neon Console > Auth settings)
NEON_AUTH_BASE_URL=https://auth.neon.tech/...   # Auth service endpoint
NEON_AUTH_COOKIE_SECRET=your-cookie-secret-here  # Secret for session cookies

Neon Client Code Generated by Agent (for Dyad context)

Next.js pattern:

typescript
// getNeonClientCode() for Next.js:
// "To connect to the Neon database, use this pattern:
//
// import { neon } from '@neondatabase/serverless';
// import { drizzle } from 'drizzle-orm/neon-http';
//
// const sql = neon(process.env.DATABASE_URL!);
// export const db = drizzle(sql);
//
// IMPORTANT: Only use this in server-side code (API routes, server actions, server components).
// NEVER import @neondatabase/serverless in client-side React components."

Implementation Plan

Phase 1: Agent Plumbing + UI Foundation (Small effort)

  • Add neonProjectId, neonDevelopmentBranchId, neonActiveBranchId, frameworkType to AgentContext in types.ts
  • Populate Neon context fields in local_agent_handler.ts from chat.app.*
  • Populate frameworkType via framework detection (check for next.config.* in appPath, same pattern as vercel_handlers.ts)
  • Add "neon" to SUPPORTED_PROVIDERS in add_integration.ts (Next.js and React/Vite)
  • Add neonActiveBranchId column to apps table in schema.ts
  • Move/refactor NeonConnector to app-details page using Card components (match SupabaseConnector pattern)
  • Add neon:list-projects IPC handler (wire up API client's listProjects)
  • Add neon:set-app-project and neon:unset-app-project IPC handlers
  • Build NeonProjectSelector component with project dropdown + "Create New" option

Phase 2: Neon Context + SQL Execution (Medium effort)

  • Create src/neon_admin/neon_context.ts with:
    • executeNeonSql() — uses @neondatabase/serverless (extract from neon_timestamp_utils.ts)
    • getNeonProjectInfo() — project ID, branches, table names via information_schema
    • getNeonTableSchema() — columns, constraints, indexes via information_schema
    • getNeonClientCode(frameworkType) — generates framework-specific boilerplate: Drizzle + @neondatabase/serverless for Next.js, createClient + Data API for React/Vite
    • getNeonContext() — full context for agent prompt
  • Create get_neon_project_info.ts agent tool (mirrors get_supabase_project_info.ts)
  • Create get_neon_table_schema.ts agent tool (mirrors get_supabase_table_schema.ts)
  • Create execute_neon_sql.ts agent tool (mirrors execute_sql.ts, uses serverless driver)
  • Register all 3 tools in tool_definitions.ts
  • Add neon:execute-sql, neon:get-connection-uri, neon:get-table-schema IPC contracts
  • Update response_processor.ts to route <dyad-execute-sql> to Neon executor when neonProjectId is set
  • Add neon:set-active-branch IPC handler for branch switching

Phase 3: System Prompt + Branch UI (Medium effort)

  • Write src/prompts/neon_prompt.ts with framework-conditional sections:
    • Shared: Auth recommendation (Neon Auth), RLS policy templates, empty database first-run guidance, migration patterns
    • Next.js: Connection security rules (NEVER client-side DATABASE_URL), Drizzle ORM setup, API route / Server Action patterns, @neondatabase/auth server SDK + @neondatabase/neon-js client SDK
    • React/Vite: Data API setup (createClient<Database> with BetterAuthReactAdapter + auth/dataApi URLs), PostgREST-style query patterns (.from().select().eq(), .insert().select().single()), RLS policies with TO "authenticated" role required on all tables, @neondatabase/neon-js client SDK only (no server SDK), NEVER use DATABASE_URL or @neondatabase/serverless
  • Inject the correct prompt section based on frameworkType from AgentContext
  • Write NEON_NOT_AVAILABLE_SYSTEM_PROMPT (parallel to Supabase's)
  • Integrate Neon prompt into chat_stream_handlers.ts (conditional on neonProjectId)
  • Build branch selector UI in integration card with color-coded badges
  • Wire branch selection to neonActiveBranchId on app record
  • Ensure branch change updates connection URI for SQL execution
  • Add Neon-specific error messages in response processor (auth failure vs query failure)

Phase 4: Polish + Testing (Small-Medium effort)

  • Enhance NeonIntegration.tsx settings page (show project info, usage)
  • Handle Portal template migration (detect existing neonProjectId, show connected state)
  • Unit tests for neon_context.ts functions with mocked @neondatabase/serverless
  • Agent tool tests (enable/disable based on context)
  • E2E tests: connect flow, project selection, SQL execution, schema introspection
  • System prompt tests: verify Neon instructions injected correctly
  • Integration test: full flow from connect → schema → SQL → code generation

Testing Strategy

  • Unit: neon_context.ts functions with mocked @neondatabase/serverless and @neondatabase/api-client
  • Agent tools: Mock AgentContext with neonProjectId set/unset, verify tools enable/disable correctly
  • E2E (connect flow): Extend existing neon:fake-connect fixture for Next.js and React/Vite templates
  • E2E (SQL execution): Agent generates schema, executes <dyad-execute-sql>, verifies result
  • E2E (schema introspection): After table creation, verify get_neon_table_schema returns correct columns
  • System prompt: Verify Neon instructions injected when neonProjectId present, NOT when Supabase connected
  • Security (Next.js): Verify system prompt prevents client-side DATABASE_URL usage in generated code
  • Security (React/Vite): Verify system prompt enforces Data API + RLS pattern, never uses DATABASE_URL or @neondatabase/serverless
  • Branch switching: Verify SQL execution targets correct branch after branch change

Risks & Mitigations

RiskLikelihoodImpactMitigation
Connection string exposed in client codeMediumHighNext.js: system prompt rule #1 NEVER client-side. React/Vite: uses Data API instead, no connection string needed.
AI generates insecure homegrown authMediumHighPrompt explicitly forbids JWT+bcrypt, recommends Neon Auth (Better Auth) only. Auth data lives in Neon DB.
Neon free tier quota exhaustionLowMediumDocument in system prompt. Consider surfacing usage in get_neon_project_info.
Branch switching causes data confusionLowMediumToast notification on branch change. Agent prompt mentions active branch.
Portal template users lose existing connectionLowHighDetect existing neonProjectId in new flow, show connected state.
On-demand connection URI fetch latencyMediumLowAcceptable for v1 (~200-500ms). Cache in v2 if needed.
Missing RLS policies in React/Vite appsMediumHighData API exposes full table without RLS. System prompt must generate RLS policies for every table. Agent should run ALTER TABLE ... ENABLE ROW LEVEL SECURITY as part of table creation.
Data API not enabled on Neon projectMediumMediumAgent checks Data API status via get_neon_project_info. System prompt guides user to enable it in Neon Console.

Open Questions

  1. Neon free tier limits: Should get_neon_project_info surface current usage (storage, compute hours)? This would help users and the agent avoid heavy operations that burn through quotas.

  2. NeonConfigure preview panel: The existing NeonConfigure.tsx shows branch visualization in the preview panel for Portal apps. Should this be removed/replaced by the new branch selector in the integration card, or kept as a read-only status display?

  3. Mutual exclusivity enforcement: When a user has Supabase connected and tries to connect Neon (or vice versa), should we show a warning dialog, or silently hide the other provider's connect button?

Decision Log

DecisionReasoningAlternatives Considered
Next.js and React/Vite in v1Next.js uses DATABASE_URL server-side. React/Vite uses Neon Data API (managed REST proxy with JWT validation + RLS), eliminating the need for a server layer or exposed credentials.Next.js only (unnecessarily restrictive now that Data API exists)
Recommend Neon AuthNeon now provides built-in auth via Better Auth. Auth data stored in the Neon database, branches with database. No external auth providers needed.NextAuth.js (separate config + secrets); Clerk (external SaaS); homegrown JWT+bcrypt (security risk)
Full branch selector in v1Branching is Neon's key differentiator over Supabase. Color-coded badges provide clear visual hierarchy.Default to dev only (simpler); Read-only display (compromise)
Mutually exclusive providers per appAgent prompt can't cleanly handle both Supabase and Neon contexts. Avoids ambiguity in SQL execution target.Allow both (complex, no clear user value)
SQL execution in Dyad (management plane)Matches Supabase pattern. Agent needs to create tables and seed data during build. Credentials already stored.Only in generated app (limits agent capabilities)
Fetch connection URI on-demandConsistent with existing neon_timestamp_utils.ts pattern. Avoids credential rotation complexity.Cache (faster but more complex)
@neondatabase/serverless for SQLAlready a dependency. HTTP-based, works in Electron. Lower latency than Management API for queries.Management API SQL endpoint (higher latency, fewer features)

Generated by dyad:swarm-to-plan